]> git.immae.eu Git - github/shaarli/Shaarli.git/blobdiff - application/LinkDB.php
namespacing: \Shaarli\Exceptions\IOException
[github/shaarli/Shaarli.git] / application / LinkDB.php
index 7802cc8a1dd408a022d2471efec794fbca1bbfe7..87a53e2e99563edf6749f52c763053599b40a399 100644 (file)
@@ -1,4 +1,7 @@
 <?php
+
+use Shaarli\Exceptions\IOException;
+
 /**
  * Data storage for links.
  *
@@ -107,8 +110,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
         $hidePublicLinks,
         $redirector = '',
         $redirectorEncode = true
-    )
-    {
+    ) {
         $this->datastore = $datastore;
         $this->loggedIn = $isLoggedIn;
         $this->hidePublicLinks = $hidePublicLinks;
@@ -133,16 +135,16 @@ class LinkDB implements Iterator, Countable, ArrayAccess
     {
         // TODO: use exceptions instead of "die"
         if (!$this->loggedIn) {
-            die('You are not authorized to add a link.');
+            die(t('You are not authorized to add a link.'));
         }
         if (!isset($value['id']) || empty($value['url'])) {
-            die('Internal Error: A link should always have an id and URL.');
+            die(t('Internal Error: A link should always have an id and URL.'));
         }
         if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
-            die('You must specify an integer as a key.');
+            die(t('You must specify an integer as a key.'));
         }
         if ($offset !== null && $offset !== $value['id']) {
-            die('Array offset and link ID must be equal.');
+            die(t('Array offset and link ID must be equal.'));
         }
 
         // If the link exists, we reuse the real offset, otherwise new entry
@@ -248,13 +250,16 @@ class LinkDB implements Iterator, Countable, ArrayAccess
         $this->links = array();
         $link = array(
             'id' => 1,
-            'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
-            'url'=>'https://github.com/shaarli/Shaarli/wiki',
-            'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
+            'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'),
+            'url'=>'https://shaarli.readthedocs.io',
+            'description'=>t(
+                'Welcome to Shaarli! This is your first public bookmark. '
+                .'To edit or delete me, you must first login.
 
-To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page.
+To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
 
-You use the community supported version of the original Shaarli project, by Sebastien Sauvage.',
+You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
+            ),
             'private'=>0,
             'created'=> new DateTime(),
             'tags'=>'opensource software'
@@ -264,9 +269,9 @@ You use the community supported version of the original Shaarli project, by Seba
 
         $link = array(
             'id' => 0,
-            'title'=>'My secret stuff... - Pastebin.com',
+            'title'=> t('My secret stuff... - Pastebin.com'),
             'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
-            'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.',
+            'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
             'private'=>1,
             'created'=> new DateTime('1 minute ago'),
             'tags'=>'secretstuff',
@@ -289,13 +294,15 @@ You use the community supported version of the original Shaarli project, by Seba
             return;
         }
 
+        $this->urls = [];
+        $this->ids = [];
         $this->links = FileUtils::readFlatDB($this->datastore, []);
 
         $toremove = array();
         foreach ($this->links as $key => &$link) {
             if (! $this->loggedIn && $link['private'] != 0) {
                 // Transition for not upgraded databases.
-                $toremove[] = $key;
+                unset($this->links[$key]);
                 continue;
             }
 
@@ -315,8 +322,7 @@ You use the community supported version of the original Shaarli project, by Seba
                 } else {
                     $link['real_url'] .= $link['url'];
                 }
-            }
-            else {
+            } else {
                 $link['real_url'] = $link['url'];
             }
 
@@ -329,14 +335,10 @@ You use the community supported version of the original Shaarli project, by Seba
                 }
                 $link['shorturl'] = smallHash($link['linkdate']);
             }
-        }
 
-        // If user is not logged in, filter private links.
-        foreach ($toremove as $offset) {
-            unset($this->links[$offset]);
+            $this->urls[$link['url']] = $key;
+            $this->ids[$link['id']] = $key;
         }
-
-        $this->reorder();
     }
 
     /**
@@ -346,6 +348,7 @@ You use the community supported version of the original Shaarli project, by Seba
      */
     private function write()
     {
+        $this->reorder();
         FileUtils::writeFlatDB($this->datastore, $this->links);
     }
 
@@ -404,7 +407,8 @@ You use the community supported version of the original Shaarli project, by Seba
      *
      * @return array list of shaare found.
      */
-    public function filterDay($request) {
+    public function filterDay($request)
+    {
         $linkFilter = new LinkFilter($this->links);
         return $linkFilter->filter(LinkFilter::$FILTER_DAY, $request);
     }
@@ -417,51 +421,41 @@ You use the community supported version of the original Shaarli project, by Seba
      *                                - searchterm: term search
      * @param bool   $casesensitive Optional: Perform case sensitive filter
      * @param string $visibility    return only all/private/public links
+     * @param string $untaggedonly  return only untagged links
      *
      * @return array filtered links, all links if no suitable filter was provided.
      */
-    public function filterSearch($filterRequest = array(), $casesensitive = false, $visibility = 'all')
-    {
+    public function filterSearch(
+        $filterRequest = array(),
+        $casesensitive = false,
+        $visibility = 'all',
+        $untaggedonly = false
+    ) {
         // Filter link database according to parameters.
-        $searchtags = !empty($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
-        $searchterm = !empty($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
+        $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
+        $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
 
-        // Search tags + fullsearch.
-        if (! empty($searchtags) && ! empty($searchterm)) {
-            $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT;
-            $request = array($searchtags, $searchterm);
-        }
-        // Search by tags.
-        elseif (! empty($searchtags)) {
-            $type = LinkFilter::$FILTER_TAG;
-            $request = $searchtags;
-        }
-        // Fulltext search.
-        elseif (! empty($searchterm)) {
-            $type = LinkFilter::$FILTER_TEXT;
-            $request = $searchterm;
-        }
-        // Otherwise, display without filtering.
-        else {
-            $type = '';
-            $request = '';
-        }
+        // Search tags + fullsearch - blank string parameter will return all links.
+        $type = LinkFilter::$FILTER_TAG | LinkFilter::$FILTER_TEXT; // == "vuotext"
+        $request = [$searchtags, $searchterm];
 
         $linkFilter = new LinkFilter($this);
-        return $linkFilter->filter($type, $request, $casesensitive, $visibility);
+        return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly);
     }
 
     /**
      * Returns the list tags appearing in the links with the given tags
-     * @param $filteringTags: tags selecting the links to consider
-     * @param $visibility: process only all/private/public links
-     * @return: a tag=>linksCount array
+     *
+     * @param array $filteringTags tags selecting the links to consider
+     * @param string $visibility   process only all/private/public links
+     *
+     * @return array tag => linksCount
      */
     public function linksCountPerTag($filteringTags = [], $visibility = 'all')
     {
-        $links = empty($filteringTags) ? $this->links : $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
-        $tags = array();
-        $caseMapping = array();
+        $links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
+        $tags = [];
+        $caseMapping = [];
         foreach ($links as $link) {
             foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
                 if (empty($tag)) {
@@ -475,11 +469,54 @@ You use the community supported version of the original Shaarli project, by Seba
                 $tags[$caseMapping[strtolower($tag)]]++;
             }
         }
-        // Sort tags by usage (most used tag first)
-        arsort($tags);
+
+        /*
+         * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
+         * Also, this function doesn't produce the same result between PHP 5.6 and 7.
+         *
+         * So we now use array_multisort() to sort tags by DESC occurrences,
+         * then ASC alphabetically for equal values.
+         *
+         * @see https://github.com/shaarli/Shaarli/issues/1142
+         */
+        $keys = array_keys($tags);
+        $tmpTags = array_combine($keys, $keys);
+        array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
         return $tags;
     }
 
+    /**
+     * Rename or delete a tag across all links.
+     *
+     * @param string $from Tag to rename
+     * @param string $to   New tag. If none is provided, the from tag will be deleted
+     *
+     * @return array|bool List of altered links or false on error
+     */
+    public function renameTag($from, $to)
+    {
+        if (empty($from)) {
+            return false;
+        }
+        $delete = empty($to);
+        // True for case-sensitive tag search.
+        $linksToAlter = $this->filterSearch(['searchtags' => $from], true);
+        foreach ($linksToAlter as $key => &$value) {
+            $tags = preg_split('/\s+/', trim($value['tags']));
+            if (($pos = array_search($from, $tags)) !== false) {
+                if ($delete) {
+                    unset($tags[$pos]); // Remove tag.
+                } else {
+                    $tags[$pos] = trim($to);
+                }
+                $value['tags'] = trim(implode(' ', array_unique($tags)));
+                $this[$value['id']] = $value;
+            }
+        }
+
+        return $linksToAlter;
+    }
+
     /**
      * Returns the list of days containing articles (oldest first)
      * Output: An array containing days (in format YYYYMMDD).
@@ -507,12 +544,15 @@ You use the community supported version of the original Shaarli project, by Seba
     {
         $order = $order === 'ASC' ? -1 : 1;
         // Reorder array by dates.
-        usort($this->links, function($a, $b) use ($order) {
+        usort($this->links, function ($a, $b) use ($order) {
+            if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) {
+                return $a['sticky'] ? -1 : 1;
+            }
             return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
         });
 
-        $this->urls = array();
-        $this->ids = array();
+        $this->urls = [];
+        $this->ids = [];
         foreach ($this->links as $key => $link) {
             $this->urls[$link['url']] = $key;
             $this->ids[$link['id']] = $key;