UserQuery.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * Contains the description of a user query
  5. *
  6. * It allows to extract the meaningful bits of the query to be manipulated in an
  7. * easy way.
  8. */
  9. class FreshRSS_UserQuery {
  10. private bool $deprecated = false;
  11. private string $get = '';
  12. private string $get_name = '';
  13. private string $get_type = '';
  14. private string $name = '';
  15. private string $order = '';
  16. private FreshRSS_BooleanSearch $search;
  17. private int $state = 0;
  18. private string $url = '';
  19. private string $token = '';
  20. private bool $shareRss = false;
  21. private bool $shareOpml = false;
  22. /** @var array<int,FreshRSS_Category> $categories */
  23. private array $categories;
  24. /** @var array<int,FreshRSS_Tag> $labels */
  25. private array $labels;
  26. public static function generateToken(string $salt): string {
  27. if (!FreshRSS_Context::hasSystemConf()) {
  28. return '';
  29. }
  30. $hash = md5(FreshRSS_Context::systemConf()->salt . $salt . random_bytes(16));
  31. if (function_exists('gmp_init')) {
  32. // Shorten the hash if possible by converting from base 16 to base 62
  33. $hash = gmp_strval(gmp_init($hash, 16), 62);
  34. }
  35. return $hash;
  36. }
  37. /**
  38. * @param array{get?:string,name?:string,order?:string,search?:string,state?:int,url?:string,token?:string,shareRss?:bool,shareOpml?:bool} $query
  39. * @param array<int,FreshRSS_Category> $categories
  40. * @param array<int,FreshRSS_Tag> $labels
  41. */
  42. public function __construct(array $query, array $categories, array $labels) {
  43. $this->categories = $categories;
  44. $this->labels = $labels;
  45. if (isset($query['get'])) {
  46. $this->parseGet($query['get']);
  47. } else {
  48. $this->get_type = 'all';
  49. }
  50. if (isset($query['name'])) {
  51. $this->name = trim($query['name']);
  52. }
  53. if (isset($query['order'])) {
  54. $this->order = $query['order'];
  55. }
  56. if (empty($query['url'])) {
  57. if (!empty($query)) {
  58. unset($query['name']);
  59. $this->url = Minz_Url::display(['params' => $query]);
  60. }
  61. } else {
  62. $this->url = $query['url'];
  63. }
  64. if (!isset($query['search'])) {
  65. $query['search'] = '';
  66. }
  67. if (!empty($query['token'])) {
  68. $this->token = $query['token'];
  69. }
  70. if (isset($query['shareRss'])) {
  71. $this->shareRss = $query['shareRss'];
  72. }
  73. if (isset($query['shareOpml'])) {
  74. $this->shareOpml = $query['shareOpml'];
  75. }
  76. // linked too deeply with the search object, need to use dependency injection
  77. $this->search = new FreshRSS_BooleanSearch($query['search'], 0, 'AND', false);
  78. if (!empty($query['state'])) {
  79. $this->state = intval($query['state']);
  80. }
  81. }
  82. /**
  83. * Convert the current object to an array.
  84. *
  85. * @return array{'get'?:string,'name'?:string,'order'?:string,'search'?:string,'state'?:int,'url'?:string,'token'?:string}
  86. */
  87. public function toArray(): array {
  88. return array_filter([
  89. 'get' => $this->get,
  90. 'name' => $this->name,
  91. 'order' => $this->order,
  92. 'search' => $this->search->getRawInput(),
  93. 'state' => $this->state,
  94. 'url' => $this->url,
  95. 'token' => $this->token,
  96. 'shareRss' => $this->shareRss,
  97. 'shareOpml' => $this->shareOpml,
  98. ]);
  99. }
  100. /**
  101. * Parse the get parameter in the query string to extract its name and type
  102. */
  103. private function parseGet(string $get): void {
  104. $this->get = $get;
  105. if ($this->get === '') {
  106. $this->get_type = 'all';
  107. } elseif (preg_match('/(?P<type>[acfistT])(_(?P<id>\d+))?/', $get, $matches)) {
  108. $id = intval($matches['id'] ?? '0');
  109. switch ($matches['type']) {
  110. case 'a':
  111. $this->get_type = 'all';
  112. break;
  113. case 'c':
  114. $this->get_type = 'category';
  115. $c = $this->categories[$id] ?? null;
  116. $this->get_name = $c === null ? '' : $c->name();
  117. break;
  118. case 'f':
  119. $this->get_type = 'feed';
  120. $f = FreshRSS_Category::findFeed($this->categories, $id);
  121. $this->get_name = $f === null ? '' : $f->name();
  122. break;
  123. case 'i':
  124. $this->get_type = 'important';
  125. break;
  126. case 's':
  127. $this->get_type = 'favorite';
  128. break;
  129. case 't':
  130. $this->get_type = 'label';
  131. $l = $this->labels[$id] ?? null;
  132. $this->get_name = $l === null ? '' : $l->name();
  133. break;
  134. case 'T':
  135. $this->get_type = 'all_labels';
  136. break;
  137. }
  138. if ($this->get_name === '' && in_array($matches['type'], ['c', 'f', 't'], true)) {
  139. $this->deprecated = true;
  140. }
  141. }
  142. }
  143. /**
  144. * Check if the current user query is deprecated.
  145. * It is deprecated if the category or the feed used in the query are
  146. * not existing.
  147. */
  148. public function isDeprecated(): bool {
  149. return $this->deprecated;
  150. }
  151. /**
  152. * Check if the user query has parameters.
  153. */
  154. public function hasParameters(): bool {
  155. if ($this->get_type !== 'all') {
  156. return true;
  157. }
  158. if ($this->hasSearch()) {
  159. return true;
  160. }
  161. if (!in_array($this->state, [
  162. 0,
  163. FreshRSS_Entry::STATE_READ | FreshRSS_Entry::STATE_NOT_READ,
  164. FreshRSS_Entry::STATE_READ | FreshRSS_Entry::STATE_NOT_READ | FreshRSS_Entry::STATE_FAVORITE | FreshRSS_Entry::STATE_NOT_FAVORITE
  165. ], true)) {
  166. return true;
  167. }
  168. if ($this->order !== '' && $this->order !== FreshRSS_Context::userConf()->sort_order) {
  169. return true;
  170. }
  171. return false;
  172. }
  173. /**
  174. * Check if there is a search in the search object
  175. */
  176. public function hasSearch(): bool {
  177. return $this->search->getRawInput() !== '';
  178. }
  179. public function getGet(): string {
  180. return $this->get;
  181. }
  182. public function getGetName(): string {
  183. return $this->get_name;
  184. }
  185. public function getGetType(): string {
  186. return $this->get_type;
  187. }
  188. public function getName(): string {
  189. return $this->name;
  190. }
  191. public function getOrder(): string {
  192. return $this->order ?: FreshRSS_Context::userConf()->sort_order;
  193. }
  194. public function getSearch(): FreshRSS_BooleanSearch {
  195. return $this->search;
  196. }
  197. public function getState(): int {
  198. $state = $this->state;
  199. if (!($state & FreshRSS_Entry::STATE_READ) && !($state & FreshRSS_Entry::STATE_NOT_READ)) {
  200. $state |= FreshRSS_Entry::STATE_READ | FreshRSS_Entry::STATE_NOT_READ;
  201. }
  202. if (!($state & FreshRSS_Entry::STATE_FAVORITE) && !($state & FreshRSS_Entry::STATE_NOT_FAVORITE)) {
  203. $state |= FreshRSS_Entry::STATE_FAVORITE | FreshRSS_Entry::STATE_NOT_FAVORITE;
  204. }
  205. return $state;
  206. }
  207. public function getUrl(): string {
  208. return $this->url;
  209. }
  210. public function getToken(): string {
  211. return $this->token;
  212. }
  213. public function setToken(string $token): void {
  214. $this->token = $token;
  215. }
  216. public function setShareRss(bool $shareRss): void {
  217. $this->shareRss = $shareRss;
  218. }
  219. public function shareRss(): bool {
  220. return $this->shareRss;
  221. }
  222. public function setShareOpml(bool $shareOpml): void {
  223. $this->shareOpml = $shareOpml;
  224. }
  225. public function shareOpml(): bool {
  226. return $this->shareOpml;
  227. }
  228. protected function sharedUrl(bool $xmlEscaped = true): string {
  229. $currentUser = Minz_User::name() ?? '';
  230. return Minz_Url::display("/api/query.php?user={$currentUser}&t={$this->token}", $xmlEscaped ? 'html' : '', true);
  231. }
  232. public function sharedUrlRss(bool $xmlEscaped = true): string {
  233. if ($this->shareRss && $this->token !== '') {
  234. return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=rss';
  235. }
  236. return '';
  237. }
  238. public function sharedUrlHtml(bool $xmlEscaped = true): string {
  239. if ($this->shareRss && $this->token !== '') {
  240. return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=html';
  241. }
  242. return '';
  243. }
  244. /**
  245. * OPML is only safe for some query types, otherwise it risks leaking unwanted feed information.
  246. */
  247. public function safeForOpml(): bool {
  248. return in_array($this->get_type, ['all', 'category', 'feed'], true);
  249. }
  250. public function sharedUrlOpml(bool $xmlEscaped = true): string {
  251. if ($this->shareOpml && $this->token !== '' && $this->safeForOpml()) {
  252. return $this->sharedUrl($xmlEscaped) . ($xmlEscaped ? '&amp;' : '&') . 'f=opml';
  253. }
  254. return '';
  255. }
  256. }