aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/bookmark/BookmarkFilter.php
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2021-02-04 11:11:33 +0100
committerGitHub <noreply@github.com>2021-02-04 11:11:33 +0100
commit9db1ccdf2ce8381e1f3acb581d3c6a6def095d8c (patch)
tree97a618b77d327a5f963c91522988e24db5a9e158 /application/bookmark/BookmarkFilter.php
parent8997ae6c8e24286f7d47981eaf905e80d2481c10 (diff)
parentbcba6bd353161fab456b423e93571ab027d5423c (diff)
downloadShaarli-master.tar.gz
Shaarli-master.tar.zst
Shaarli-master.zip
Merge pull request #1698 from ArthurHoaro/feature/plugins-search-filterHEADmaster
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.php246
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
5namespace Shaarli\Bookmark; 5namespace Shaarli\Bookmark;
6 6
7use Exception;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException; 7use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Config\ConfigManager; 8use Shaarli\Config\ConfigManager;
9use 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.