Search.php 49 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483
  1. <?php
  2. declare(strict_types=1);
  3. require_once LIB_PATH . '/lib_date.php';
  4. /**
  5. * Contains a search from the search form.
  6. *
  7. * It allows to extract meaningful bits of the search and store them in a
  8. * convenient object
  9. */
  10. class FreshRSS_Search implements \Stringable {
  11. /**
  12. * This contains the user input string
  13. */
  14. private string $raw_input = '';
  15. // The following properties are extracted from the raw input
  16. /** @var list<string>|null */
  17. private ?array $entry_ids = null;
  18. /** @var list<int>|null */
  19. private ?array $feed_ids = null;
  20. /** @var list<int>|null */
  21. private ?array $category_ids = null;
  22. /** @var list<list<int>|'*'>|null */
  23. private $label_ids = null;
  24. /** @var list<list<string>>|null */
  25. private ?array $label_names = null;
  26. /** @var list<list<int>|'*'>|null */
  27. private $user_query_ids = null;
  28. /** @var list<list<string>>|null */
  29. private ?array $user_query_names = null;
  30. /** @var list<string>|null */
  31. private ?array $intitle = null;
  32. /** @var list<string>|null */
  33. private ?array $intitle_regex = null;
  34. /** @var list<string>|null */
  35. private ?array $intext = null;
  36. /** @var list<string>|null */
  37. private ?array $intext_regex = null;
  38. private ?string $input_date = null;
  39. private int|false|null $min_date = null;
  40. private int|false|null $max_date = null;
  41. private ?string $input_pubdate = null;
  42. private int|false|null $min_pubdate = null;
  43. private int|false|null $max_pubdate = null;
  44. private ?string $input_userdate = null;
  45. private int|false|null $min_userdate = null;
  46. private int|false|null $max_userdate = null;
  47. private ?string $input_modified_date = null;
  48. private int|false|null $min_modified_date = null;
  49. private int|false|null $max_modified_date = null;
  50. /** @var list<string>|null */
  51. private ?array $inurl = null;
  52. /** @var list<string>|null */
  53. private ?array $inurl_regex = null;
  54. /** @var list<string>|null */
  55. private ?array $author = null;
  56. /** @var list<string>|null */
  57. private ?array $author_regex = null;
  58. /** @var list<string>|null */
  59. private ?array $tags = null;
  60. /** @var list<string>|null */
  61. private ?array $tags_regex = null;
  62. /** @var list<string>|null */
  63. private ?array $search = null;
  64. /** @var list<string>|null */
  65. private ?array $search_regex = null;
  66. /** @var list<string>|null */
  67. private ?array $not_entry_ids = null;
  68. /** @var list<int>|null */
  69. private ?array $not_feed_ids = null;
  70. /** @var list<int>|null */
  71. private ?array $not_category_ids = null;
  72. /** @var list<list<int>|'*'>|null */
  73. private $not_label_ids = null;
  74. /** @var list<list<string>>|null */
  75. private ?array $not_label_names = null;
  76. /** @var list<list<int>|'*'>|null */
  77. private $not_user_query_ids = null;
  78. /** @var list<list<string>>|null */
  79. private ?array $not_user_query_names = null;
  80. /** @var list<string>|null */
  81. private ?array $not_intitle = null;
  82. /** @var list<string>|null */
  83. private ?array $not_intitle_regex = null;
  84. /** @var list<string>|null */
  85. private ?array $not_intext = null;
  86. /** @var list<string>|null */
  87. private ?array $not_intext_regex = null;
  88. private ?string $input_not_date = null;
  89. private int|false|null $not_min_date = null;
  90. private int|false|null $not_max_date = null;
  91. private ?string $input_not_pubdate = null;
  92. private int|false|null $not_min_pubdate = null;
  93. private int|false|null $not_max_pubdate = null;
  94. private ?string $input_not_userdate = null;
  95. private int|false|null $not_min_userdate = null;
  96. private int|false|null $not_max_userdate = null;
  97. private ?string $input_not_modified_date = null;
  98. private int|false|null $not_min_modified_date = null;
  99. private int|false|null $not_max_modified_date = null;
  100. /** @var list<string>|null */
  101. private ?array $not_inurl = null;
  102. /** @var list<string>|null */
  103. private ?array $not_inurl_regex = null;
  104. /** @var list<string>|null */
  105. private ?array $not_author = null;
  106. /** @var list<string>|null */
  107. private ?array $not_author_regex = null;
  108. /** @var list<string>|null */
  109. private ?array $not_tags = null;
  110. /** @var list<string>|null */
  111. private ?array $not_tags_regex = null;
  112. /** @var list<string>|null */
  113. private ?array $not_search = null;
  114. /** @var list<string>|null */
  115. private ?array $not_search_regex = null;
  116. public function __construct(string $input) {
  117. $input = self::cleanSearch($input);
  118. $input = self::unescape($input);
  119. $input = FreshRSS_BooleanSearch::unescapeLiterals($input);
  120. $this->raw_input = $input;
  121. $input = $this->parseNotEntryIds($input);
  122. $input = $this->parseNotFeedIds($input);
  123. $input = $this->parseNotCategoryIds($input);
  124. $input = $this->parseNotLabelIds($input);
  125. $input = $this->parseNotLabelNames($input);
  126. $input = $this->parseNotUserQueryIds($input);
  127. $input = $this->parseNotUserQueryNames($input);
  128. $input = $this->parseNotUserdateSearch($input);
  129. $input = $this->parseNotModifiedDateSearch($input);
  130. $input = $this->parseNotPubdateSearch($input);
  131. $input = $this->parseNotDateSearch($input);
  132. $input = $this->parseNotIntitleSearch($input);
  133. $input = $this->parseNotIntextSearch($input);
  134. $input = $this->parseNotAuthorSearch($input);
  135. $input = $this->parseNotInurlSearch($input);
  136. $input = $this->parseNotTagsSearch($input);
  137. $input = $this->parseEntryIds($input);
  138. $input = $this->parseFeedIds($input);
  139. $input = $this->parseCategoryIds($input);
  140. $input = $this->parseLabelIds($input);
  141. $input = $this->parseLabelNames($input);
  142. $input = $this->parseUserQueryIds($input);
  143. $input = $this->parseUserQueryNames($input);
  144. $input = $this->parseUserdateSearch($input);
  145. $input = $this->parseModifiedDateSearch($input);
  146. $input = $this->parsePubdateSearch($input);
  147. $input = $this->parseDateSearch($input);
  148. $input = $this->parseIntitleSearch($input);
  149. $input = $this->parseIntextSearch($input);
  150. $input = $this->parseAuthorSearch($input);
  151. $input = $this->parseInurlSearch($input);
  152. $input = $this->parseTagsSearch($input);
  153. $input = $this->parseQuotedSearch($input);
  154. $input = $this->parseNotSearch($input);
  155. $this->parseSearch($input);
  156. }
  157. private static function quote(string $s): string {
  158. if (str_starts_with($s, 'S:') || str_starts_with($s, 'search:')) {
  159. // Discard user queries
  160. return $s;
  161. }
  162. if (strpbrk($s, ' "\'\\/:') !== false || $s === '') {
  163. return '"' . addcslashes($s, '"') . '"';
  164. }
  165. return $s;
  166. }
  167. // TODO: Reuse as option for a string representation resolving and expanding date intervals
  168. // private static function dateIntervalToString(int|false|null $min, int|false|null $max): string {
  169. // if ($min === false) {
  170. // $min = null;
  171. // }
  172. // if ($max === false) {
  173. // $max = null;
  174. // }
  175. // if ($min === null && $max === null) {
  176. // return '';
  177. // }
  178. // $s = '';
  179. // if ($min !== null) {
  180. // $s .= date('Y-m-d\\TH:i:s', $min);
  181. // }
  182. // $s .= '/';
  183. // if ($max !== null) {
  184. // $s .= date('Y-m-d\\TH:i:s', $max);
  185. // }
  186. // return $s;
  187. // }
  188. /**
  189. * Return true if both searches have the same constraint parameters (even if the values differ), false otherwise.
  190. */
  191. public function hasSameOperators(FreshRSS_Search $search): bool {
  192. $properties = array_keys(get_object_vars($this));
  193. $properties = array_diff($properties, ['raw_input']); // raw_input is not a constraint parameter
  194. foreach ($properties as $property) {
  195. // @phpstan-ignore property.dynamicName, property.dynamicName
  196. if (gettype($this->$property) !== gettype($search->$property)) {
  197. if (is_string($property) && (str_contains($property, 'min_') || str_contains($property, 'max_'))) {
  198. // Process {min_*, max_*} pairs together (for dates)
  199. $mate = str_contains($property, 'min_') ? str_replace('min_', 'max_', $property) : str_replace('max_', 'min_', $property);
  200. // @phpstan-ignore property.dynamicName, property.dynamicName, property.dynamicName, property.dynamicName
  201. if (($this->$property !== null || $this->$mate !== null) !== ($search->$property !== null || $search->$mate !== null)) {
  202. return false;
  203. }
  204. } else {
  205. return false;
  206. }
  207. }
  208. // @phpstan-ignore property.dynamicName, property.dynamicName
  209. if (is_array($this->$property) && is_array($search->$property)) {
  210. // @phpstan-ignore property.dynamicName, property.dynamicName
  211. if (count($this->$property) !== count($search->$property)) {
  212. return false;
  213. }
  214. }
  215. }
  216. return true;
  217. }
  218. /**
  219. * Modifies this search by enforcing the constraint parameters of another search.
  220. * @return FreshRSS_Search a new instance, modified.
  221. */
  222. public function enforce(FreshRSS_Search $search): self {
  223. $result = clone $this;
  224. $properties = array_keys(get_object_vars($result));
  225. $properties = array_diff($properties, ['raw_input']); // raw_input is not a constraint parameter
  226. $result->raw_input = '';
  227. foreach ($properties as $property) {
  228. // @phpstan-ignore property.dynamicName
  229. if ($search->$property !== null) {
  230. // @phpstan-ignore property.dynamicName, property.dynamicName
  231. $result->$property = $search->$property;
  232. if (is_string($property) && (str_contains($property, 'min_') || str_contains($property, 'max_'))) {
  233. // Process {min_*, max_*} pairs together (for dates)
  234. $mate = str_contains($property, 'min_') ? str_replace('min_', 'max_', $property) : str_replace('max_', 'min_', $property);
  235. // @phpstan-ignore property.dynamicName, property.dynamicName
  236. $result->$mate = $search->$mate;
  237. }
  238. }
  239. }
  240. return $result;
  241. }
  242. /**
  243. * Modifies this search by removing the constraints given by another search.
  244. * @return FreshRSS_Search a new instance, modified.
  245. */
  246. public function remove(FreshRSS_Search $search): self {
  247. $result = clone $this;
  248. $properties = array_keys(get_object_vars($result));
  249. $properties = array_diff($properties, ['raw_input']); // raw_input is not a constraint parameter
  250. $result->raw_input = '';
  251. foreach ($properties as $property) {
  252. // @phpstan-ignore property.dynamicName
  253. if ($search->$property !== null) {
  254. // @phpstan-ignore property.dynamicName
  255. $result->$property = null;
  256. if (is_string($property) && (str_contains($property, 'min_') || str_contains($property, 'max_'))) {
  257. // Process {min_*, max_*} pairs together (for dates)
  258. $mate = str_contains($property, 'min_') ? str_replace('min_', 'max_', $property) : str_replace('max_', 'min_', $property);
  259. // @phpstan-ignore property.dynamicName
  260. $result->$mate = null;
  261. }
  262. }
  263. }
  264. return $result;
  265. }
  266. #[\Override]
  267. public function __toString(): string {
  268. $result = '';
  269. if ($this->entry_ids !== null) {
  270. $result .= ' e:' . implode(',', $this->entry_ids);
  271. }
  272. if ($this->feed_ids !== null) {
  273. $result .= ' f:' . implode(',', $this->feed_ids);
  274. }
  275. if ($this->category_ids !== null) {
  276. $result .= ' c:' . implode(',', $this->category_ids);
  277. }
  278. if ($this->label_ids !== null) {
  279. foreach ($this->label_ids as $ids) {
  280. $result .= ' L:' . (is_array($ids) ? implode(',', $ids) : $ids);
  281. }
  282. }
  283. if ($this->label_names !== null) {
  284. foreach ($this->label_names as $names) {
  285. $result .= ' labels:' . self::quote(implode(',', $names));
  286. }
  287. }
  288. if ($this->input_userdate !== null) {
  289. $result .= ' userdate:' . $this->input_userdate;
  290. }
  291. if ($this->input_modified_date !== null) {
  292. $result .= ' mdate:' . $this->input_modified_date;
  293. }
  294. if ($this->input_pubdate !== null) {
  295. $result .= ' pubdate:' . $this->input_pubdate;
  296. }
  297. if ($this->input_date !== null) {
  298. $result .= ' date:' . $this->input_date;
  299. }
  300. if ($this->intitle_regex !== null) {
  301. foreach ($this->intitle_regex as $s) {
  302. $result .= ' intitle:' . $s;
  303. }
  304. }
  305. if ($this->intitle !== null) {
  306. foreach ($this->intitle as $s) {
  307. $result .= ' intitle:' . self::quote($s);
  308. }
  309. }
  310. if ($this->intext_regex !== null) {
  311. foreach ($this->intext_regex as $s) {
  312. $result .= ' intext:' . $s;
  313. }
  314. }
  315. if ($this->intext !== null) {
  316. foreach ($this->intext as $s) {
  317. $result .= ' intext:' . self::quote($s);
  318. }
  319. }
  320. if ($this->author_regex !== null) {
  321. foreach ($this->author_regex as $s) {
  322. $result .= ' author:' . $s;
  323. }
  324. }
  325. if ($this->author !== null) {
  326. foreach ($this->author as $s) {
  327. $result .= ' author:' . self::quote($s);
  328. }
  329. }
  330. if ($this->inurl_regex !== null) {
  331. foreach ($this->inurl_regex as $s) {
  332. $result .= ' inurl:' . $s;
  333. }
  334. }
  335. if ($this->inurl !== null) {
  336. foreach ($this->inurl as $s) {
  337. $result .= ' inurl:' . self::quote($s);
  338. }
  339. }
  340. if ($this->tags_regex !== null) {
  341. foreach ($this->tags_regex as $s) {
  342. $result .= ' #' . $s;
  343. }
  344. }
  345. if ($this->tags !== null) {
  346. foreach ($this->tags as $s) {
  347. $result .= ' #' . self::quote($s);
  348. }
  349. }
  350. if ($this->search_regex !== null) {
  351. foreach ($this->search_regex as $s) {
  352. $result .= ' ' . $s;
  353. }
  354. }
  355. if ($this->search !== null) {
  356. foreach ($this->search as $s) {
  357. $result .= ' ' . self::quote($s);
  358. }
  359. }
  360. if ($this->user_query_ids !== null) {
  361. foreach ($this->user_query_ids as $ids) {
  362. $result .= ' S:' . (is_array($ids) ? implode(',', $ids) : $ids);
  363. }
  364. }
  365. if ($this->user_query_names !== null) {
  366. foreach ($this->user_query_names as $names) {
  367. $result .= ' search:' . self::quote(implode(',', $names));
  368. }
  369. }
  370. if ($this->not_entry_ids !== null) {
  371. $result .= ' -e:' . implode(',', $this->not_entry_ids);
  372. }
  373. if ($this->not_feed_ids !== null) {
  374. $result .= ' -f:' . implode(',', $this->not_feed_ids);
  375. }
  376. if ($this->not_category_ids !== null) {
  377. $result .= ' -c:' . implode(',', $this->not_category_ids);
  378. }
  379. if ($this->not_label_ids !== null) {
  380. foreach ($this->not_label_ids as $ids) {
  381. $result .= ' -L:' . (is_array($ids) ? implode(',', $ids) : $ids);
  382. }
  383. }
  384. if ($this->not_label_names !== null) {
  385. foreach ($this->not_label_names as $names) {
  386. $result .= ' -labels:' . self::quote(implode(',', $names));
  387. }
  388. }
  389. if ($this->input_not_userdate !== null) {
  390. $result .= ' -userdate:' . $this->input_not_userdate;
  391. }
  392. if ($this->input_not_modified_date !== null) {
  393. $result .= ' -mdate:' . $this->input_not_modified_date;
  394. }
  395. if ($this->input_not_pubdate !== null) {
  396. $result .= ' -pubdate:' . $this->input_not_pubdate;
  397. }
  398. if ($this->input_not_date !== null) {
  399. $result .= ' -date:' . $this->input_not_date;
  400. }
  401. if ($this->not_intitle_regex !== null) {
  402. foreach ($this->not_intitle_regex as $s) {
  403. $result .= ' -intitle:' . $s;
  404. }
  405. }
  406. if ($this->not_intitle !== null) {
  407. foreach ($this->not_intitle as $s) {
  408. $result .= ' -intitle:' . self::quote($s);
  409. }
  410. }
  411. if ($this->not_intext_regex !== null) {
  412. foreach ($this->not_intext_regex as $s) {
  413. $result .= ' -intext:' . $s;
  414. }
  415. }
  416. if ($this->not_intext !== null) {
  417. foreach ($this->not_intext as $s) {
  418. $result .= ' -intext:' . self::quote($s);
  419. }
  420. }
  421. if ($this->not_author_regex !== null) {
  422. foreach ($this->not_author_regex as $s) {
  423. $result .= ' -author:' . $s;
  424. }
  425. }
  426. if ($this->not_author !== null) {
  427. foreach ($this->not_author as $s) {
  428. $result .= ' -author:' . self::quote($s);
  429. }
  430. }
  431. if ($this->not_inurl_regex !== null) {
  432. foreach ($this->not_inurl_regex as $s) {
  433. $result .= ' -inurl:' . $s;
  434. }
  435. }
  436. if ($this->not_inurl !== null) {
  437. foreach ($this->not_inurl as $s) {
  438. $result .= ' -inurl:' . self::quote($s);
  439. }
  440. }
  441. if ($this->not_tags_regex !== null) {
  442. foreach ($this->not_tags_regex as $s) {
  443. $result .= ' -#' . $s;
  444. }
  445. }
  446. if ($this->not_tags !== null) {
  447. foreach ($this->not_tags as $s) {
  448. $result .= ' -#' . self::quote($s);
  449. }
  450. }
  451. if ($this->not_search_regex !== null) {
  452. foreach ($this->not_search_regex as $s) {
  453. $result .= ' -' . $s;
  454. }
  455. }
  456. if ($this->not_search !== null) {
  457. foreach ($this->not_search as $s) {
  458. $result .= ' -' . self::quote($s);
  459. }
  460. }
  461. if ($this->not_user_query_ids !== null) {
  462. foreach ($this->not_user_query_ids as $ids) {
  463. $result .= ' -S:' . (is_array($ids) ? implode(',', $ids) : $ids);
  464. }
  465. }
  466. if ($this->not_user_query_names !== null) {
  467. foreach ($this->not_user_query_names as $names) {
  468. $result .= ' -search:' . self::quote(implode(',', $names));
  469. }
  470. }
  471. return trim($result);
  472. }
  473. #[Deprecated('Use __toString() instead')]
  474. public function getRawInput(): string {
  475. return $this->raw_input;
  476. }
  477. /** @return list<string>|null */
  478. public function getEntryIds(): ?array {
  479. return $this->entry_ids;
  480. }
  481. /** @return list<string>|null */
  482. public function getNotEntryIds(): ?array {
  483. return $this->not_entry_ids;
  484. }
  485. /** @return list<int>|null */
  486. public function getFeedIds(): ?array {
  487. return $this->feed_ids;
  488. }
  489. /** @return list<int>|null */
  490. public function getNotFeedIds(): ?array {
  491. return $this->not_feed_ids;
  492. }
  493. /** @return list<int>|null */
  494. public function getCategoryIds(): ?array {
  495. return $this->category_ids;
  496. }
  497. /** @return list<int>|null */
  498. public function getNotCategoryIds(): ?array {
  499. return $this->not_category_ids;
  500. }
  501. /** @return list<list<int>|'*'>|null */
  502. public function getLabelIds(): array|null {
  503. return $this->label_ids;
  504. }
  505. /** @return list<list<int>|'*'>|null */
  506. public function getNotLabelIds(): array|null {
  507. return $this->not_label_ids;
  508. }
  509. /** @return list<list<string>>|null */
  510. public function getLabelNames(bool $plaintext = false): ?array {
  511. return $plaintext ? $this->label_names : Minz_Helper::htmlspecialchars_utf8($this->label_names, ENT_NOQUOTES);
  512. }
  513. /** @return list<list<string>>|null */
  514. public function getNotLabelNames(bool $plaintext = false): ?array {
  515. return $plaintext ? $this->not_label_names : Minz_Helper::htmlspecialchars_utf8($this->not_label_names, ENT_NOQUOTES);
  516. }
  517. /** @return list<string>|null */
  518. public function getIntitle(bool $plaintext = false): ?array {
  519. return $plaintext ? $this->intitle : Minz_Helper::htmlspecialchars_utf8($this->intitle, ENT_NOQUOTES);
  520. }
  521. /** @return list<string>|null */
  522. public function getIntitleRegex(): ?array {
  523. return $this->intitle_regex;
  524. }
  525. /** @return list<string>|null */
  526. public function getNotIntitle(bool $plaintext = false): ?array {
  527. return $plaintext ? $this->not_intitle : Minz_Helper::htmlspecialchars_utf8($this->not_intitle, ENT_NOQUOTES);
  528. }
  529. /** @return list<string>|null */
  530. public function getNotIntitleRegex(): ?array {
  531. return $this->not_intitle_regex;
  532. }
  533. /** @return list<string>|null */
  534. public function getIntext(bool $plaintext = false): ?array {
  535. return $plaintext ? $this->intext : Minz_Helper::htmlspecialchars_utf8($this->intext, ENT_NOQUOTES);
  536. }
  537. /** @return list<string>|null */
  538. public function getIntextRegex(): ?array {
  539. return $this->intext_regex;
  540. }
  541. /** @return list<string>|null */
  542. public function getNotIntext(bool $plaintext = false): ?array {
  543. return $plaintext ? $this->not_intext : Minz_Helper::htmlspecialchars_utf8($this->not_intext, ENT_NOQUOTES);
  544. }
  545. /** @return list<string>|null */
  546. public function getNotIntextRegex(): ?array {
  547. return $this->not_intext_regex;
  548. }
  549. public function getMinDate(): ?int {
  550. return $this->min_date ?: null;
  551. }
  552. public function getNotMinDate(): ?int {
  553. return $this->not_min_date ?: null;
  554. }
  555. public function setMinDate(int $value): void {
  556. $this->min_date = $value;
  557. }
  558. public function getMaxDate(): ?int {
  559. return $this->max_date ?: null;
  560. }
  561. public function getNotMaxDate(): ?int {
  562. return $this->not_max_date ?: null;
  563. }
  564. public function setMaxDate(int $value): void {
  565. $this->max_date = $value;
  566. }
  567. public function getMinPubdate(): ?int {
  568. return $this->min_pubdate ?: null;
  569. }
  570. public function getNotMinPubdate(): ?int {
  571. return $this->not_min_pubdate ?: null;
  572. }
  573. public function getMaxPubdate(): ?int {
  574. return $this->max_pubdate ?: null;
  575. }
  576. public function getNotMaxPubdate(): ?int {
  577. return $this->not_max_pubdate ?: null;
  578. }
  579. public function setMaxPubdate(int $value): void {
  580. $this->max_pubdate = $value;
  581. }
  582. public function getMinUserdate(): ?int {
  583. return $this->min_userdate ?: null;
  584. }
  585. public function getNotMinUserdate(): ?int {
  586. return $this->not_min_userdate ?: null;
  587. }
  588. public function setMinUserdate(int $value): void {
  589. $this->min_userdate = $value;
  590. }
  591. public function getMaxUserdate(): ?int {
  592. return $this->max_userdate ?: null;
  593. }
  594. public function getNotMaxUserdate(): ?int {
  595. return $this->not_max_userdate ?: null;
  596. }
  597. public function setMaxUserdate(int $value): void {
  598. $this->max_userdate = $value;
  599. }
  600. public function getMinModifiedDate(): ?int {
  601. return $this->min_modified_date ?: null;
  602. }
  603. public function getNotMinModifiedDate(): ?int {
  604. return $this->not_min_modified_date ?: null;
  605. }
  606. public function setMinModifiedDate(int $value): void {
  607. $this->min_modified_date = $value;
  608. }
  609. public function getMaxModifiedDate(): ?int {
  610. return $this->max_modified_date ?: null;
  611. }
  612. public function getNotMaxModifiedDate(): ?int {
  613. return $this->not_max_modified_date ?: null;
  614. }
  615. public function setMaxModifiedDate(int $value): void {
  616. $this->max_modified_date = $value;
  617. }
  618. /** @return list<string>|null */
  619. public function getInurl(bool $plaintext = false): ?array {
  620. return $plaintext ? $this->inurl : Minz_Helper::htmlspecialchars_utf8($this->inurl, ENT_NOQUOTES);
  621. }
  622. /** @return list<string>|null */
  623. public function getInurlRegex(): ?array {
  624. return $this->inurl_regex;
  625. }
  626. /** @return list<string>|null */
  627. public function getNotInurl(bool $plaintext = false): ?array {
  628. return $plaintext ? $this->not_inurl : Minz_Helper::htmlspecialchars_utf8($this->not_inurl, ENT_NOQUOTES);
  629. }
  630. /** @return list<string>|null */
  631. public function getNotInurlRegex(): ?array {
  632. return $this->not_inurl_regex;
  633. }
  634. /** @return list<string>|null */
  635. public function getAuthor(bool $plaintext = false): ?array {
  636. return $plaintext ? $this->author : Minz_Helper::htmlspecialchars_utf8($this->author, ENT_NOQUOTES);
  637. }
  638. /** @return list<string>|null */
  639. public function getAuthorRegex(): ?array {
  640. return $this->author_regex;
  641. }
  642. /** @return list<string>|null */
  643. public function getNotAuthor(bool $plaintext = false): ?array {
  644. return $plaintext ? $this->not_author : Minz_Helper::htmlspecialchars_utf8($this->not_author, ENT_NOQUOTES);
  645. }
  646. /** @return list<string>|null */
  647. public function getNotAuthorRegex(): ?array {
  648. return $this->not_author_regex;
  649. }
  650. /** @return list<string>|null */
  651. public function getTags(bool $plaintext = false): ?array {
  652. return $plaintext ? $this->tags : Minz_Helper::htmlspecialchars_utf8($this->tags, ENT_NOQUOTES);
  653. }
  654. /** @return list<string>|null */
  655. public function getTagsRegex(): ?array {
  656. return $this->tags_regex;
  657. }
  658. /** @return list<string>|null */
  659. public function getNotTags(bool $plaintext = false): ?array {
  660. return $plaintext ? $this->not_tags : Minz_Helper::htmlspecialchars_utf8($this->not_tags, ENT_NOQUOTES);
  661. }
  662. /** @return list<string>|null */
  663. public function getNotTagsRegex(): ?array {
  664. return $this->not_tags_regex;
  665. }
  666. /** @return list<string>|null */
  667. public function getSearch(bool $plaintext = false): ?array {
  668. return $plaintext ? $this->search : Minz_Helper::htmlspecialchars_utf8($this->search, ENT_NOQUOTES);
  669. }
  670. /** @return list<string>|null */
  671. public function getSearchRegex(): ?array {
  672. return $this->search_regex;
  673. }
  674. /** @return list<string>|null */
  675. public function getNotSearch(bool $plaintext = false): ?array {
  676. return $plaintext ? $this->not_search : Minz_Helper::htmlspecialchars_utf8($this->not_search, ENT_NOQUOTES);
  677. }
  678. /** @return list<string>|null */
  679. public function getNotSearchRegex(): ?array {
  680. return $this->not_search_regex;
  681. }
  682. /**
  683. * @param list<string>|null $anArray
  684. * @return list<string>
  685. */
  686. private static function removeEmptyValues(?array $anArray): array {
  687. return empty($anArray) ? [] : array_values(array_filter($anArray, static fn(string $value) => $value !== ''));
  688. }
  689. /**
  690. * @param list<string>|string $value
  691. * @return ($value is string ? string : list<string>)
  692. */
  693. private static function decodeSpaces(array|string $value): array|string {
  694. if (is_array($value)) {
  695. foreach ($value as &$val) {
  696. $val = self::decodeSpaces($val);
  697. }
  698. } else {
  699. $value = trim(str_replace('+', ' ', $value));
  700. }
  701. return $value;
  702. }
  703. /**
  704. * Parse the search string to find entry (article) IDs.
  705. */
  706. private function parseEntryIds(string $input): string {
  707. if (preg_match_all('/\\be:(?P<search>[0-9,]*)/', $input, $matches)) {
  708. $input = str_replace($matches[0], '', $input);
  709. $ids_lists = $matches['search'];
  710. $this->entry_ids = [];
  711. foreach ($ids_lists as $ids_list) {
  712. $entry_ids = explode(',', $ids_list);
  713. $entry_ids = self::removeEmptyValues($entry_ids);
  714. if (!empty($entry_ids)) {
  715. $this->entry_ids = array_merge($this->entry_ids, $entry_ids);
  716. }
  717. }
  718. }
  719. return $input;
  720. }
  721. private function parseNotEntryIds(string $input): string {
  722. if (preg_match_all('/(?<=[\\s(]|^)[!-]e:(?P<search>[0-9,]*)/', $input, $matches)) {
  723. $input = str_replace($matches[0], '', $input);
  724. $ids_lists = $matches['search'];
  725. $this->not_entry_ids = [];
  726. foreach ($ids_lists as $ids_list) {
  727. $entry_ids = explode(',', $ids_list);
  728. $entry_ids = self::removeEmptyValues($entry_ids);
  729. if (!empty($entry_ids)) {
  730. $this->not_entry_ids = array_merge($this->not_entry_ids, $entry_ids);
  731. }
  732. }
  733. }
  734. return $input;
  735. }
  736. private function parseFeedIds(string $input): string {
  737. if (preg_match_all('/\\bf:(?P<search>[0-9,]*)/', $input, $matches)) {
  738. $input = str_replace($matches[0], '', $input);
  739. $ids_lists = $matches['search'];
  740. $this->feed_ids = [];
  741. foreach ($ids_lists as $ids_list) {
  742. $feed_ids = explode(',', $ids_list);
  743. $feed_ids = self::removeEmptyValues($feed_ids);
  744. /** @var list<int> $feed_ids */
  745. $feed_ids = array_map('intval', $feed_ids);
  746. if (!empty($feed_ids)) {
  747. $this->feed_ids = array_merge($this->feed_ids, $feed_ids);
  748. }
  749. }
  750. }
  751. return $input;
  752. }
  753. private function parseNotFeedIds(string $input): string {
  754. if (preg_match_all('/(?<=[\\s(]|^)[!-]f:(?P<search>[0-9,]*)/', $input, $matches)) {
  755. $input = str_replace($matches[0], '', $input);
  756. $ids_lists = $matches['search'];
  757. $this->not_feed_ids = [];
  758. foreach ($ids_lists as $ids_list) {
  759. $feed_ids = explode(',', $ids_list);
  760. $feed_ids = self::removeEmptyValues($feed_ids);
  761. /** @var list<int> $feed_ids */
  762. $feed_ids = array_map('intval', $feed_ids);
  763. if (!empty($feed_ids)) {
  764. $this->not_feed_ids = array_merge($this->not_feed_ids, $feed_ids);
  765. }
  766. }
  767. }
  768. return $input;
  769. }
  770. private function parseCategoryIds(string $input): string {
  771. if (preg_match_all('/\\bc:(?P<search>[0-9,]*)/', $input, $matches)) {
  772. $input = str_replace($matches[0], '', $input);
  773. $ids_lists = $matches['search'];
  774. $this->category_ids = [];
  775. foreach ($ids_lists as $ids_list) {
  776. $category_ids = explode(',', $ids_list);
  777. $category_ids = self::removeEmptyValues($category_ids);
  778. /** @var list<int> $category_ids */
  779. $category_ids = array_map('intval', $category_ids);
  780. if (!empty($category_ids)) {
  781. $this->category_ids = array_merge($this->category_ids, $category_ids);
  782. }
  783. }
  784. }
  785. return $input;
  786. }
  787. private function parseNotCategoryIds(string $input): string {
  788. if (preg_match_all('/(?<=[\\s(]|^)[!-]c:(?P<search>[0-9,]*)/', $input, $matches)) {
  789. $input = str_replace($matches[0], '', $input);
  790. $ids_lists = $matches['search'];
  791. $this->not_category_ids = [];
  792. foreach ($ids_lists as $ids_list) {
  793. $category_ids = explode(',', $ids_list);
  794. $category_ids = self::removeEmptyValues($category_ids);
  795. /** @var list<int> $category_ids */
  796. $category_ids = array_map('intval', $category_ids);
  797. if (!empty($category_ids)) {
  798. $this->not_category_ids = array_merge($this->not_category_ids, $category_ids);
  799. }
  800. }
  801. }
  802. return $input;
  803. }
  804. /**
  805. * Parse the search string to find user query IDs.
  806. */
  807. private function parseUserQueryIds(string $input): string {
  808. if (preg_match_all('/\\b[S]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
  809. $input = str_replace($matches[0], '', $input);
  810. $ids_lists = $matches['search'];
  811. $this->user_query_ids = [];
  812. foreach ($ids_lists as $ids_list) {
  813. if ($ids_list === '*') {
  814. $this->user_query_ids[] = '*';
  815. break;
  816. }
  817. $user_query_ids = explode(',', $ids_list);
  818. $user_query_ids = self::removeEmptyValues($user_query_ids);
  819. /** @var list<int> $user_query_ids */
  820. $user_query_ids = array_map('intval', $user_query_ids);
  821. if (!empty($user_query_ids)) {
  822. $this->user_query_ids[] = $user_query_ids;
  823. }
  824. }
  825. }
  826. return $input;
  827. }
  828. private function parseNotUserQueryIds(string $input): string {
  829. if (preg_match_all('/(?<=[\\s(]|^)[!-][S]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
  830. $input = str_replace($matches[0], '', $input);
  831. $ids_lists = $matches['search'];
  832. $this->not_user_query_ids = [];
  833. foreach ($ids_lists as $ids_list) {
  834. if ($ids_list === '*') {
  835. $this->not_user_query_ids[] = '*';
  836. break;
  837. }
  838. $user_query_ids = explode(',', $ids_list);
  839. $user_query_ids = self::removeEmptyValues($user_query_ids);
  840. /** @var list<int> $user_query_ids */
  841. $user_query_ids = array_map('intval', $user_query_ids);
  842. if (!empty($user_query_ids)) {
  843. $this->not_user_query_ids[] = $user_query_ids;
  844. }
  845. }
  846. }
  847. return $input;
  848. }
  849. /**
  850. * Parse the search string to find user query names.
  851. */
  852. private function parseUserQueryNames(string $input): string {
  853. $names_lists = [];
  854. if (preg_match_all('/\\bsearch?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  855. $names_lists = $matches['search'];
  856. $input = str_replace($matches[0], '', $input);
  857. }
  858. if (preg_match_all('/\\bsearch?:(?P<search>[^\s"]*)/', $input, $matches)) {
  859. $names_lists = array_merge($names_lists, $matches['search']);
  860. $input = str_replace($matches[0], '', $input);
  861. }
  862. if (!empty($names_lists)) {
  863. $this->user_query_names = [];
  864. foreach ($names_lists as $names_list) {
  865. $names_array = explode(',', $names_list);
  866. $names_array = self::removeEmptyValues($names_array);
  867. if (!empty($names_array)) {
  868. $this->user_query_names[] = $names_array;
  869. }
  870. }
  871. }
  872. return $input;
  873. }
  874. /**
  875. * Parse the search string to find user query names to exclude.
  876. */
  877. private function parseNotUserQueryNames(string $input): string {
  878. $names_lists = [];
  879. if (preg_match_all('/(?<=[\\s(]|^)[!-]search?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  880. $names_lists = $matches['search'];
  881. $input = str_replace($matches[0], '', $input);
  882. }
  883. if (preg_match_all('/(?<=[\\s(]|^)[!-]search?:(?P<search>[^\\s"]*)/', $input, $matches)) {
  884. $names_lists = array_merge($names_lists, $matches['search']);
  885. $input = str_replace($matches[0], '', $input);
  886. }
  887. if (!empty($names_lists)) {
  888. $this->not_user_query_names = [];
  889. foreach ($names_lists as $names_list) {
  890. $names_array = explode(',', $names_list);
  891. $names_array = self::removeEmptyValues($names_array);
  892. if (!empty($names_array)) {
  893. $this->not_user_query_names[] = $names_array;
  894. }
  895. }
  896. }
  897. return $input;
  898. }
  899. /**
  900. * Parse the search string to find tags (labels) IDs.
  901. */
  902. private function parseLabelIds(string $input): string {
  903. if (preg_match_all('/\\b[lL]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
  904. $input = str_replace($matches[0], '', $input);
  905. $ids_lists = $matches['search'];
  906. $this->label_ids = [];
  907. foreach ($ids_lists as $ids_list) {
  908. if ($ids_list === '*') {
  909. $this->label_ids[] = '*';
  910. break;
  911. }
  912. $label_ids = explode(',', $ids_list);
  913. $label_ids = self::removeEmptyValues($label_ids);
  914. /** @var list<int> $label_ids */
  915. $label_ids = array_map('intval', $label_ids);
  916. if (!empty($label_ids)) {
  917. $this->label_ids[] = $label_ids;
  918. }
  919. }
  920. }
  921. return $input;
  922. }
  923. private function parseNotLabelIds(string $input): string {
  924. if (preg_match_all('/(?<=[\\s(]|^)[!-][lL]:(?P<search>[0-9,]+|[*])/', $input, $matches)) {
  925. $input = str_replace($matches[0], '', $input);
  926. $ids_lists = $matches['search'];
  927. $this->not_label_ids = [];
  928. foreach ($ids_lists as $ids_list) {
  929. if ($ids_list === '*') {
  930. $this->not_label_ids[] = '*';
  931. break;
  932. }
  933. $label_ids = explode(',', $ids_list);
  934. $label_ids = self::removeEmptyValues($label_ids);
  935. /** @var list<int> $label_ids */
  936. $label_ids = array_map('intval', $label_ids);
  937. if (!empty($label_ids)) {
  938. $this->not_label_ids[] = $label_ids;
  939. }
  940. }
  941. }
  942. return $input;
  943. }
  944. /**
  945. * Parse the search string to find tags (labels) names.
  946. */
  947. private function parseLabelNames(string $input): string {
  948. $names_lists = [];
  949. if (preg_match_all('/\\blabels?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  950. $names_lists = $matches['search'];
  951. $input = str_replace($matches[0], '', $input);
  952. }
  953. if (preg_match_all('/\\blabels?:(?P<search>[^\s"]*)/', $input, $matches)) {
  954. $names_lists = array_merge($names_lists, $matches['search']);
  955. $input = str_replace($matches[0], '', $input);
  956. }
  957. if (!empty($names_lists)) {
  958. $this->label_names = [];
  959. foreach ($names_lists as $names_list) {
  960. $names_array = explode(',', $names_list);
  961. $names_array = self::removeEmptyValues($names_array);
  962. if (!empty($names_array)) {
  963. $this->label_names[] = $names_array;
  964. }
  965. }
  966. }
  967. return $input;
  968. }
  969. /**
  970. * Parse the search string to find tags (labels) names to exclude.
  971. */
  972. private function parseNotLabelNames(string $input): string {
  973. $names_lists = [];
  974. if (preg_match_all('/(?<=[\\s(]|^)[!-]labels?:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  975. $names_lists = $matches['search'];
  976. $input = str_replace($matches[0], '', $input);
  977. }
  978. if (preg_match_all('/(?<=[\\s(]|^)[!-]labels?:(?P<search>[^\\s"]*)/', $input, $matches)) {
  979. $names_lists = array_merge($names_lists, $matches['search']);
  980. $input = str_replace($matches[0], '', $input);
  981. }
  982. if (!empty($names_lists)) {
  983. $this->not_label_names = [];
  984. foreach ($names_lists as $names_list) {
  985. $names_array = explode(',', $names_list);
  986. $names_array = self::removeEmptyValues($names_array);
  987. if (!empty($names_array)) {
  988. $this->not_label_names[] = $names_array;
  989. }
  990. }
  991. }
  992. return $input;
  993. }
  994. /**
  995. * Parse the search string to find intitle keyword and the search related to it.
  996. */
  997. private function parseIntitleSearch(string $input): string {
  998. if (preg_match_all('#\\bintitle:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
  999. $this->intitle_regex = $matches['search'];
  1000. $input = str_replace($matches[0], '', $input);
  1001. }
  1002. if (preg_match_all('/\\bintitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1003. $this->intitle = $matches['search'];
  1004. $input = str_replace($matches[0], '', $input);
  1005. }
  1006. if (preg_match_all('/\\bintitle:(?P<search>[^\s"]*)/', $input, $matches)) {
  1007. $this->intitle = array_merge($this->intitle ?? [], $matches['search']);
  1008. $input = str_replace($matches[0], '', $input);
  1009. }
  1010. $this->intitle = self::removeEmptyValues($this->intitle);
  1011. if (empty($this->intitle)) {
  1012. $this->intitle = null;
  1013. }
  1014. return $input;
  1015. }
  1016. private function parseNotIntitleSearch(string $input): string {
  1017. if (preg_match_all('#(?<=[\\s(]|^)[!-]intitle:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
  1018. $this->not_intitle_regex = $matches['search'];
  1019. $input = str_replace($matches[0], '', $input);
  1020. }
  1021. if (preg_match_all('/(?<=[\\s(]|^)[!-]intitle:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1022. $this->not_intitle = $matches['search'];
  1023. $input = str_replace($matches[0], '', $input);
  1024. }
  1025. if (preg_match_all('/(?<=[\\s(]|^)[!-]intitle:(?P<search>[^\s"]*)/', $input, $matches)) {
  1026. $this->not_intitle = array_merge($this->not_intitle ?? [], $matches['search']);
  1027. $input = str_replace($matches[0], '', $input);
  1028. }
  1029. $this->not_intitle = self::removeEmptyValues($this->not_intitle);
  1030. if (empty($this->not_intitle)) {
  1031. $this->not_intitle = null;
  1032. }
  1033. return $input;
  1034. }
  1035. /**
  1036. * Parse the search string to find intext keyword and the search related to it.
  1037. */
  1038. private function parseIntextSearch(string $input): string {
  1039. if (preg_match_all('#\\bintext:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
  1040. $this->intext_regex = $matches['search'];
  1041. $input = str_replace($matches[0], '', $input);
  1042. }
  1043. if (preg_match_all('/\\bintext:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1044. $this->intext = $matches['search'];
  1045. $input = str_replace($matches[0], '', $input);
  1046. }
  1047. if (preg_match_all('/\\bintext:(?P<search>[^\s"]*)/', $input, $matches)) {
  1048. $this->intext = array_merge($this->intext ?? [], $matches['search']);
  1049. $input = str_replace($matches[0], '', $input);
  1050. }
  1051. $this->intext = self::removeEmptyValues($this->intext);
  1052. if (empty($this->intext)) {
  1053. $this->intext = null;
  1054. }
  1055. return $input;
  1056. }
  1057. private function parseNotIntextSearch(string $input): string {
  1058. if (preg_match_all('#(?<=[\\s(]|^)[!-]intext:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
  1059. $this->not_intext_regex = $matches['search'];
  1060. $input = str_replace($matches[0], '', $input);
  1061. }
  1062. if (preg_match_all('/(?<=[\\s(]|^)[!-]intext:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1063. $this->not_intext = $matches['search'];
  1064. $input = str_replace($matches[0], '', $input);
  1065. }
  1066. if (preg_match_all('/(?<=[\\s(]|^)[!-]intext:(?P<search>[^\s"]*)/', $input, $matches)) {
  1067. $this->not_intext = array_merge($this->not_intext ?? [], $matches['search']);
  1068. $input = str_replace($matches[0], '', $input);
  1069. }
  1070. $this->not_intext = self::removeEmptyValues($this->not_intext);
  1071. if (empty($this->not_intext)) {
  1072. $this->not_intext = null;
  1073. }
  1074. return $input;
  1075. }
  1076. /**
  1077. * Parse the search string to find author keyword and the search related to it.
  1078. * The search is the first word following the keyword except when using
  1079. * a delimiter. Supported delimiters are single quote (') and double quotes (").
  1080. */
  1081. private function parseAuthorSearch(string $input): string {
  1082. if (preg_match_all('#\\bauthor:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
  1083. $this->author_regex = $matches['search'];
  1084. $input = str_replace($matches[0], '', $input);
  1085. }
  1086. if (preg_match_all('/\\bauthor:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1087. $this->author = $matches['search'];
  1088. $input = str_replace($matches[0], '', $input);
  1089. }
  1090. if (preg_match_all('/\\bauthor:(?P<search>[^\s"]*)/', $input, $matches)) {
  1091. $this->author = array_merge($this->author ?? [], $matches['search']);
  1092. $input = str_replace($matches[0], '', $input);
  1093. }
  1094. $this->author = self::removeEmptyValues($this->author);
  1095. if (empty($this->author)) {
  1096. $this->author = null;
  1097. }
  1098. return $input;
  1099. }
  1100. private function parseNotAuthorSearch(string $input): string {
  1101. if (preg_match_all('#(?<=[\\s(]|^)[!-]author:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
  1102. $this->not_author_regex = $matches['search'];
  1103. $input = str_replace($matches[0], '', $input);
  1104. }
  1105. if (preg_match_all('/(?<=[\\s(]|^)[!-]author:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1106. $this->not_author = $matches['search'];
  1107. $input = str_replace($matches[0], '', $input);
  1108. }
  1109. if (preg_match_all('/(?<=[\\s(]|^)[!-]author:(?P<search>[^\s"]*)/', $input, $matches)) {
  1110. $this->not_author = array_merge($this->not_author ?? [], $matches['search']);
  1111. $input = str_replace($matches[0], '', $input);
  1112. }
  1113. $this->not_author = self::removeEmptyValues($this->not_author);
  1114. if (empty($this->not_author)) {
  1115. $this->not_author = null;
  1116. }
  1117. return $input;
  1118. }
  1119. /**
  1120. * Parse the search string to find inurl keyword and the search related to it.
  1121. * The search is the first word following the keyword.
  1122. */
  1123. private function parseInurlSearch(string $input): string {
  1124. if (preg_match_all('#\\binurl:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
  1125. $this->inurl_regex = $matches['search'];
  1126. $input = str_replace($matches[0], '', $input);
  1127. }
  1128. if (preg_match_all('/\\binurl:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1129. $this->inurl = $matches['search'];
  1130. $input = str_replace($matches[0], '', $input);
  1131. }
  1132. if (preg_match_all('/\\binurl:(?P<search>[^\\s]*)/', $input, $matches)) {
  1133. $this->inurl = $matches['search'];
  1134. $input = str_replace($matches[0], '', $input);
  1135. }
  1136. $this->inurl = self::removeEmptyValues($this->inurl);
  1137. if (empty($this->inurl)) {
  1138. $this->inurl = null;
  1139. }
  1140. return $input;
  1141. }
  1142. private function parseNotInurlSearch(string $input): string {
  1143. if (preg_match_all('#(?<=[\\s(]|^)[!-]inurl:(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
  1144. $this->not_inurl_regex = $matches['search'];
  1145. $input = str_replace($matches[0], '', $input);
  1146. }
  1147. if (preg_match_all('/(?<=[\\s(]|^)[!-]inurl:(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1148. $this->not_inurl = $matches['search'];
  1149. $input = str_replace($matches[0], '', $input);
  1150. }
  1151. if (preg_match_all('/(?<=[\\s(]|^)[!-]inurl:(?P<search>[^\\s]*)/', $input, $matches)) {
  1152. $this->not_inurl = $matches['search'];
  1153. $input = str_replace($matches[0], '', $input);
  1154. }
  1155. $this->not_inurl = self::removeEmptyValues($this->not_inurl);
  1156. if (empty($this->not_inurl)) {
  1157. $this->not_inurl = null;
  1158. }
  1159. return $input;
  1160. }
  1161. /**
  1162. * Parse the search string to find date keyword and the search related to it.
  1163. * The search is the first word following the keyword.
  1164. */
  1165. private function parseDateSearch(string $input): string {
  1166. if (preg_match_all('/\\bdate:(?P<search>[^\\s]*)/', $input, $matches)) {
  1167. $input = str_replace($matches[0], '', $input);
  1168. $dates = self::removeEmptyValues($matches['search']);
  1169. if (!empty($dates[0])) {
  1170. [$this->min_date, $this->max_date] = parseDateInterval($dates[0]);
  1171. if (is_int($this->min_date) || is_int($this->max_date)) {
  1172. $this->input_date = $dates[0];
  1173. }
  1174. }
  1175. }
  1176. return $input;
  1177. }
  1178. private function parseNotDateSearch(string $input): string {
  1179. if (preg_match_all('/(?<=[\\s(]|^)[!-]date:(?P<search>[^\\s]*)/', $input, $matches)) {
  1180. $input = str_replace($matches[0], '', $input);
  1181. $dates = self::removeEmptyValues($matches['search']);
  1182. if (!empty($dates[0])) {
  1183. [$this->not_min_date, $this->not_max_date] = parseDateInterval($dates[0]);
  1184. if (is_int($this->not_min_date) || is_int($this->not_max_date)) {
  1185. $this->input_not_date = $dates[0];
  1186. }
  1187. }
  1188. }
  1189. return $input;
  1190. }
  1191. /**
  1192. * Parse the search string to find pubdate keyword and the search related to it.
  1193. * The search is the first word following the keyword.
  1194. */
  1195. private function parsePubdateSearch(string $input): string {
  1196. if (preg_match_all('/\\bpubdate:(?P<search>[^\\s]*)/', $input, $matches)) {
  1197. $input = str_replace($matches[0], '', $input);
  1198. $dates = self::removeEmptyValues($matches['search']);
  1199. if (!empty($dates[0])) {
  1200. [$this->min_pubdate, $this->max_pubdate] = parseDateInterval($dates[0]);
  1201. if (is_int($this->min_pubdate) || is_int($this->max_pubdate)) {
  1202. $this->input_pubdate = $dates[0];
  1203. }
  1204. }
  1205. }
  1206. return $input;
  1207. }
  1208. private function parseNotPubdateSearch(string $input): string {
  1209. if (preg_match_all('/(?<=[\\s(]|^)[!-]pubdate:(?P<search>[^\\s]*)/', $input, $matches)) {
  1210. $input = str_replace($matches[0], '', $input);
  1211. $dates = self::removeEmptyValues($matches['search']);
  1212. if (!empty($dates[0])) {
  1213. [$this->not_min_pubdate, $this->not_max_pubdate] = parseDateInterval($dates[0]);
  1214. if (is_int($this->not_min_pubdate) || is_int($this->not_max_pubdate)) {
  1215. $this->input_not_pubdate = $dates[0];
  1216. }
  1217. }
  1218. }
  1219. return $input;
  1220. }
  1221. private function parseModifiedDateSearch(string $input): string {
  1222. if (preg_match_all('/\bmdate:(?P<search>[^\s]*)/', $input, $matches)) {
  1223. $input = str_replace($matches[0], '', $input);
  1224. $dates = self::removeEmptyValues($matches['search']);
  1225. if (!empty($dates[0])) {
  1226. [$this->min_modified_date, $this->max_modified_date] = parseDateInterval($dates[0]);
  1227. if (is_int($this->min_modified_date) || is_int($this->max_modified_date)) {
  1228. $this->input_modified_date = $dates[0];
  1229. }
  1230. }
  1231. }
  1232. return $input;
  1233. }
  1234. private function parseNotModifiedDateSearch(string $input): string {
  1235. if (preg_match_all('/(?<=[\s(]|^)[!-]mdate:(?P<search>[^\s]*)/', $input, $matches)) {
  1236. $input = str_replace($matches[0], '', $input);
  1237. $dates = self::removeEmptyValues($matches['search']);
  1238. if (!empty($dates[0])) {
  1239. [$this->not_min_modified_date, $this->not_max_modified_date] = parseDateInterval($dates[0]);
  1240. if (is_int($this->not_min_modified_date) || is_int($this->not_max_modified_date)) {
  1241. $this->input_not_modified_date = $dates[0];
  1242. }
  1243. }
  1244. }
  1245. return $input;
  1246. }
  1247. /**
  1248. * Parse the search string to find userdate keyword and the search related to it.
  1249. * The search is the first word following the keyword.
  1250. */
  1251. private function parseUserdateSearch(string $input): string {
  1252. if (preg_match_all('/\\buserdate:(?P<search>[^\\s]*)/', $input, $matches)) {
  1253. $input = str_replace($matches[0], '', $input);
  1254. $dates = self::removeEmptyValues($matches['search']);
  1255. if (!empty($dates[0])) {
  1256. [$this->min_userdate, $this->max_userdate] = parseDateInterval($dates[0]);
  1257. if (is_int($this->min_userdate) || is_int($this->max_userdate)) {
  1258. $this->input_userdate = $dates[0];
  1259. }
  1260. }
  1261. }
  1262. return $input;
  1263. }
  1264. private function parseNotUserdateSearch(string $input): string {
  1265. if (preg_match_all('/(?<=[\\s(]|^)[!-]userdate:(?P<search>[^\\s]*)/', $input, $matches)) {
  1266. $input = str_replace($matches[0], '', $input);
  1267. $dates = self::removeEmptyValues($matches['search']);
  1268. if (!empty($dates[0])) {
  1269. [$this->not_min_userdate, $this->not_max_userdate] = parseDateInterval($dates[0]);
  1270. if (is_int($this->not_min_userdate) || is_int($this->not_max_userdate)) {
  1271. $this->input_not_userdate = $dates[0];
  1272. }
  1273. }
  1274. }
  1275. return $input;
  1276. }
  1277. /**
  1278. * Parse the search string to find tags keyword (# followed by a word)
  1279. * and the search related to it.
  1280. * The search is the first word following the #.
  1281. */
  1282. private function parseTagsSearch(string $input): string {
  1283. if (preg_match_all('%#(?P<search>/.*?(?<!\\\\)/[im]*)%', $input, $matches)) {
  1284. $this->tags_regex = $matches['search'];
  1285. $input = str_replace($matches[0], '', $input);
  1286. }
  1287. if (preg_match_all('/#(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1288. $this->tags = $matches['search'];
  1289. $input = str_replace($matches[0], '', $input);
  1290. }
  1291. if (preg_match_all('/#(?P<search>[^\\s]+)/', $input, $matches)) {
  1292. $this->tags = $matches['search'];
  1293. $input = str_replace($matches[0], '', $input);
  1294. }
  1295. $this->tags = self::removeEmptyValues($this->tags);
  1296. if (empty($this->tags)) {
  1297. $this->tags = null;
  1298. } else {
  1299. $this->tags = self::decodeSpaces($this->tags);
  1300. }
  1301. return $input;
  1302. }
  1303. private function parseNotTagsSearch(string $input): string {
  1304. if (preg_match_all('%(?<=[\\s(]|^)[!-]#(?P<search>/.*?(?<!\\\\)/[im]*)%', $input, $matches)) {
  1305. $this->not_tags_regex = $matches['search'];
  1306. $input = str_replace($matches[0], '', $input);
  1307. }
  1308. if (preg_match_all('/(?<=[\\s(]|^)[!-]#(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1309. $this->not_tags = $matches['search'];
  1310. $input = str_replace($matches[0], '', $input);
  1311. }
  1312. if (preg_match_all('/(?<=[\\s(]|^)[!-]#(?P<search>[^\\s]+)/', $input, $matches)) {
  1313. $this->not_tags = $matches['search'];
  1314. $input = str_replace($matches[0], '', $input);
  1315. }
  1316. $this->not_tags = self::removeEmptyValues($this->not_tags);
  1317. if (empty($this->not_tags)) {
  1318. $this->not_tags = null;
  1319. } else {
  1320. $this->not_tags = self::decodeSpaces($this->not_tags);
  1321. }
  1322. return $input;
  1323. }
  1324. /**
  1325. * Parse the search string to find search values.
  1326. * Every word is a distinct search value using a delimiter.
  1327. * Supported delimiters are single quote (') and double quotes (") and regex (/).
  1328. */
  1329. private function parseQuotedSearch(string $input): string {
  1330. $input = self::cleanSearch($input);
  1331. if ($input === '') {
  1332. return '';
  1333. }
  1334. if (preg_match_all('#(?<=[\\s(]|^)(?<![!-\\\\])(?P<search>/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
  1335. $this->search_regex = $matches['search'];
  1336. //TODO: Replace all those str_replace with PREG_OFFSET_CAPTURE
  1337. $input = str_replace($matches[0], '', $input);
  1338. }
  1339. if (preg_match_all('/(?<=[\\s(]|^)(?<![!-\\\\])(?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1340. $this->search = $matches['search'];
  1341. //TODO: Replace all those str_replace with PREG_OFFSET_CAPTURE
  1342. $input = str_replace($matches[0], '', $input);
  1343. }
  1344. return $input;
  1345. }
  1346. /**
  1347. * Parse the search string to find search values.
  1348. * Every word is a distinct search value.
  1349. */
  1350. private function parseSearch(string $input): string {
  1351. $input = self::cleanSearch($input);
  1352. if ($input === '') {
  1353. return '';
  1354. }
  1355. if (is_array($this->search)) {
  1356. $this->search = array_merge($this->search, explode(' ', $input));
  1357. } else {
  1358. $this->search = explode(' ', $input);
  1359. }
  1360. return $input;
  1361. }
  1362. private function parseNotSearch(string $input): string {
  1363. $input = self::cleanSearch($input);
  1364. if ($input === '') {
  1365. return '';
  1366. }
  1367. if (preg_match_all('#(?<=[\\s(]|^)[!-](?P<search>(?<!\\\\)/.*?(?<!\\\\)/[im]*)#', $input, $matches)) {
  1368. $this->not_search_regex = $matches['search'];
  1369. $input = str_replace($matches[0], '', $input);
  1370. }
  1371. if (preg_match_all('/(?<=[\\s(]|^)[!-](?P<delim>[\'"])(?P<search>.*)(?P=delim)/U', $input, $matches)) {
  1372. $this->not_search = $matches['search'];
  1373. $input = str_replace($matches[0], '', $input);
  1374. }
  1375. $input = self::cleanSearch($input);
  1376. if ($input === '') {
  1377. return '';
  1378. }
  1379. if (preg_match_all('/(?<=[\\s(]|^)[!-](?P<search>[^\\s]+)/', $input, $matches)) {
  1380. $this->not_search = array_merge(is_array($this->not_search) ? $this->not_search : [], $matches['search']);
  1381. $input = str_replace($matches[0], '', $input);
  1382. }
  1383. $this->not_search = self::removeEmptyValues($this->not_search);
  1384. return $input;
  1385. }
  1386. /**
  1387. * Remove all unnecessary spaces in the search
  1388. */
  1389. private static function cleanSearch(string $input): string {
  1390. $input = preg_replace('/\\s+/', ' ', $input);
  1391. if (!is_string($input)) {
  1392. return '';
  1393. }
  1394. return trim($input);
  1395. }
  1396. /** Remove escaping backslashes for parenthesis logic */
  1397. private static function unescape(string $input): string {
  1398. return str_replace(['\\(', '\\)'], ['(', ')'], $input);
  1399. }
  1400. }