diff options
author | ArthurHoaro <arthur@hoa.ro> | 2021-02-04 11:11:33 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-02-04 11:11:33 +0100 |
commit | 9db1ccdf2ce8381e1f3acb581d3c6a6def095d8c (patch) | |
tree | 97a618b77d327a5f963c91522988e24db5a9e158 /application/bookmark/BookmarkFilter.php | |
parent | 8997ae6c8e24286f7d47981eaf905e80d2481c10 (diff) | |
parent | bcba6bd353161fab456b423e93571ab027d5423c (diff) | |
download | Shaarli-master.tar.gz Shaarli-master.tar.zst Shaarli-master.zip |
New plugin hook: ability to add custom filters to Shaarli search engine
Diffstat (limited to 'application/bookmark/BookmarkFilter.php')
-rw-r--r-- | application/bookmark/BookmarkFilter.php | 246 |
1 files changed, 128 insertions, 118 deletions
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index db83c51c..8b41dbb8 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php | |||
@@ -4,9 +4,9 @@ declare(strict_types=1); | |||
4 | 4 | ||
5 | namespace Shaarli\Bookmark; | 5 | namespace Shaarli\Bookmark; |
6 | 6 | ||
7 | use Exception; | ||
8 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; | 7 | use Shaarli\Bookmark\Exception\BookmarkNotFoundException; |
9 | use Shaarli\Config\ConfigManager; | 8 | use Shaarli\Config\ConfigManager; |
9 | use Shaarli\Plugin\PluginManager; | ||
10 | 10 | ||
11 | /** | 11 | /** |
12 | * Class LinkFilter. | 12 | * Class LinkFilter. |
@@ -33,11 +33,6 @@ class BookmarkFilter | |||
33 | /** | 33 | /** |
34 | * @var string filter by day. | 34 | * @var string filter by day. |
35 | */ | 35 | */ |
36 | public static $FILTER_DAY = 'FILTER_DAY'; | ||
37 | |||
38 | /** | ||
39 | * @var string filter by day. | ||
40 | */ | ||
41 | public static $DEFAULT = 'NO_FILTER'; | 36 | public static $DEFAULT = 'NO_FILTER'; |
42 | 37 | ||
43 | /** @var string Visibility: all */ | 38 | /** @var string Visibility: all */ |
@@ -62,13 +57,17 @@ class BookmarkFilter | |||
62 | /** @var ConfigManager */ | 57 | /** @var ConfigManager */ |
63 | protected $conf; | 58 | protected $conf; |
64 | 59 | ||
60 | /** @var PluginManager */ | ||
61 | protected $pluginManager; | ||
62 | |||
65 | /** | 63 | /** |
66 | * @param Bookmark[] $bookmarks initialization. | 64 | * @param Bookmark[] $bookmarks initialization. |
67 | */ | 65 | */ |
68 | public function __construct($bookmarks, ConfigManager $conf) | 66 | public function __construct($bookmarks, ConfigManager $conf, PluginManager $pluginManager) |
69 | { | 67 | { |
70 | $this->bookmarks = $bookmarks; | 68 | $this->bookmarks = $bookmarks; |
71 | $this->conf = $conf; | 69 | $this->conf = $conf; |
70 | $this->pluginManager = $pluginManager; | ||
72 | } | 71 | } |
73 | 72 | ||
74 | /** | 73 | /** |
@@ -112,12 +111,12 @@ class BookmarkFilter | |||
112 | $filtered = $this->bookmarks; | 111 | $filtered = $this->bookmarks; |
113 | } | 112 | } |
114 | if (!empty($request[0])) { | 113 | if (!empty($request[0])) { |
115 | $filtered = (new BookmarkFilter($filtered, $this->conf)) | 114 | $filtered = (new BookmarkFilter($filtered, $this->conf, $this->pluginManager)) |
116 | ->filterTags($request[0], $casesensitive, $visibility) | 115 | ->filterTags($request[0], $casesensitive, $visibility) |
117 | ; | 116 | ; |
118 | } | 117 | } |
119 | if (!empty($request[1])) { | 118 | if (!empty($request[1])) { |
120 | $filtered = (new BookmarkFilter($filtered, $this->conf)) | 119 | $filtered = (new BookmarkFilter($filtered, $this->conf, $this->pluginManager)) |
121 | ->filterFulltext($request[1], $visibility) | 120 | ->filterFulltext($request[1], $visibility) |
122 | ; | 121 | ; |
123 | } | 122 | } |
@@ -130,8 +129,6 @@ class BookmarkFilter | |||
130 | } else { | 129 | } else { |
131 | return $this->filterTags($request, $casesensitive, $visibility); | 130 | return $this->filterTags($request, $casesensitive, $visibility); |
132 | } | 131 | } |
133 | case self::$FILTER_DAY: | ||
134 | return $this->filterDay($request, $visibility); | ||
135 | default: | 132 | default: |
136 | return $this->noFilter($visibility); | 133 | return $this->noFilter($visibility); |
137 | } | 134 | } |
@@ -146,13 +143,20 @@ class BookmarkFilter | |||
146 | */ | 143 | */ |
147 | private function noFilter(string $visibility = 'all') | 144 | private function noFilter(string $visibility = 'all') |
148 | { | 145 | { |
149 | if ($visibility === 'all') { | ||
150 | return $this->bookmarks; | ||
151 | } | ||
152 | |||
153 | $out = []; | 146 | $out = []; |
154 | foreach ($this->bookmarks as $key => $value) { | 147 | foreach ($this->bookmarks as $key => $value) { |
155 | if ($value->isPrivate() && $visibility === 'private') { | 148 | if ( |
149 | !$this->pluginManager->filterSearchEntry( | ||
150 | $value, | ||
151 | ['source' => 'no_filter', 'visibility' => $visibility] | ||
152 | ) | ||
153 | ) { | ||
154 | continue; | ||
155 | } | ||
156 | |||
157 | if ($visibility === 'all') { | ||
158 | $out[$key] = $value; | ||
159 | } elseif ($value->isPrivate() && $visibility === 'private') { | ||
156 | $out[$key] = $value; | 160 | $out[$key] = $value; |
157 | } elseif (!$value->isPrivate() && $visibility === 'public') { | 161 | } elseif (!$value->isPrivate() && $visibility === 'public') { |
158 | $out[$key] = $value; | 162 | $out[$key] = $value; |
@@ -233,18 +237,34 @@ class BookmarkFilter | |||
233 | } | 237 | } |
234 | 238 | ||
235 | // Iterate over every stored link. | 239 | // Iterate over every stored link. |
236 | foreach ($this->bookmarks as $id => $link) { | 240 | foreach ($this->bookmarks as $id => $bookmark) { |
241 | if ( | ||
242 | !$this->pluginManager->filterSearchEntry( | ||
243 | $bookmark, | ||
244 | [ | ||
245 | 'source' => 'fulltext', | ||
246 | 'searchterms' => $searchterms, | ||
247 | 'andSearch' => $andSearch, | ||
248 | 'exactSearch' => $exactSearch, | ||
249 | 'excludeSearch' => $excludeSearch, | ||
250 | 'visibility' => $visibility | ||
251 | ] | ||
252 | ) | ||
253 | ) { | ||
254 | continue; | ||
255 | } | ||
256 | |||
237 | // ignore non private bookmarks when 'privatonly' is on. | 257 | // ignore non private bookmarks when 'privatonly' is on. |
238 | if ($visibility !== 'all') { | 258 | if ($visibility !== 'all') { |
239 | if (!$link->isPrivate() && $visibility === 'private') { | 259 | if (!$bookmark->isPrivate() && $visibility === 'private') { |
240 | continue; | 260 | continue; |
241 | } elseif ($link->isPrivate() && $visibility === 'public') { | 261 | } elseif ($bookmark->isPrivate() && $visibility === 'public') { |
242 | continue; | 262 | continue; |
243 | } | 263 | } |
244 | } | 264 | } |
245 | 265 | ||
246 | $lengths = []; | 266 | $lengths = []; |
247 | $content = $this->buildFullTextSearchableLink($link, $lengths); | 267 | $content = $this->buildFullTextSearchableLink($bookmark, $lengths); |
248 | 268 | ||
249 | // Be optimistic | 269 | // Be optimistic |
250 | $found = true; | 270 | $found = true; |
@@ -270,12 +290,12 @@ class BookmarkFilter | |||
270 | } | 290 | } |
271 | 291 | ||
272 | if ($found !== false) { | 292 | if ($found !== false) { |
273 | $link->addAdditionalContentEntry( | 293 | $bookmark->addAdditionalContentEntry( |
274 | 'search_highlight', | 294 | 'search_highlight', |
275 | $this->postProcessFoundPositions($lengths, $foundPositions) | 295 | $this->postProcessFoundPositions($lengths, $foundPositions) |
276 | ); | 296 | ); |
277 | 297 | ||
278 | $filtered[$id] = $link; | 298 | $filtered[$id] = $bookmark; |
279 | } | 299 | } |
280 | } | 300 | } |
281 | 301 | ||
@@ -283,56 +303,6 @@ class BookmarkFilter | |||
283 | } | 303 | } |
284 | 304 | ||
285 | /** | 305 | /** |
286 | * generate a regex fragment out of a tag | ||
287 | * | ||
288 | * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard | ||
289 | * | ||
290 | * @return string generated regex fragment | ||
291 | */ | ||
292 | protected function tag2regex(string $tag): string | ||
293 | { | ||
294 | $tagsSeparator = $this->conf->get('general.tags_separator', ' '); | ||
295 | $len = strlen($tag); | ||
296 | if (!$len || $tag === "-" || $tag === "*") { | ||
297 | // nothing to search, return empty regex | ||
298 | return ''; | ||
299 | } | ||
300 | if ($tag[0] === "-") { | ||
301 | // query is negated | ||
302 | $i = 1; // use offset to start after '-' character | ||
303 | $regex = '(?!'; // create negative lookahead | ||
304 | } else { | ||
305 | $i = 0; // start at first character | ||
306 | $regex = '(?='; // use positive lookahead | ||
307 | } | ||
308 | // before tag may only be the separator or the beginning | ||
309 | $regex .= '.*(?:^|' . $tagsSeparator . ')'; | ||
310 | // iterate over string, separating it into placeholder and content | ||
311 | for (; $i < $len; $i++) { | ||
312 | if ($tag[$i] === '*') { | ||
313 | // placeholder found | ||
314 | $regex .= '[^' . $tagsSeparator . ']*?'; | ||
315 | } else { | ||
316 | // regular characters | ||
317 | $offset = strpos($tag, '*', $i); | ||
318 | if ($offset === false) { | ||
319 | // no placeholder found, set offset to end of string | ||
320 | $offset = $len; | ||
321 | } | ||
322 | // subtract one, as we want to get before the placeholder or end of string | ||
323 | $offset -= 1; | ||
324 | // we got a tag name that we want to search for. escape any regex characters to prevent conflicts. | ||
325 | $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/'); | ||
326 | // move $i on | ||
327 | $i = $offset; | ||
328 | } | ||
329 | } | ||
330 | // after the tag may only be the separator or the end | ||
331 | $regex .= '(?:$|' . $tagsSeparator . '))'; | ||
332 | return $regex; | ||
333 | } | ||
334 | |||
335 | /** | ||
336 | * Returns the list of bookmarks associated with a given list of tags | 306 | * Returns the list of bookmarks associated with a given list of tags |
337 | * | 307 | * |
338 | * You can specify one or more tags, separated by space or a comma, e.g. | 308 | * You can specify one or more tags, separated by space or a comma, e.g. |
@@ -381,25 +351,39 @@ class BookmarkFilter | |||
381 | $filtered = []; | 351 | $filtered = []; |
382 | 352 | ||
383 | // iterate over each link | 353 | // iterate over each link |
384 | foreach ($this->bookmarks as $key => $link) { | 354 | foreach ($this->bookmarks as $key => $bookmark) { |
355 | if ( | ||
356 | !$this->pluginManager->filterSearchEntry( | ||
357 | $bookmark, | ||
358 | [ | ||
359 | 'source' => 'tags', | ||
360 | 'tags' => $tags, | ||
361 | 'casesensitive' => $casesensitive, | ||
362 | 'visibility' => $visibility | ||
363 | ] | ||
364 | ) | ||
365 | ) { | ||
366 | continue; | ||
367 | } | ||
368 | |||
385 | // check level of visibility | 369 | // check level of visibility |
386 | // ignore non private bookmarks when 'privateonly' is on. | 370 | // ignore non private bookmarks when 'privateonly' is on. |
387 | if ($visibility !== 'all') { | 371 | if ($visibility !== 'all') { |
388 | if (!$link->isPrivate() && $visibility === 'private') { | 372 | if (!$bookmark->isPrivate() && $visibility === 'private') { |
389 | continue; | 373 | continue; |
390 | } elseif ($link->isPrivate() && $visibility === 'public') { | 374 | } elseif ($bookmark->isPrivate() && $visibility === 'public') { |
391 | continue; | 375 | continue; |
392 | } | 376 | } |
393 | } | 377 | } |
394 | // build search string, start with tags of current link | 378 | // build search string, start with tags of current link |
395 | $search = $link->getTagsString($tagsSeparator); | 379 | $search = $bookmark->getTagsString($tagsSeparator); |
396 | if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { | 380 | if (strlen(trim($bookmark->getDescription())) && strpos($bookmark->getDescription(), '#') !== false) { |
397 | // description given and at least one possible tag found | 381 | // description given and at least one possible tag found |
398 | $descTags = []; | 382 | $descTags = []; |
399 | // find all tags in the form of #tag in the description | 383 | // find all tags in the form of #tag in the description |
400 | preg_match_all( | 384 | preg_match_all( |
401 | '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', | 385 | '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', |
402 | $link->getDescription(), | 386 | $bookmark->getDescription(), |
403 | $descTags | 387 | $descTags |
404 | ); | 388 | ); |
405 | if (count($descTags[1])) { | 389 | if (count($descTags[1])) { |
@@ -412,8 +396,9 @@ class BookmarkFilter | |||
412 | // this entry does _not_ match our regex | 396 | // this entry does _not_ match our regex |
413 | continue; | 397 | continue; |
414 | } | 398 | } |
415 | $filtered[$key] = $link; | 399 | $filtered[$key] = $bookmark; |
416 | } | 400 | } |
401 | |||
417 | return $filtered; | 402 | return $filtered; |
418 | } | 403 | } |
419 | 404 | ||
@@ -427,55 +412,30 @@ class BookmarkFilter | |||
427 | public function filterUntagged(string $visibility) | 412 | public function filterUntagged(string $visibility) |
428 | { | 413 | { |
429 | $filtered = []; | 414 | $filtered = []; |
430 | foreach ($this->bookmarks as $key => $link) { | 415 | foreach ($this->bookmarks as $key => $bookmark) { |
416 | if ( | ||
417 | !$this->pluginManager->filterSearchEntry( | ||
418 | $bookmark, | ||
419 | ['source' => 'untagged', 'visibility' => $visibility] | ||
420 | ) | ||
421 | ) { | ||
422 | continue; | ||
423 | } | ||
424 | |||
431 | if ($visibility !== 'all') { | 425 | if ($visibility !== 'all') { |
432 | if (!$link->isPrivate() && $visibility === 'private') { | 426 | if (!$bookmark->isPrivate() && $visibility === 'private') { |
433 | continue; | 427 | continue; |
434 | } elseif ($link->isPrivate() && $visibility === 'public') { | 428 | } elseif ($bookmark->isPrivate() && $visibility === 'public') { |
435 | continue; | 429 | continue; |
436 | } | 430 | } |
437 | } | 431 | } |
438 | 432 | ||
439 | if (empty($link->getTags())) { | 433 | if (empty($bookmark->getTags())) { |
440 | $filtered[$key] = $link; | ||
441 | } | ||
442 | } | ||
443 | |||
444 | return $filtered; | ||
445 | } | ||
446 | |||
447 | /** | ||
448 | * Returns the list of articles for a given day, chronologically sorted | ||
449 | * | ||
450 | * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g. | ||
451 | * print_r($mydb->filterDay('20120125')); | ||
452 | * | ||
453 | * @param string $day day to filter. | ||
454 | * @param string $visibility return only all/private/public bookmarks. | ||
455 | |||
456 | * @return Bookmark[] all link matching given day. | ||
457 | * | ||
458 | * @throws Exception if date format is invalid. | ||
459 | */ | ||
460 | public function filterDay(string $day, string $visibility) | ||
461 | { | ||
462 | if (!checkDateFormat('Ymd', $day)) { | ||
463 | throw new Exception('Invalid date format'); | ||
464 | } | ||
465 | |||
466 | $filtered = []; | ||
467 | foreach ($this->bookmarks as $key => $bookmark) { | ||
468 | if ($visibility === static::$PUBLIC && $bookmark->isPrivate()) { | ||
469 | continue; | ||
470 | } | ||
471 | |||
472 | if ($bookmark->getCreated()->format('Ymd') == $day) { | ||
473 | $filtered[$key] = $bookmark; | 434 | $filtered[$key] = $bookmark; |
474 | } | 435 | } |
475 | } | 436 | } |
476 | 437 | ||
477 | // sort by date ASC | 438 | return $filtered; |
478 | return array_reverse($filtered, true); | ||
479 | } | 439 | } |
480 | 440 | ||
481 | /** | 441 | /** |
@@ -498,6 +458,56 @@ class BookmarkFilter | |||
498 | } | 458 | } |
499 | 459 | ||
500 | /** | 460 | /** |
461 | * generate a regex fragment out of a tag | ||
462 | * | ||
463 | * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard | ||
464 | * | ||
465 | * @return string generated regex fragment | ||
466 | */ | ||
467 | protected function tag2regex(string $tag): string | ||
468 | { | ||
469 | $tagsSeparator = $this->conf->get('general.tags_separator', ' '); | ||
470 | $len = strlen($tag); | ||
471 | if (!$len || $tag === "-" || $tag === "*") { | ||
472 | // nothing to search, return empty regex | ||
473 | return ''; | ||
474 | } | ||
475 | if ($tag[0] === "-") { | ||
476 | // query is negated | ||
477 | $i = 1; // use offset to start after '-' character | ||
478 | $regex = '(?!'; // create negative lookahead | ||
479 | } else { | ||
480 | $i = 0; // start at first character | ||
481 | $regex = '(?='; // use positive lookahead | ||
482 | } | ||
483 | // before tag may only be the separator or the beginning | ||
484 | $regex .= '.*(?:^|' . $tagsSeparator . ')'; | ||
485 | // iterate over string, separating it into placeholder and content | ||
486 | for (; $i < $len; $i++) { | ||
487 | if ($tag[$i] === '*') { | ||
488 | // placeholder found | ||
489 | $regex .= '[^' . $tagsSeparator . ']*?'; | ||
490 | } else { | ||
491 | // regular characters | ||
492 | $offset = strpos($tag, '*', $i); | ||
493 | if ($offset === false) { | ||
494 | // no placeholder found, set offset to end of string | ||
495 | $offset = $len; | ||
496 | } | ||
497 | // subtract one, as we want to get before the placeholder or end of string | ||
498 | $offset -= 1; | ||
499 | // we got a tag name that we want to search for. escape any regex characters to prevent conflicts. | ||
500 | $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/'); | ||
501 | // move $i on | ||
502 | $i = $offset; | ||
503 | } | ||
504 | } | ||
505 | // after the tag may only be the separator or the end | ||
506 | $regex .= '(?:$|' . $tagsSeparator . '))'; | ||
507 | return $regex; | ||
508 | } | ||
509 | |||
510 | /** | ||
501 | * This method finalize the content of the foundPositions array, | 511 | * This method finalize the content of the foundPositions array, |
502 | * by associated all search results to their associated bookmark field, | 512 | * by associated all search results to their associated bookmark field, |
503 | * making sure that there is no overlapping results, etc. | 513 | * making sure that there is no overlapping results, etc. |