]> git.immae.eu Git - github/shaarli/Shaarli.git/blobdiff - application/bookmark/BookmarkFilter.php
Merge pull request #1698 from ArthurHoaro/feature/plugins-search-filter
[github/shaarli/Shaarli.git] / application / bookmark / BookmarkFilter.php
index c79386ea7ba750db4d1d7d7974ea7564154e943a..8b41dbb86766991dcc5a46ca665bfe605dc0c8c6 100644 (file)
@@ -4,8 +4,9 @@ declare(strict_types=1);
 
 namespace Shaarli\Bookmark;
 
-use Exception;
 use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
+use Shaarli\Config\ConfigManager;
+use Shaarli\Plugin\PluginManager;
 
 /**
  * Class LinkFilter.
@@ -29,11 +30,6 @@ class BookmarkFilter
      */
     public static $FILTER_TAG = 'tags';
 
-    /**
-     * @var string filter by day.
-     */
-    public static $FILTER_DAY = 'FILTER_DAY';
-
     /**
      * @var string filter by day.
      */
@@ -58,12 +54,20 @@ class BookmarkFilter
      */
     private $bookmarks;
 
+    /** @var ConfigManager */
+    protected $conf;
+
+    /** @var PluginManager */
+    protected $pluginManager;
+
     /**
      * @param Bookmark[] $bookmarks initialization.
      */
-    public function __construct($bookmarks)
+    public function __construct($bookmarks, ConfigManager $conf, PluginManager $pluginManager)
     {
         $this->bookmarks = $bookmarks;
+        $this->conf = $conf;
+        $this->pluginManager = $pluginManager;
     }
 
     /**
@@ -107,10 +111,14 @@ class BookmarkFilter
                     $filtered = $this->bookmarks;
                 }
                 if (!empty($request[0])) {
-                    $filtered = (new BookmarkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
+                    $filtered = (new BookmarkFilter($filtered, $this->conf, $this->pluginManager))
+                        ->filterTags($request[0], $casesensitive, $visibility)
+                    ;
                 }
                 if (!empty($request[1])) {
-                    $filtered = (new BookmarkFilter($filtered))->filterFulltext($request[1], $visibility);
+                    $filtered = (new BookmarkFilter($filtered, $this->conf, $this->pluginManager))
+                        ->filterFulltext($request[1], $visibility)
+                    ;
                 }
                 return $filtered;
             case self::$FILTER_TEXT:
@@ -121,8 +129,6 @@ class BookmarkFilter
                 } else {
                     return $this->filterTags($request, $casesensitive, $visibility);
                 }
-            case self::$FILTER_DAY:
-                return $this->filterDay($request, $visibility);
             default:
                 return $this->noFilter($visibility);
         }
@@ -137,13 +143,20 @@ class BookmarkFilter
      */
     private function noFilter(string $visibility = 'all')
     {
-        if ($visibility === 'all') {
-            return $this->bookmarks;
-        }
-
-        $out = array();
+        $out = [];
         foreach ($this->bookmarks as $key => $value) {
-            if ($value->isPrivate() && $visibility === 'private') {
+            if (
+                !$this->pluginManager->filterSearchEntry(
+                    $value,
+                    ['source' => 'no_filter', 'visibility' => $visibility]
+                )
+            ) {
+                continue;
+            }
+
+            if ($visibility === 'all') {
+                $out[$key] = $value;
+            } elseif ($value->isPrivate() && $visibility === 'private') {
                 $out[$key] = $value;
             } elseif (!$value->isPrivate() && $visibility === 'public') {
                 $out[$key] = $value;
@@ -224,18 +237,34 @@ class BookmarkFilter
         }
 
         // Iterate over every stored link.
-        foreach ($this->bookmarks as $id => $link) {
+        foreach ($this->bookmarks as $id => $bookmark) {
+            if (
+                !$this->pluginManager->filterSearchEntry(
+                    $bookmark,
+                    [
+                    'source' => 'fulltext',
+                    'searchterms' => $searchterms,
+                    'andSearch' => $andSearch,
+                    'exactSearch' => $exactSearch,
+                    'excludeSearch' => $excludeSearch,
+                    'visibility' => $visibility
+                    ]
+                )
+            ) {
+                continue;
+            }
+
             // ignore non private bookmarks when 'privatonly' is on.
             if ($visibility !== 'all') {
-                if (!$link->isPrivate() && $visibility === 'private') {
+                if (!$bookmark->isPrivate() && $visibility === 'private') {
                     continue;
-                } elseif ($link->isPrivate() && $visibility === 'public') {
+                } elseif ($bookmark->isPrivate() && $visibility === 'public') {
                     continue;
                 }
             }
 
             $lengths = [];
-            $content = $this->buildFullTextSearchableLink($link, $lengths);
+            $content = $this->buildFullTextSearchableLink($bookmark, $lengths);
 
             // Be optimistic
             $found = true;
@@ -261,65 +290,18 @@ class BookmarkFilter
             }
 
             if ($found !== false) {
-                $link->addAdditionalContentEntry(
+                $bookmark->addAdditionalContentEntry(
                     'search_highlight',
                     $this->postProcessFoundPositions($lengths, $foundPositions)
                 );
 
-                $filtered[$id] = $link;
+                $filtered[$id] = $bookmark;
             }
         }
 
         return $filtered;
     }
 
-    /**
-     * generate a regex fragment out of a tag
-     *
-     * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
-     *
-     * @return string generated regex fragment
-     */
-    private static function tag2regex(string $tag): string
-    {
-        $len = strlen($tag);
-        if (!$len || $tag === "-" || $tag === "*") {
-            // nothing to search, return empty regex
-            return '';
-        }
-        if ($tag[0] === "-") {
-            // query is negated
-            $i = 1; // use offset to start after '-' character
-            $regex = '(?!'; // create negative lookahead
-        } else {
-            $i = 0; // start at first character
-            $regex = '(?='; // use positive lookahead
-        }
-        $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
-        // iterate over string, separating it into placeholder and content
-        for (; $i < $len; $i++) {
-            if ($tag[$i] === '*') {
-                // placeholder found
-                $regex .= '[^ ]*?';
-            } else {
-                // regular characters
-                $offset = strpos($tag, '*', $i);
-                if ($offset === false) {
-                    // no placeholder found, set offset to end of string
-                    $offset = $len;
-                }
-                // subtract one, as we want to get before the placeholder or end of string
-                $offset -= 1;
-                // we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
-                $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
-                // move $i on
-                $i = $offset;
-            }
-        }
-        $regex .= '(?:$| ))'; // after the tag may only be a space or the end
-        return $regex;
-    }
-
     /**
      * Returns the list of bookmarks associated with a given list of tags
      *
@@ -334,14 +316,15 @@ class BookmarkFilter
      */
     public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all')
     {
+        $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
         // get single tags (we may get passed an array, even though the docs say different)
         $inputTags = $tags;
         if (!is_array($tags)) {
             // we got an input string, split tags
-            $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
+            $inputTags = tags_str2array($inputTags, $tagsSeparator);
         }
 
-        if (!count($inputTags)) {
+        if (count($inputTags) === 0) {
             // no input tags
             return $this->noFilter($visibility);
         }
@@ -358,7 +341,7 @@ class BookmarkFilter
         }
 
         // build regex from all tags
-        $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
+        $re = '/^' . implode(array_map([$this, 'tag2regex'], $inputTags)) . '.*$/';
         if (!$casesensitive) {
             // make regex case insensitive
             $re .= 'i';
@@ -368,38 +351,54 @@ class BookmarkFilter
         $filtered = [];
 
         // iterate over each link
-        foreach ($this->bookmarks as $key => $link) {
+        foreach ($this->bookmarks as $key => $bookmark) {
+            if (
+                !$this->pluginManager->filterSearchEntry(
+                    $bookmark,
+                    [
+                    'source' => 'tags',
+                    'tags' => $tags,
+                    'casesensitive' => $casesensitive,
+                    'visibility' => $visibility
+                    ]
+                )
+            ) {
+                continue;
+            }
+
             // check level of visibility
             // ignore non private bookmarks when 'privateonly' is on.
             if ($visibility !== 'all') {
-                if (!$link->isPrivate() && $visibility === 'private') {
+                if (!$bookmark->isPrivate() && $visibility === 'private') {
                     continue;
-                } elseif ($link->isPrivate() && $visibility === 'public') {
+                } elseif ($bookmark->isPrivate() && $visibility === 'public') {
                     continue;
                 }
             }
-            $search = $link->getTagsString(); // build search string, start with tags of current link
-            if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
+            // build search string, start with tags of current link
+            $search = $bookmark->getTagsString($tagsSeparator);
+            if (strlen(trim($bookmark->getDescription())) && strpos($bookmark->getDescription(), '#') !== false) {
                 // description given and at least one possible tag found
-                $descTags = array();
+                $descTags = [];
                 // find all tags in the form of #tag in the description
                 preg_match_all(
                     '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
-                    $link->getDescription(),
+                    $bookmark->getDescription(),
                     $descTags
                 );
                 if (count($descTags[1])) {
                     // there were some tags in the description, add them to the search string
-                    $search .= ' ' . implode(' ', $descTags[1]);
+                    $search .= $tagsSeparator . tags_array2str($descTags[1], $tagsSeparator);
                 }
-            };
+            }
             // match regular expression with search string
             if (!preg_match($re, $search)) {
                 // this entry does _not_ match our regex
                 continue;
             }
-            $filtered[$key] = $link;
+            $filtered[$key] = $bookmark;
         }
+
         return $filtered;
     }
 
@@ -413,55 +412,30 @@ class BookmarkFilter
     public function filterUntagged(string $visibility)
     {
         $filtered = [];
-        foreach ($this->bookmarks as $key => $link) {
+        foreach ($this->bookmarks as $key => $bookmark) {
+            if (
+                !$this->pluginManager->filterSearchEntry(
+                    $bookmark,
+                    ['source' => 'untagged', 'visibility' => $visibility]
+                )
+            ) {
+                continue;
+            }
+
             if ($visibility !== 'all') {
-                if (!$link->isPrivate() && $visibility === 'private') {
+                if (!$bookmark->isPrivate() && $visibility === 'private') {
                     continue;
-                } elseif ($link->isPrivate() && $visibility === 'public') {
+                } elseif ($bookmark->isPrivate() && $visibility === 'public') {
                     continue;
                 }
             }
 
-            if (empty(trim($link->getTagsString()))) {
-                $filtered[$key] = $link;
-            }
-        }
-
-        return $filtered;
-    }
-
-    /**
-     * Returns the list of articles for a given day, chronologically sorted
-     *
-     * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g.
-     *  print_r($mydb->filterDay('20120125'));
-     *
-     * @param string $day day to filter.
-     * @param string $visibility return only all/private/public bookmarks.
-
-     * @return Bookmark[] all link matching given day.
-     *
-     * @throws Exception if date format is invalid.
-     */
-    public function filterDay(string $day, string $visibility)
-    {
-        if (!checkDateFormat('Ymd', $day)) {
-            throw new Exception('Invalid date format');
-        }
-
-        $filtered = [];
-        foreach ($this->bookmarks as $key => $bookmark) {
-            if ($visibility === static::$PUBLIC && $bookmark->isPrivate()) {
-                continue;
-            }
-
-            if ($bookmark->getCreated()->format('Ymd') == $day) {
+            if (empty($bookmark->getTags())) {
                 $filtered[$key] = $bookmark;
             }
         }
 
-        // sort by date ASC
-        return array_reverse($filtered, true);
+        return $filtered;
     }
 
     /**
@@ -483,6 +457,56 @@ class BookmarkFilter
         return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
     }
 
+    /**
+     * generate a regex fragment out of a tag
+     *
+     * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
+     *
+     * @return string generated regex fragment
+     */
+    protected function tag2regex(string $tag): string
+    {
+        $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
+        $len = strlen($tag);
+        if (!$len || $tag === "-" || $tag === "*") {
+            // nothing to search, return empty regex
+            return '';
+        }
+        if ($tag[0] === "-") {
+            // query is negated
+            $i = 1; // use offset to start after '-' character
+            $regex = '(?!'; // create negative lookahead
+        } else {
+            $i = 0; // start at first character
+            $regex = '(?='; // use positive lookahead
+        }
+        // before tag may only be the separator or the beginning
+        $regex .= '.*(?:^|' . $tagsSeparator . ')';
+        // iterate over string, separating it into placeholder and content
+        for (; $i < $len; $i++) {
+            if ($tag[$i] === '*') {
+                // placeholder found
+                $regex .= '[^' . $tagsSeparator . ']*?';
+            } else {
+                // regular characters
+                $offset = strpos($tag, '*', $i);
+                if ($offset === false) {
+                    // no placeholder found, set offset to end of string
+                    $offset = $len;
+                }
+                // subtract one, as we want to get before the placeholder or end of string
+                $offset -= 1;
+                // we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
+                $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
+                // move $i on
+                $i = $offset;
+            }
+        }
+        // after the tag may only be the separator or the end
+        $regex .= '(?:$|' . $tagsSeparator . '))';
+        return $regex;
+    }
+
     /**
      * This method finalize the content of the foundPositions array,
      * by associated all search results to their associated bookmark field,
@@ -537,10 +561,11 @@ class BookmarkFilter
      */
     protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string
     {
-        $content  = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') .'\\';
-        $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') .'\\';
-        $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') .'\\';
-        $content .= mb_convert_case($link->getTagsString(), MB_CASE_LOWER, 'UTF-8') .'\\';
+        $tagString = $link->getTagsString($this->conf->get('general.tags_separator', ' '));
+        $content  = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') . '\\';
+        $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') . '\\';
+        $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') . '\\';
+        $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') . '\\';
 
         $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())];
         $nextField = $lengths['title']['end'] + 1;
@@ -548,7 +573,7 @@ class BookmarkFilter
         $nextField = $lengths['description']['end'] + 1;
         $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())];
         $nextField = $lengths['url']['end'] + 1;
-        $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getTagsString())];
+        $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($tagString)];
 
         return $content;
     }