Procházet zdrojové kódy

Merge pull request #697 from ArthurHoaro/feature/ids-bis

Link ID refactoring
Arthur před 7 roky
rodič
revize
9cf93bcfc5

+ 3 - 3
application/FeedBuilder.php

@@ -143,7 +143,7 @@ class FeedBuilder
      */
      */
     protected function buildItem($link, $pageaddr)
     protected function buildItem($link, $pageaddr)
     {
     {
-        $link['guid'] = $pageaddr .'?'. smallHash($link['linkdate']);
+        $link['guid'] = $pageaddr .'?'. $link['shorturl'];
         // Check for both signs of a note: starting with ? and 7 chars long.
         // Check for both signs of a note: starting with ? and 7 chars long.
         if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
         if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
             $link['url'] = $pageaddr . $link['url'];
             $link['url'] = $pageaddr . $link['url'];
@@ -156,12 +156,12 @@ class FeedBuilder
         $link['description']  = format_description($link['description'], '', $pageaddr);
         $link['description']  = format_description($link['description'], '', $pageaddr);
         $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;
         $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;
 
 
-        $pubDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
+        $pubDate = $link['created'];
         $link['pub_iso_date'] = $this->getIsoDate($pubDate);
         $link['pub_iso_date'] = $this->getIsoDate($pubDate);
 
 
         // atom:entry elements MUST contain exactly one atom:updated element.
         // atom:entry elements MUST contain exactly one atom:updated element.
         if (!empty($link['updated'])) {
         if (!empty($link['updated'])) {
-            $upDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['updated']);
+            $upDate = $link['updated'];
             $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
             $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
         } else {
         } else {
             $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);;
             $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);;

+ 140 - 44
application/LinkDB.php

@@ -6,15 +6,15 @@
  *
  *
  * Example:
  * Example:
  *    $myLinks = new LinkDB();
  *    $myLinks = new LinkDB();
- *    echo $myLinks['20110826_161819']['title'];
+ *    echo $myLinks[350]['title'];
  *    foreach ($myLinks as $link)
  *    foreach ($myLinks as $link)
  *       echo $link['title'].' at url '.$link['url'].'; description:'.$link['description'];
  *       echo $link['title'].' at url '.$link['url'].'; description:'.$link['description'];
  *
  *
  * Available keys:
  * Available keys:
+ *  - id:       primary key, incremental integer identifier (persistent)
  *  - description: description of the entry
  *  - description: description of the entry
- *  - linkdate: creation date of this entry, format: YYYYMMDD_HHMMSS
- *              (e.g.'20110914_192317')
- *  - updated:  last modification date of this entry, format: YYYYMMDD_HHMMSS
+ *  - created:  creation date of this entry, DateTime object.
+ *  - updated:  last modification date of this entry, DateTime object.
  *  - private:  Is this link private? 0=no, other value=yes
  *  - private:  Is this link private? 0=no, other value=yes
  *  - tags:     tags attached to this entry (separated by spaces)
  *  - tags:     tags attached to this entry (separated by spaces)
  *  - title     Title of the link
  *  - title     Title of the link
@@ -22,11 +22,25 @@
  *              Can be absolute or relative.
  *              Can be absolute or relative.
  *              Relative URLs are permalinks (e.g.'?m-ukcw')
  *              Relative URLs are permalinks (e.g.'?m-ukcw')
  *  - real_url  Absolute processed URL.
  *  - real_url  Absolute processed URL.
+ *  - shorturl  Permalink smallhash
  *
  *
  * Implements 3 interfaces:
  * Implements 3 interfaces:
  *  - ArrayAccess: behaves like an associative array;
  *  - ArrayAccess: behaves like an associative array;
  *  - Countable:   there is a count() method;
  *  - Countable:   there is a count() method;
  *  - Iterator:    usable in foreach () loops.
  *  - Iterator:    usable in foreach () loops.
+ *
+ * ID mechanism:
+ *   ArrayAccess is implemented in a way that will allow to access a link
+ *   with the unique identifier ID directly with $link[ID].
+ *   Note that it's not the real key of the link array attribute.
+ *   This mechanism is in place to have persistent link IDs,
+ *   even though the internal array is reordered by date.
+ *   Example:
+ *     - DB: link #1 (2010-01-01) link #2 (2016-01-01)
+ *     - Order: #2 #1
+ *     - Import links containing: link #3 (2013-01-01)
+ *     - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01)
+ *     - Real order: #2 #3 #1
  */
  */
 class LinkDB implements Iterator, Countable, ArrayAccess
 class LinkDB implements Iterator, Countable, ArrayAccess
 {
 {
@@ -47,11 +61,17 @@ class LinkDB implements Iterator, Countable, ArrayAccess
     //  - value: associative array (keys: title, description...)
     //  - value: associative array (keys: title, description...)
     private $links;
     private $links;
 
 
-    // List of all recorded URLs (key=url, value=linkdate)
-    // for fast reserve search (url-->linkdate)
+    // List of all recorded URLs (key=url, value=link offset)
+    // for fast reserve search (url-->link offset)
     private $urls;
     private $urls;
 
 
-    // List of linkdate keys (for the Iterator interface implementation)
+    /**
+     * @var array List of all links IDS mapped with their array offset.
+     *            Map: id->offset.
+     */
+    protected $ids;
+
+    // List of offset keys (for the Iterator interface implementation)
     private $keys;
     private $keys;
 
 
     // Position in the $this->keys array (for the Iterator interface)
     // Position in the $this->keys array (for the Iterator interface)
@@ -121,14 +141,26 @@ class LinkDB implements Iterator, Countable, ArrayAccess
         if (!$this->loggedIn) {
         if (!$this->loggedIn) {
             die('You are not authorized to add a link.');
             die('You are not authorized to add a link.');
         }
         }
-        if (empty($value['linkdate']) || empty($value['url'])) {
-            die('Internal Error: A link should always have a linkdate and URL.');
+        if (!isset($value['id']) || empty($value['url'])) {
+            die('Internal Error: A link should always have an id and URL.');
         }
         }
-        if (empty($offset)) {
-            die('You must specify a key.');
+        if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) {
+            die('You must specify an integer as a key.');
+        }
+        if (! empty($offset) && $offset !== $value['id']) {
+            die('Array offset and link ID must be equal.');
+        }
+
+        // If the link exists, we reuse the real offset, otherwise new entry
+        $existing = $this->getLinkOffset($offset);
+        if ($existing !== null) {
+            $offset = $existing;
+        } else {
+            $offset = count($this->links);
         }
         }
         $this->links[$offset] = $value;
         $this->links[$offset] = $value;
-        $this->urls[$value['url']]=$offset;
+        $this->urls[$value['url']] = $offset;
+        $this->ids[$value['id']] = $offset;
     }
     }
 
 
     /**
     /**
@@ -136,7 +168,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
      */
      */
     public function offsetExists($offset)
     public function offsetExists($offset)
     {
     {
-        return array_key_exists($offset, $this->links);
+        return array_key_exists($this->getLinkOffset($offset), $this->links);
     }
     }
 
 
     /**
     /**
@@ -148,9 +180,11 @@ class LinkDB implements Iterator, Countable, ArrayAccess
             // TODO: raise an exception
             // TODO: raise an exception
             die('You are not authorized to delete a link.');
             die('You are not authorized to delete a link.');
         }
         }
-        $url = $this->links[$offset]['url'];
+        $realOffset = $this->getLinkOffset($offset);
+        $url = $this->links[$realOffset]['url'];
         unset($this->urls[$url]);
         unset($this->urls[$url]);
-        unset($this->links[$offset]);
+        unset($this->ids[$realOffset]);
+        unset($this->links[$realOffset]);
     }
     }
 
 
     /**
     /**
@@ -158,7 +192,8 @@ class LinkDB implements Iterator, Countable, ArrayAccess
      */
      */
     public function offsetGet($offset)
     public function offsetGet($offset)
     {
     {
-        return isset($this->links[$offset]) ? $this->links[$offset] : null;
+        $realOffset = $this->getLinkOffset($offset);
+        return isset($this->links[$realOffset]) ? $this->links[$realOffset] : null;
     }
     }
 
 
     /**
     /**
@@ -166,7 +201,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
      */
      */
     public function current()
     public function current()
     {
     {
-        return $this->links[$this->keys[$this->position]];
+        return $this[$this->keys[$this->position]];
     }
     }
 
 
     /**
     /**
@@ -192,8 +227,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
      */
      */
     public function rewind()
     public function rewind()
     {
     {
-        $this->keys = array_keys($this->links);
-        rsort($this->keys);
+        $this->keys = array_keys($this->ids);
         $this->position = 0;
         $this->position = 0;
     }
     }
 
 
@@ -219,6 +253,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
         // Create a dummy database for example
         // Create a dummy database for example
         $this->links = array();
         $this->links = array();
         $link = array(
         $link = array(
+            'id' => 1,
             'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
             'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
             'url'=>'https://github.com/shaarli/Shaarli/wiki',
             '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.
             'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
@@ -227,20 +262,23 @@ To learn how to use Shaarli, consult the link "Help/documentation" at the bottom
 
 
 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,
             'private'=>0,
-            'linkdate'=> date('Ymd_His'),
+            'created'=> new DateTime(),
             'tags'=>'opensource software'
             'tags'=>'opensource software'
         );
         );
-        $this->links[$link['linkdate']] = $link;
+        $link['shorturl'] = link_small_hash($link['created'], $link['id']);
+        $this->links[1] = $link;
 
 
         $link = array(
         $link = array(
+            'id' => 0,
             'title'=>'My secret stuff... - Pastebin.com',
             'title'=>'My secret stuff... - Pastebin.com',
             'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
             'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
             'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.',
             'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.',
             'private'=>1,
             'private'=>1,
-            'linkdate'=> date('Ymd_His', strtotime('-1 minute')),
-            'tags'=>'secretstuff'
+            'created'=> new DateTime('1 minute ago'),
+            'tags'=>'secretstuff',
         );
         );
-        $this->links[$link['linkdate']] = $link;
+        $link['shorturl'] = link_small_hash($link['created'], $link['id']);
+        $this->links[0] = $link;
 
 
         // Write database to disk
         // Write database to disk
         $this->write();
         $this->write();
@@ -251,7 +289,6 @@ You use the community supported version of the original Shaarli project, by Seba
      */
      */
     private function read()
     private function read()
     {
     {
-
         // Public links are hidden and user not logged in => nothing to show
         // Public links are hidden and user not logged in => nothing to show
         if ($this->hidePublicLinks && !$this->loggedIn) {
         if ($this->hidePublicLinks && !$this->loggedIn) {
             $this->links = array();
             $this->links = array();
@@ -269,23 +306,13 @@ You use the community supported version of the original Shaarli project, by Seba
                        strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
                        strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
         }
         }
 
 
-        // If user is not logged in, filter private links.
-        if (!$this->loggedIn) {
-            $toremove = array();
-            foreach ($this->links as $link) {
-                if ($link['private'] != 0) {
-                    $toremove[] = $link['linkdate'];
-                }
-            }
-            foreach ($toremove as $linkdate) {
-                unset($this->links[$linkdate]);
+        $toremove = array();
+        foreach ($this->links as $key => &$link) {
+            if (! $this->loggedIn && $link['private'] != 0) {
+                // Transition for not upgraded databases.
+                $toremove[] = $key;
+                continue;
             }
             }
-        }
-
-        $this->urls = array();
-        foreach ($this->links as &$link) {
-            // Keep the list of the mapping URLs-->linkdate up-to-date.
-            $this->urls[$link['url']] = $link['linkdate'];
 
 
             // Sanitize data fields.
             // Sanitize data fields.
             sanitizeLink($link);
             sanitizeLink($link);
@@ -307,7 +334,24 @@ You use the community supported version of the original Shaarli project, by Seba
             else {
             else {
                 $link['real_url'] = $link['url'];
                 $link['real_url'] = $link['url'];
             }
             }
+
+            // To be able to load links before running the update, and prepare the update
+            if (! isset($link['created'])) {
+                $link['id'] = $link['linkdate'];
+                $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
+                if (! empty($link['updated'])) {
+                    $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']);
+                }
+                $link['shorturl'] = smallHash($link['linkdate']);
+            }
+        }
+
+        // If user is not logged in, filter private links.
+        foreach ($toremove as $offset) {
+            unset($this->links[$offset]);
         }
         }
+
+        $this->reorder();
     }
     }
 
 
     /**
     /**
@@ -430,7 +474,7 @@ You use the community supported version of the original Shaarli project, by Seba
             $request = '';
             $request = '';
         }
         }
 
 
-        $linkFilter = new LinkFilter($this->links);
+        $linkFilter = new LinkFilter($this);
         return $linkFilter->filter($type, $request, $casesensitive, $privateonly);
         return $linkFilter->filter($type, $request, $casesensitive, $privateonly);
     }
     }
 
 
@@ -467,12 +511,64 @@ You use the community supported version of the original Shaarli project, by Seba
     public function days()
     public function days()
     {
     {
         $linkDays = array();
         $linkDays = array();
-        foreach (array_keys($this->links) as $day) {
-            $linkDays[substr($day, 0, 8)] = 0;
+        foreach ($this->links as $link) {
+            $linkDays[$link['created']->format('Ymd')] = 0;
         }
         }
         $linkDays = array_keys($linkDays);
         $linkDays = array_keys($linkDays);
         sort($linkDays);
         sort($linkDays);
 
 
         return $linkDays;
         return $linkDays;
     }
     }
+
+    /**
+     * Reorder links by creation date (newest first).
+     *
+     * Also update the urls and ids mapping arrays.
+     *
+     * @param string $order ASC|DESC
+     */
+    public function reorder($order = 'DESC')
+    {
+        $order = $order === 'ASC' ? -1 : 1;
+        // Reorder array by dates.
+        usort($this->links, function($a, $b) use ($order) {
+            return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
+        });
+
+        $this->urls = array();
+        $this->ids = array();
+        foreach ($this->links as $key => $link) {
+            $this->urls[$link['url']] = $key;
+            $this->ids[$link['id']] = $key;
+        }
+    }
+
+    /**
+     * Return the next key for link creation.
+     * E.g. If the last ID is 597, the next will be 598.
+     *
+     * @return int next ID.
+     */
+    public function getNextId()
+    {
+        if (!empty($this->ids)) {
+            return max(array_keys($this->ids)) + 1;
+        }
+        return 0;
+    }
+
+    /**
+     * Returns a link offset in links array from its unique ID.
+     *
+     * @param int $id Persistent ID of a link.
+     *
+     * @return int Real offset in local array, or null if doesn't exist.
+     */
+    protected function getLinkOffset($id)
+    {
+        if (isset($this->ids[$id])) {
+            return $this->ids[$id];
+        }
+        return null;
+    }
 }
 }

+ 17 - 20
application/LinkFilter.php

@@ -33,12 +33,12 @@ class LinkFilter
     public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
     public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
 
 
     /**
     /**
-     * @var array all available links.
+     * @var LinkDB all available links.
      */
      */
     private $links;
     private $links;
 
 
     /**
     /**
-     * @param array $links initialization.
+     * @param LinkDB $links initialization.
      */
      */
     public function __construct($links)
     public function __construct($links)
     {
     {
@@ -94,18 +94,16 @@ class LinkFilter
     private function noFilter($privateonly = false)
     private function noFilter($privateonly = false)
     {
     {
         if (! $privateonly) {
         if (! $privateonly) {
-            krsort($this->links);
             return $this->links;
             return $this->links;
         }
         }
 
 
         $out = array();
         $out = array();
-        foreach ($this->links as $value) {
+        foreach ($this->links as $key => $value) {
             if ($value['private']) {
             if ($value['private']) {
-                $out[$value['linkdate']] = $value;
+                $out[$key] = $value;
             }
             }
         }
         }
 
 
-        krsort($out);
         return $out;
         return $out;
     }
     }
 
 
@@ -121,10 +119,10 @@ class LinkFilter
     private function filterSmallHash($smallHash)
     private function filterSmallHash($smallHash)
     {
     {
         $filtered = array();
         $filtered = array();
-        foreach ($this->links as $l) {
-            if ($smallHash == smallHash($l['linkdate'])) {
+        foreach ($this->links as $key => $l) {
+            if ($smallHash == $l['shorturl']) {
                 // Yes, this is ugly and slow
                 // Yes, this is ugly and slow
-                $filtered[$l['linkdate']] = $l;
+                $filtered[$key] = $l;
                 return $filtered;
                 return $filtered;
             }
             }
         }
         }
@@ -188,7 +186,7 @@ class LinkFilter
         $keys = array('title', 'description', 'url', 'tags');
         $keys = array('title', 'description', 'url', 'tags');
 
 
         // Iterate over every stored link.
         // Iterate over every stored link.
-        foreach ($this->links as $link) {
+        foreach ($this->links as $id => $link) {
 
 
             // ignore non private links when 'privatonly' is on.
             // ignore non private links when 'privatonly' is on.
             if (! $link['private'] && $privateonly === true) {
             if (! $link['private'] && $privateonly === true) {
@@ -222,11 +220,10 @@ class LinkFilter
             }
             }
 
 
             if ($found) {
             if ($found) {
-                $filtered[$link['linkdate']] = $link;
+                $filtered[$id] = $link;
             }
             }
         }
         }
 
 
-        krsort($filtered);
         return $filtered;
         return $filtered;
     }
     }
 
 
@@ -256,7 +253,7 @@ class LinkFilter
             return $filtered;
             return $filtered;
         }
         }
 
 
-        foreach ($this->links as $link) {
+        foreach ($this->links as $key => $link) {
             // ignore non private links when 'privatonly' is on.
             // ignore non private links when 'privatonly' is on.
             if (! $link['private'] && $privateonly === true) {
             if (! $link['private'] && $privateonly === true) {
                 continue;
                 continue;
@@ -278,10 +275,9 @@ class LinkFilter
             }
             }
 
 
             if ($found) {
             if ($found) {
-                $filtered[$link['linkdate']] = $link;
+                $filtered[$key] = $link;
             }
             }
         }
         }
-        krsort($filtered);
         return $filtered;
         return $filtered;
     }
     }
 
 
@@ -304,13 +300,14 @@ class LinkFilter
         }
         }
 
 
         $filtered = array();
         $filtered = array();
-        foreach ($this->links as $l) {
-            if (startsWith($l['linkdate'], $day)) {
-                $filtered[$l['linkdate']] = $l;
+        foreach ($this->links as $key => $l) {
+            if ($l['created']->format('Ymd') == $day) {
+                $filtered[$key] = $l;
             }
             }
         }
         }
-        ksort($filtered);
-        return $filtered;
+
+        // sort by date ASC
+        return array_reverse($filtered, true);
     }
     }
 
 
     /**
     /**

+ 13 - 0
application/LinkUtils.php

@@ -169,3 +169,16 @@ function space2nbsp($text)
 function format_description($description, $redirector = '', $indexUrl = '') {
 function format_description($description, $redirector = '', $indexUrl = '') {
     return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl)));
     return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl)));
 }
 }
+
+/**
+ * Generate a small hash for a link.
+ *
+ * @param DateTime $date Link creation date.
+ * @param int      $id   Link ID.
+ *
+ * @return string the small hash generated from link data.
+ */
+function link_small_hash($date, $id)
+{
+    return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id);
+}

+ 11 - 15
application/NetscapeBookmarkUtils.php

@@ -38,7 +38,7 @@ class NetscapeBookmarkUtils
             if ($link['private'] == 0 && $selection == 'private') {
             if ($link['private'] == 0 && $selection == 'private') {
                 continue;
                 continue;
             }
             }
-            $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
+            $date = $link['created'];
             $link['timestamp'] = $date->getTimestamp();
             $link['timestamp'] = $date->getTimestamp();
             $link['taglist'] = str_replace(' ', ',', $link['tags']);
             $link['taglist'] = str_replace(' ', ',', $link['tags']);
 
 
@@ -147,7 +147,6 @@ class NetscapeBookmarkUtils
                 'url' => $bkm['uri'],
                 'url' => $bkm['uri'],
                 'description' => $bkm['note'],
                 'description' => $bkm['note'],
                 'private' => $private,
                 'private' => $private,
-                'linkdate'=> '',
                 'tags' => $bkm['tags']
                 'tags' => $bkm['tags']
             );
             );
 
 
@@ -161,25 +160,22 @@ class NetscapeBookmarkUtils
                 }
                 }
 
 
                 // Overwrite an existing link, keep its date
                 // Overwrite an existing link, keep its date
-                $newLink['linkdate'] = $existingLink['linkdate'];
-                $linkDb[$existingLink['linkdate']] = $newLink;
+                $newLink['id'] = $existingLink['id'];
+                $newLink['created'] = $existingLink['created'];
+                $newLink['updated'] = new DateTime();
+                $linkDb[$existingLink['id']] = $newLink;
                 $importCount++;
                 $importCount++;
                 $overwriteCount++;
                 $overwriteCount++;
                 continue;
                 continue;
             }
             }
 
 
-            // Add a new link
+            // Add a new link - @ used for UNIX timestamps
             $newLinkDate = new DateTime('@'.strval($bkm['time']));
             $newLinkDate = new DateTime('@'.strval($bkm['time']));
-            while (!empty($linkDb[$newLinkDate->format(LinkDB::LINK_DATE_FORMAT)])) {
-                // Ensure the date/time is not already used
-                // - this hack is necessary as the date/time acts as a primary key
-                // - apply 1 second increments until an unused index is found
-                // See https://github.com/shaarli/Shaarli/issues/351
-                $newLinkDate->add(new DateInterval('PT1S'));
-            }
-            $linkDbDate = $newLinkDate->format(LinkDB::LINK_DATE_FORMAT);
-            $newLink['linkdate'] = $linkDbDate;
-            $linkDb[$linkDbDate] = $newLink;
+            $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
+            $newLink['created'] = $newLinkDate;
+            $newLink['id'] = $linkDb->getNextId();
+            $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
+            $linkDb[$newLink['id']] = $newLink;
             $importCount++;
             $importCount++;
         }
         }
 
 

+ 43 - 2
application/Updater.php

@@ -138,10 +138,10 @@ class Updater
     public function updateMethodRenameDashTags()
     public function updateMethodRenameDashTags()
     {
     {
         $linklist = $this->linkDB->filterSearch();
         $linklist = $this->linkDB->filterSearch();
-        foreach ($linklist as $link) {
+        foreach ($linklist as $key => $link) {
             $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
             $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
             $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
             $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
-            $this->linkDB[$link['linkdate']] = $link;
+            $this->linkDB[$key] = $link;
         }
         }
         $this->linkDB->save($this->conf->get('resource.page_cache'));
         $this->linkDB->save($this->conf->get('resource.page_cache'));
         return true;
         return true;
@@ -215,6 +215,47 @@ class Updater
         }
         }
         return true;
         return true;
     }
     }
+
+    /**
+     * Update the database to use the new ID system, which replaces linkdate primary keys.
+     * Also, creation and update dates are now DateTime objects (done by LinkDB).
+     *
+     * Since this update is very sensitve (changing the whole database), the datastore will be
+     * automatically backed up into the file datastore.<datetime>.php.
+     *
+     * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
+     * which will be saved by this method.
+     *
+     * @return bool true if the update is successful, false otherwise.
+     */
+    public function updateMethodDatastoreIds()
+    {
+        // up to date database
+        if (isset($this->linkDB[0])) {
+            return true;
+        }
+
+        $save = $this->conf->get('resource.data_dir') .'/datastore.'. date('YmdHis') .'.php';
+        copy($this->conf->get('resource.datastore'), $save);
+
+        $links = array();
+        foreach ($this->linkDB as $offset => $value) {
+            $links[] = $value;
+            unset($this->linkDB[$offset]);
+        }
+        $links = array_reverse($links);
+        $cpt = 0;
+        foreach ($links as $l) {
+            unset($l['linkdate']);
+            $l['id'] = $cpt;
+            $this->linkDB[$cpt++] = $l;
+        }
+
+        $this->linkDB->save($this->conf->get('resource.page_cache'));
+        $this->linkDB->reorder();
+
+        return true;
+    }
 }
 }
 
 
 /**
 /**

+ 5 - 1
application/Utils.php

@@ -31,7 +31,11 @@ function logm($logFile, $clientIp, $message)
  *   - are NOT cryptographically secure (they CAN be forged)
  *   - are NOT cryptographically secure (they CAN be forged)
  *
  *
  *  In Shaarli, they are used as a tinyurl-like link to individual entries,
  *  In Shaarli, they are used as a tinyurl-like link to individual entries,
- *  e.g. smallHash('20111006_131924') --> yZH23w
+ *  built once with the combination of the date and item ID.
+ *  e.g. smallHash('20111006_131924' . 142) --> eaWxtQ
+ *
+ * @warning before v0.8.1, smallhashes were built only with the date,
+ *          and their value has been preserved.
  *
  *
  * @param string $text Create a hash from this text.
  * @param string $text Create a hash from this text.
  *
  *

+ 58 - 46
index.php

@@ -564,24 +564,19 @@ function showDailyRSS($conf) {
     );
     );
 
 
     /* Some Shaarlies may have very few links, so we need to look
     /* Some Shaarlies may have very few links, so we need to look
-       back in time (rsort()) until we have enough days ($nb_of_days).
+       back in time until we have enough days ($nb_of_days).
     */
     */
-    $linkdates = array();
-    foreach ($LINKSDB as $linkdate => $value) {
-        $linkdates[] = $linkdate;
-    }
-    rsort($linkdates);
     $nb_of_days = 7; // We take 7 days.
     $nb_of_days = 7; // We take 7 days.
     $today = date('Ymd');
     $today = date('Ymd');
     $days = array();
     $days = array();
 
 
-    foreach ($linkdates as $linkdate) {
-        $day = substr($linkdate, 0, 8); // Extract day (without time)
-        if (strcmp($day,$today) < 0) {
+    foreach ($LINKSDB as $link) {
+        $day = $link['created']->format('Ymd'); // Extract day (without time)
+        if (strcmp($day, $today) < 0) {
             if (empty($days[$day])) {
             if (empty($days[$day])) {
                 $days[$day] = array();
                 $days[$day] = array();
             }
             }
-            $days[$day][] = $linkdate;
+            $days[$day][] = $link;
         }
         }
 
 
         if (count($days) > $nb_of_days) {
         if (count($days) > $nb_of_days) {
@@ -601,24 +596,18 @@ function showDailyRSS($conf) {
     echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL;
     echo '<copyright>'. $pageaddr .'</copyright>'. PHP_EOL;
 
 
     // For each day.
     // For each day.
-    foreach ($days as $day => $linkdates) {
+    foreach ($days as $day => $links) {
         $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
         $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
         $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day);  // Absolute URL of the corresponding "Daily" page.
         $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day);  // Absolute URL of the corresponding "Daily" page.
 
 
-        // Build the HTML body of this RSS entry.
-        $links = array();
-
         // We pre-format some fields for proper output.
         // We pre-format some fields for proper output.
-        foreach ($linkdates as $linkdate) {
-            $l = $LINKSDB[$linkdate];
-            $l['formatedDescription'] = format_description($l['description'], $conf->get('redirector.url'));
-            $l['thumbnail'] = thumbnail($conf, $l['url']);
-            $l_date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $l['linkdate']);
-            $l['timestamp'] = $l_date->getTimestamp();
-            if (startsWith($l['url'], '?')) {
-                $l['url'] = index_url($_SERVER) . $l['url'];  // make permalink URL absolute
+        foreach ($links as &$link) {
+            $link['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url'));
+            $link['thumbnail'] = thumbnail($conf, $link['url']);
+            $link['timestamp'] = $link['created']->getTimestamp();
+            if (startsWith($link['url'], '?')) {
+                $link['url'] = index_url($_SERVER) . $link['url'];  // make permalink URL absolute
             }
             }
-            $links[$linkdate] = $l;
         }
         }
 
 
         // Then build the HTML for this day:
         // Then build the HTML for this day:
@@ -680,8 +669,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager)
         $linksToDisplay[$key]['taglist']=$taglist;
         $linksToDisplay[$key]['taglist']=$taglist;
         $linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url'));
         $linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $conf->get('redirector.url'));
         $linksToDisplay[$key]['thumbnail'] = thumbnail($conf, $link['url']);
         $linksToDisplay[$key]['thumbnail'] = thumbnail($conf, $link['url']);
-        $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
-        $linksToDisplay[$key]['timestamp'] = $date->getTimestamp();
+        $linksToDisplay[$key]['timestamp'] =  $link['created']->getTimestamp();
     }
     }
 
 
     /* We need to spread the articles on 3 columns.
     /* We need to spread the articles on 3 columns.
@@ -831,7 +819,7 @@ function renderPage($conf, $pluginManager)
         // Get only links which have a thumbnail.
         // Get only links which have a thumbnail.
         foreach($links as $link)
         foreach($links as $link)
         {
         {
-            $permalink='?'.escape(smallHash($link['linkdate']));
+            $permalink='?'.$link['shorturl'];
             $thumb=lazyThumbnail($conf, $link['url'],$permalink);
             $thumb=lazyThumbnail($conf, $link['url'],$permalink);
             if ($thumb!='') // Only output links which have a thumbnail.
             if ($thumb!='') // Only output links which have a thumbnail.
             {
             {
@@ -1245,13 +1233,28 @@ function renderPage($conf, $pluginManager)
     // -------- User clicked the "Save" button when editing a link: Save link to database.
     // -------- User clicked the "Save" button when editing a link: Save link to database.
     if (isset($_POST['save_edit']))
     if (isset($_POST['save_edit']))
     {
     {
-        $linkdate = $_POST['lf_linkdate'];
-        $updated = isset($LINKSDB[$linkdate]) ? strval(date('Ymd_His')) : false;
-
         // Go away!
         // Go away!
         if (! tokenOk($_POST['token'])) {
         if (! tokenOk($_POST['token'])) {
             die('Wrong token.');
             die('Wrong token.');
         }
         }
+
+        // lf_id should only be present if the link exists.
+        $id = !empty($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId();
+        // Linkdate is kept here to:
+        //   - use the same permalink for notes as they're displayed when creating them
+        //   - let users hack creation date of their posts
+        //     See: https://github.com/shaarli/Shaarli/wiki/Datastore-hacks#changing-the-timestamp-for-a-link
+        $linkdate = escape($_POST['lf_linkdate']);
+        if (isset($LINKSDB[$id])) {
+            // Edit
+            $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
+            $updated = new DateTime();
+        } else {
+            // New link
+            $created = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
+            $updated = null;
+        }
+
         // Remove multiple spaces.
         // Remove multiple spaces.
         $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags']));
         $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags']));
         // Remove first '-' char in tags.
         // Remove first '-' char in tags.
@@ -1268,14 +1271,17 @@ function renderPage($conf, $pluginManager)
         }
         }
 
 
         $link = array(
         $link = array(
+            'id' => $id,
             'title' => trim($_POST['lf_title']),
             'title' => trim($_POST['lf_title']),
             'url' => $url,
             'url' => $url,
             'description' => $_POST['lf_description'],
             'description' => $_POST['lf_description'],
             'private' => (isset($_POST['lf_private']) ? 1 : 0),
             'private' => (isset($_POST['lf_private']) ? 1 : 0),
-            'linkdate' => $linkdate,
+            'created' => $created,
             'updated' => $updated,
             'updated' => $updated,
-            'tags' => str_replace(',', ' ', $tags)
+            'tags' => str_replace(',', ' ', $tags),
+            'shorturl' => link_small_hash($created, $id),
         );
         );
+
         // If title is empty, use the URL as title.
         // If title is empty, use the URL as title.
         if ($link['title'] == '') {
         if ($link['title'] == '') {
             $link['title'] = $link['url'];
             $link['title'] = $link['url'];
@@ -1283,7 +1289,7 @@ function renderPage($conf, $pluginManager)
 
 
         $pluginManager->executeHooks('save_link', $link);
         $pluginManager->executeHooks('save_link', $link);
 
 
-        $LINKSDB[$linkdate] = $link;
+        $LINKSDB[$id] = $link;
         $LINKSDB->save($conf->get('resource.page_cache'));
         $LINKSDB->save($conf->get('resource.page_cache'));
         pubsubhub($conf);
         pubsubhub($conf);
 
 
@@ -1296,7 +1302,7 @@ function renderPage($conf, $pluginManager)
         $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
         $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
         $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
         $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
         // Scroll to the link which has been edited.
         // Scroll to the link which has been edited.
-        $location .= '#' . smallHash($_POST['lf_linkdate']);
+        $location .= '#' . $link['shorturl'];
         // After saving the link, redirect to the page the user was on.
         // After saving the link, redirect to the page the user was on.
         header('Location: '. $location);
         header('Location: '. $location);
         exit;
         exit;
@@ -1307,8 +1313,10 @@ function renderPage($conf, $pluginManager)
     {
     {
         // If we are called from the bookmarklet, we must close the popup:
         // If we are called from the bookmarklet, we must close the popup:
         if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
         if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo '<script>self.close();</script>'; exit; }
+        $link = $LINKSDB[(int) escape($_POST['lf_id'])];
         $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
         $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
-        $returnurl .= '#'.smallHash($_POST['lf_linkdate']);  // Scroll to the link which has been edited.
+        // Scroll to the link which has been edited.
+        $returnurl .= '#'. $link['shorturl'];
         $returnurl = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
         $returnurl = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
         header('Location: '.$returnurl); // After canceling, redirect to the page the user was on.
         header('Location: '.$returnurl); // After canceling, redirect to the page the user was on.
         exit;
         exit;
@@ -1318,14 +1326,17 @@ function renderPage($conf, $pluginManager)
     if (isset($_POST['delete_link']))
     if (isset($_POST['delete_link']))
     {
     {
         if (!tokenOk($_POST['token'])) die('Wrong token.');
         if (!tokenOk($_POST['token'])) die('Wrong token.');
+
         // We do not need to ask for confirmation:
         // We do not need to ask for confirmation:
         // - confirmation is handled by JavaScript
         // - confirmation is handled by JavaScript
         // - we are protected from XSRF by the token.
         // - we are protected from XSRF by the token.
-        $linkdate=$_POST['lf_linkdate'];
 
 
-        $pluginManager->executeHooks('delete_link', $LINKSDB[$linkdate]);
+        // FIXME! We keep `lf_linkdate` for consistency before a proper API. To be removed.
+        $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : intval(escape($_POST['lf_linkdate']));
+
+        $pluginManager->executeHooks('delete_link', $LINKSDB[$id]);
 
 
-        unset($LINKSDB[$linkdate]);
+        unset($LINKSDB[$id]);
         $LINKSDB->save('resource.page_cache'); // save to disk
         $LINKSDB->save('resource.page_cache'); // save to disk
 
 
         // If we are called from the bookmarklet, we must close the popup:
         // If we are called from the bookmarklet, we must close the popup:
@@ -1364,8 +1375,10 @@ function renderPage($conf, $pluginManager)
     // -------- User clicked the "EDIT" button on a link: Display link edit form.
     // -------- User clicked the "EDIT" button on a link: Display link edit form.
     if (isset($_GET['edit_link']))
     if (isset($_GET['edit_link']))
     {
     {
-        $link = $LINKSDB[$_GET['edit_link']];  // Read database
+        $id = (int) escape($_GET['edit_link']);
+        $link = $LINKSDB[$id];  // Read database
         if (!$link) { header('Location: ?'); exit; } // Link not found in database.
         if (!$link) { header('Location: ?'); exit; } // Link not found in database.
+        $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
         $data = array(
         $data = array(
             'link' => $link,
             'link' => $link,
             'link_is_new' => false,
             'link_is_new' => false,
@@ -1389,10 +1402,10 @@ function renderPage($conf, $pluginManager)
         $link_is_new = false;
         $link_is_new = false;
         // Check if URL is not already in database (in this case, we will edit the existing link)
         // Check if URL is not already in database (in this case, we will edit the existing link)
         $link = $LINKSDB->getLinkFromUrl($url);
         $link = $LINKSDB->getLinkFromUrl($url);
-        if (!$link)
+        if (! $link)
         {
         {
             $link_is_new = true;
             $link_is_new = true;
-            $linkdate = strval(date('Ymd_His'));
+            $linkdate = strval(date(LinkDB::LINK_DATE_FORMAT));
             // Get title if it was provided in URL (by the bookmarklet).
             // Get title if it was provided in URL (by the bookmarklet).
             $title = empty($_GET['title']) ? '' : escape($_GET['title']);
             $title = empty($_GET['title']) ? '' : escape($_GET['title']);
             // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
             // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
@@ -1416,7 +1429,7 @@ function renderPage($conf, $pluginManager)
             }
             }
 
 
             if ($url == '') {
             if ($url == '') {
-                $url = '?' . smallHash($linkdate);
+                $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
                 $title = 'Note: ';
                 $title = 'Note: ';
             }
             }
             $url = escape($url);
             $url = escape($url);
@@ -1430,6 +1443,8 @@ function renderPage($conf, $pluginManager)
                 'tags' => $tags,
                 'tags' => $tags,
                 'private' => $private
                 'private' => $private
             );
             );
+        } else {
+            $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
         }
         }
 
 
         $data = array(
         $data = array(
@@ -1635,18 +1650,15 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager)
         $link['description'] = format_description($link['description'], $conf->get('redirector.url'));
         $link['description'] = format_description($link['description'], $conf->get('redirector.url'));
         $classLi =  ($i % 2) != 0 ? '' : 'publicLinkHightLight';
         $classLi =  ($i % 2) != 0 ? '' : 'publicLinkHightLight';
         $link['class'] = $link['private'] == 0 ? $classLi : 'private';
         $link['class'] = $link['private'] == 0 ? $classLi : 'private';
-        $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
-        $link['timestamp'] = $date->getTimestamp();
+        $link['timestamp'] = $link['created']->getTimestamp();
         if (! empty($link['updated'])) {
         if (! empty($link['updated'])) {
-            $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['updated']);
-            $link['updated_timestamp'] = $date->getTimestamp();
+            $link['updated_timestamp'] = $link['updated']->getTimestamp();
         } else {
         } else {
             $link['updated_timestamp'] = '';
             $link['updated_timestamp'] = '';
         }
         }
         $taglist = explode(' ', $link['tags']);
         $taglist = explode(' ', $link['tags']);
         uasort($taglist, 'strcasecmp');
         uasort($taglist, 'strcasecmp');
         $link['taglist'] = $taglist;
         $link['taglist'] = $taglist;
-        $link['shorturl'] = smallHash($link['linkdate']);
         // Check for both signs of a note: starting with ? and 7 chars long.
         // Check for both signs of a note: starting with ? and 7 chars long.
         if ($link['url'][0] === '?' &&
         if ($link['url'][0] === '?' &&
             strlen($link['url']) === 7) {
             strlen($link['url']) === 7) {

+ 2 - 2
plugins/isso/isso.php

@@ -41,9 +41,9 @@ function hook_isso_render_linklist($data, $conf)
     // Only display comments for permalinks.
     // Only display comments for permalinks.
     if (count($data['links']) == 1 && empty($data['search_tags']) && empty($data['search_term'])) {
     if (count($data['links']) == 1 && empty($data['search_tags']) && empty($data['search_term'])) {
         $link = reset($data['links']);
         $link = reset($data['links']);
-        $isso_html = file_get_contents(PluginManager::$PLUGINS_PATH . '/isso/isso.html');
+        $issoHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/isso/isso.html');
 
 
-        $isso = sprintf($isso_html, $issoUrl, $issoUrl, $link['linkdate'], $link['linkdate']);
+        $isso = sprintf($issoHtml, $issoUrl, $issoUrl, $link['id'], $link['id']);
         $data['plugin_end_zone'][] = $isso;
         $data['plugin_end_zone'][] = $isso;
 
 
         // Hackish way to include this CSS file only when necessary.
         // Hackish way to include this CSS file only when necessary.

+ 18 - 13
tests/FeedBuilderTest.php

@@ -84,8 +84,9 @@ class FeedBuilderTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
         $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
 
 
         // Test first link (note link)
         // Test first link (note link)
-        $link = array_shift($data['links']);
-        $this->assertEquals('20150310_114651', $link['linkdate']);
+        $link = reset($data['links']);
+        $this->assertEquals(41, $link['id']);
+        $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
         $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
         $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
         $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
         $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
         $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']);
         $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']);
@@ -99,14 +100,14 @@ class FeedBuilderTest extends PHPUnit_Framework_TestCase
         $this->assertEquals('sTuff', $link['taglist'][0]);
         $this->assertEquals('sTuff', $link['taglist'][0]);
 
 
         // Test URL with external link.
         // Test URL with external link.
-        $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $data['links']['20150310_114633']['url']);
+        $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $data['links'][8]['url']);
 
 
         // Test multitags.
         // Test multitags.
-        $this->assertEquals(5, count($data['links']['20141125_084734']['taglist']));
-        $this->assertEquals('css', $data['links']['20141125_084734']['taglist'][0]);
+        $this->assertEquals(5, count($data['links'][6]['taglist']));
+        $this->assertEquals('css', $data['links'][6]['taglist'][0]);
 
 
         // Test update date
         // Test update date
-        $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links']['20150310_114633']['up_iso_date']);
+        $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links'][8]['up_iso_date']);
     }
     }
 
 
     /**
     /**
@@ -119,9 +120,9 @@ class FeedBuilderTest extends PHPUnit_Framework_TestCase
         $data = $feedBuilder->buildData();
         $data = $feedBuilder->buildData();
         $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
         $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links']));
         $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['last_update']);
         $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['last_update']);
-        $link = array_shift($data['links']);
+        $link = reset($data['links']);
         $this->assertRegExp('/2015-03-10T11:46:51\+\d{2}:\d{2}/', $link['pub_iso_date']);
         $this->assertRegExp('/2015-03-10T11:46:51\+\d{2}:\d{2}/', $link['pub_iso_date']);
-        $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links']['20150310_114633']['up_iso_date']);
+        $this->assertRegExp('/2016-08-03T09:30:33\+\d{2}:\d{2}/', $data['links'][8]['up_iso_date']);
     }
     }
 
 
     /**
     /**
@@ -138,7 +139,8 @@ class FeedBuilderTest extends PHPUnit_Framework_TestCase
         $data = $feedBuilder->buildData();
         $data = $feedBuilder->buildData();
         $this->assertEquals(1, count($data['links']));
         $this->assertEquals(1, count($data['links']));
         $link = array_shift($data['links']);
         $link = array_shift($data['links']);
-        $this->assertEquals('20150310_114651', $link['linkdate']);
+        $this->assertEquals(41, $link['id']);
+        $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
     }
     }
 
 
     /**
     /**
@@ -154,7 +156,8 @@ class FeedBuilderTest extends PHPUnit_Framework_TestCase
         $data = $feedBuilder->buildData();
         $data = $feedBuilder->buildData();
         $this->assertEquals(1, count($data['links']));
         $this->assertEquals(1, count($data['links']));
         $link = array_shift($data['links']);
         $link = array_shift($data['links']);
-        $this->assertEquals('20150310_114651', $link['linkdate']);
+        $this->assertEquals(41, $link['id']);
+        $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
     }
     }
 
 
     /**
     /**
@@ -170,15 +173,17 @@ class FeedBuilderTest extends PHPUnit_Framework_TestCase
         $this->assertTrue($data['usepermalinks']);
         $this->assertTrue($data['usepermalinks']);
         // First link is a permalink
         // First link is a permalink
         $link = array_shift($data['links']);
         $link = array_shift($data['links']);
-        $this->assertEquals('20150310_114651', $link['linkdate']);
+        $this->assertEquals(41, $link['id']);
+        $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), $link['created']);
         $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
         $this->assertEquals('http://host.tld/?WDWyig', $link['guid']);
         $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
         $this->assertEquals('http://host.tld/?WDWyig', $link['url']);
         $this->assertContains('Direct link', $link['description']);
         $this->assertContains('Direct link', $link['description']);
         $this->assertContains('http://host.tld/?WDWyig', $link['description']);
         $this->assertContains('http://host.tld/?WDWyig', $link['description']);
         // Second link is a direct link
         // Second link is a direct link
         $link = array_shift($data['links']);
         $link = array_shift($data['links']);
-        $this->assertEquals('20150310_114633', $link['linkdate']);
-        $this->assertEquals('http://host.tld/?kLHmZg', $link['guid']);
+        $this->assertEquals(8, $link['id']);
+        $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'), $link['created']);
+        $this->assertEquals('http://host.tld/?RttfEw', $link['guid']);
         $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']);
         $this->assertEquals('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['url']);
         $this->assertContains('Direct link', $link['description']);
         $this->assertContains('Direct link', $link['description']);
         $this->assertContains('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']);
         $this->assertContains('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']);

+ 33 - 6
tests/LinkDBTest.php

@@ -186,14 +186,15 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
         $dbSize = sizeof($testDB);
         $dbSize = sizeof($testDB);
 
 
         $link = array(
         $link = array(
+            'id' => 42,
             'title'=>'an additional link',
             'title'=>'an additional link',
             'url'=>'http://dum.my',
             'url'=>'http://dum.my',
             'description'=>'One more',
             'description'=>'One more',
             'private'=>0,
             'private'=>0,
-            'linkdate'=>'20150518_190000',
+            'created'=> DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150518_190000'),
             'tags'=>'unit test'
             'tags'=>'unit test'
         );
         );
-        $testDB[$link['linkdate']] = $link;
+        $testDB[$link['id']] = $link;
         $testDB->save('tests');
         $testDB->save('tests');
 
 
         $testDB = new LinkDB(self::$testDatastore, true, false);
         $testDB = new LinkDB(self::$testDatastore, true, false);
@@ -238,12 +239,12 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
     public function testDays()
     public function testDays()
     {
     {
         $this->assertEquals(
         $this->assertEquals(
-            array('20121206', '20130614', '20150310'),
+            array('20100310', '20121206', '20130614', '20150310'),
             self::$publicLinkDB->days()
             self::$publicLinkDB->days()
         );
         );
 
 
         $this->assertEquals(
         $this->assertEquals(
-            array('20121206', '20130614', '20141125', '20150310'),
+            array('20100310', '20121206', '20130614', '20141125', '20150310'),
             self::$privateLinkDB->days()
             self::$privateLinkDB->days()
         );
         );
     }
     }
@@ -290,10 +291,11 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
                 'stallman' => 1,
                 'stallman' => 1,
                 'free' => 1,
                 'free' => 1,
                 '-exclude' => 1,
                 '-exclude' => 1,
+                'hashtag' => 2,
                 // The DB contains a link with `sTuff` and another one with `stuff` tag.
                 // The DB contains a link with `sTuff` and another one with `stuff` tag.
-                // They need to be grouped with the first case found (`sTuff`).
+                // They need to be grouped with the first case found - order by date DESC: `sTuff`.
                 'sTuff' => 2,
                 'sTuff' => 2,
-                'hashtag' => 2,
+                'ut' => 1,
             ),
             ),
             self::$publicLinkDB->allTags()
             self::$publicLinkDB->allTags()
         );
         );
@@ -321,6 +323,7 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
                 'tag2' => 1,
                 'tag2' => 1,
                 'tag3' => 1,
                 'tag3' => 1,
                 'tag4' => 1,
                 'tag4' => 1,
+                'ut' => 1,
             ),
             ),
             self::$privateLinkDB->allTags()
             self::$privateLinkDB->allTags()
         );
         );
@@ -411,6 +414,11 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
             1,
             1,
             count(self::$publicLinkDB->filterHash($request))
             count(self::$publicLinkDB->filterHash($request))
         );
         );
+        $request = smallHash('20150310_114633' . 8);
+        $this->assertEquals(
+            1,
+            count(self::$publicLinkDB->filterHash($request))
+        );
     }
     }
 
 
     /**
     /**
@@ -433,4 +441,23 @@ class LinkDBTest extends PHPUnit_Framework_TestCase
     {
     {
         self::$publicLinkDB->filterHash('');
         self::$publicLinkDB->filterHash('');
     }
     }
+
+    /**
+     * Test reorder with asc/desc parameter.
+     */
+    public function testReorderLinksDesc()
+    {
+        self::$privateLinkDB->reorder('ASC');
+        $linkIds = array(42, 4, 1, 0, 7, 6, 8, 41);
+        $cpt = 0;
+        foreach (self::$privateLinkDB as $key => $value) {
+            $this->assertEquals($linkIds[$cpt++], $key);
+        }
+        self::$privateLinkDB->reorder('DESC');
+        $linkIds = array_reverse($linkIds);
+        $cpt = 0;
+        foreach (self::$privateLinkDB as $key => $value) {
+            $this->assertEquals($linkIds[$cpt++], $key);
+        }
+    }
 }
 }

+ 3 - 3
tests/LinkFilterTest.php

@@ -159,7 +159,7 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
 
 
         $this->assertEquals(
         $this->assertEquals(
             'MediaGoblin',
             'MediaGoblin',
-            $links['20130614_184135']['title']
+            $links[7]['title']
         );
         );
     }
     }
 
 
@@ -286,7 +286,7 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
         );
         );
 
 
         $this->assertEquals(
         $this->assertEquals(
-            6,
+            7,
             count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '-revolution'))
             count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '-revolution'))
         );
         );
     }
     }
@@ -346,7 +346,7 @@ class LinkFilterTest extends PHPUnit_Framework_TestCase
         );
         );
 
 
         $this->assertEquals(
         $this->assertEquals(
-            6,
+            7,
             count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free'))
             count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free'))
         );
         );
     }
     }

+ 3 - 3
tests/NetscapeBookmarkUtils/BookmarkExportTest.php

@@ -50,7 +50,7 @@ class BookmarkExportTest extends PHPUnit_Framework_TestCase
         $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'all', false, '');
         $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'all', false, '');
         $this->assertEquals(self::$refDb->countLinks(), sizeof($links));
         $this->assertEquals(self::$refDb->countLinks(), sizeof($links));
         foreach ($links as $link) {
         foreach ($links as $link) {
-            $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
+            $date = $link['created'];
             $this->assertEquals(
             $this->assertEquals(
                 $date->getTimestamp(),
                 $date->getTimestamp(),
                 $link['timestamp']
                 $link['timestamp']
@@ -70,7 +70,7 @@ class BookmarkExportTest extends PHPUnit_Framework_TestCase
         $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'private', false, '');
         $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'private', false, '');
         $this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links));
         $this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links));
         foreach ($links as $link) {
         foreach ($links as $link) {
-            $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
+            $date = $link['created'];
             $this->assertEquals(
             $this->assertEquals(
                 $date->getTimestamp(),
                 $date->getTimestamp(),
                 $link['timestamp']
                 $link['timestamp']
@@ -90,7 +90,7 @@ class BookmarkExportTest extends PHPUnit_Framework_TestCase
         $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public', false, '');
         $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public', false, '');
         $this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links));
         $this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links));
         foreach ($links as $link) {
         foreach ($links as $link) {
-            $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']);
+            $date = $link['created'];
             $this->assertEquals(
             $this->assertEquals(
                 $date->getTimestamp(),
                 $date->getTimestamp(),
                 $link['timestamp']
                 $link['timestamp']

+ 95 - 51
tests/NetscapeBookmarkUtils/BookmarkImportTest.php

@@ -42,6 +42,18 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
      */
      */
     protected $pagecache = 'tests';
     protected $pagecache = 'tests';
 
 
+    /**
+     * @var string Save the current timezone.
+     */
+    protected static $defaultTimeZone;
+
+    public static function setUpBeforeClass()
+    {
+        self::$defaultTimeZone = date_default_timezone_get();
+        // Timezone without DST for test consistency
+        date_default_timezone_set('Africa/Nairobi');
+    }
+
     /**
     /**
      * Resets test data before each test
      * Resets test data before each test
      */
      */
@@ -55,6 +67,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->linkDb = new LinkDB(self::$testDatastore, true, false);
         $this->linkDb = new LinkDB(self::$testDatastore, true, false);
     }
     }
 
 
+    public static function tearDownAfterClass()
+    {
+        date_default_timezone_set(self::$defaultTimeZone);
+    }
+
     /**
     /**
      * Attempt to import bookmarks from an empty file
      * Attempt to import bookmarks from an empty file
      */
      */
@@ -98,18 +115,19 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
 
 
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160618_173944',
+                'id' => 0,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160618_203944'),
                 'title' => 'Hg Init a Mercurial tutorial by Joel Spolsky',
                 'title' => 'Hg Init a Mercurial tutorial by Joel Spolsky',
                 'url' => 'http://hginit.com/',
                 'url' => 'http://hginit.com/',
                 'description' => '',
                 'description' => '',
                 'private' => 0,
                 'private' => 0,
-                'tags' => ''
+                'tags' => '',
+                'shorturl' => 'La37cg',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://hginit.com/')
             $this->linkDb->getLinkFromUrl('http://hginit.com/')
         );
         );
     }
     }
 
 
-
     /**
     /**
      * Import bookmarks nested in a folder hierarchy
      * Import bookmarks nested in a folder hierarchy
      */
      */
@@ -126,89 +144,105 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
 
 
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160225_205541',
+                'id' => 0,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235541'),
                 'title' => 'Nested 1',
                 'title' => 'Nested 1',
                 'url' => 'http://nest.ed/1',
                 'url' => 'http://nest.ed/1',
                 'description' => '',
                 'description' => '',
                 'private' => 0,
                 'private' => 0,
-                'tags' => 'tag1 tag2'
+                'tags' => 'tag1 tag2',
+                'shorturl' => 'KyDNKA',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://nest.ed/1')
             $this->linkDb->getLinkFromUrl('http://nest.ed/1')
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160225_205542',
+                'id' => 1,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235542'),
                 'title' => 'Nested 1-1',
                 'title' => 'Nested 1-1',
                 'url' => 'http://nest.ed/1-1',
                 'url' => 'http://nest.ed/1-1',
                 'description' => '',
                 'description' => '',
                 'private' => 0,
                 'private' => 0,
-                'tags' => 'folder1 tag1 tag2'
+                'tags' => 'folder1 tag1 tag2',
+                'shorturl' => 'T2LnXg',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://nest.ed/1-1')
             $this->linkDb->getLinkFromUrl('http://nest.ed/1-1')
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160225_205547',
+                'id' => 2,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235547'),
                 'title' => 'Nested 1-2',
                 'title' => 'Nested 1-2',
                 'url' => 'http://nest.ed/1-2',
                 'url' => 'http://nest.ed/1-2',
                 'description' => '',
                 'description' => '',
                 'private' => 0,
                 'private' => 0,
-                'tags' => 'folder1 tag3 tag4'
+                'tags' => 'folder1 tag3 tag4',
+                'shorturl' => '46SZxA',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://nest.ed/1-2')
             $this->linkDb->getLinkFromUrl('http://nest.ed/1-2')
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160202_172222',
+                'id' => 3,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160202_202222'),
                 'title' => 'Nested 2-1',
                 'title' => 'Nested 2-1',
                 'url' => 'http://nest.ed/2-1',
                 'url' => 'http://nest.ed/2-1',
                 'description' => 'First link of the second section',
                 'description' => 'First link of the second section',
                 'private' => 1,
                 'private' => 1,
-                'tags' => 'folder2'
+                'tags' => 'folder2',
+                'shorturl' => '4UHOSw',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://nest.ed/2-1')
             $this->linkDb->getLinkFromUrl('http://nest.ed/2-1')
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160119_200227',
+                'id' => 4,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160119_230227'),
                 'title' => 'Nested 2-2',
                 'title' => 'Nested 2-2',
                 'url' => 'http://nest.ed/2-2',
                 'url' => 'http://nest.ed/2-2',
                 'description' => 'Second link of the second section',
                 'description' => 'Second link of the second section',
                 'private' => 1,
                 'private' => 1,
-                'tags' => 'folder2'
+                'tags' => 'folder2',
+                'shorturl' => 'yfzwbw',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://nest.ed/2-2')
             $this->linkDb->getLinkFromUrl('http://nest.ed/2-2')
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160202_172223',
+                'id' => 5,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160202_202222'),
                 'title' => 'Nested 3-1',
                 'title' => 'Nested 3-1',
                 'url' => 'http://nest.ed/3-1',
                 'url' => 'http://nest.ed/3-1',
                 'description' => '',
                 'description' => '',
                 'private' => 0,
                 'private' => 0,
-                'tags' => 'folder3 folder3-1 tag3'
+                'tags' => 'folder3 folder3-1 tag3',
+                'shorturl' => 'UwxIUQ',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://nest.ed/3-1')
             $this->linkDb->getLinkFromUrl('http://nest.ed/3-1')
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160119_200228',
+                'id' => 6,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160119_230227'),
                 'title' => 'Nested 3-2',
                 'title' => 'Nested 3-2',
                 'url' => 'http://nest.ed/3-2',
                 'url' => 'http://nest.ed/3-2',
                 'description' => '',
                 'description' => '',
                 'private' => 0,
                 'private' => 0,
-                'tags' => 'folder3 folder3-1'
+                'tags' => 'folder3 folder3-1',
+                'shorturl' => 'p8dyZg',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://nest.ed/3-2')
             $this->linkDb->getLinkFromUrl('http://nest.ed/3-2')
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160229_081541',
+                'id' => 7,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160229_111541'),
                 'title' => 'Nested 2',
                 'title' => 'Nested 2',
                 'url' => 'http://nest.ed/2',
                 'url' => 'http://nest.ed/2',
                 'description' => '',
                 'description' => '',
                 'private' => 0,
                 'private' => 0,
-                'tags' => 'tag4'
+                'tags' => 'tag4',
+                'shorturl' => 'Gt3Uug',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://nest.ed/2')
             $this->linkDb->getLinkFromUrl('http://nest.ed/2')
         );
         );
@@ -227,28 +261,34 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             .' 2 links imported, 0 links overwritten, 0 links skipped.',
             NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache)
             NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache)
         );
         );
+
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(2, count($this->linkDb));
         $this->assertEquals(1, count_private($this->linkDb));
         $this->assertEquals(1, count_private($this->linkDb));
 
 
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20001010_105536',
+                'id' => 0,
+                // Old link - UTC+4 (note that TZ in the import file is ignored).
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20001010_135536'),
                 'title' => 'Secret stuff',
                 'title' => 'Secret stuff',
                 'url' => 'https://private.tld',
                 'url' => 'https://private.tld',
                 'description' => "Super-secret stuff you're not supposed to know about",
                 'description' => "Super-secret stuff you're not supposed to know about",
                 'private' => 1,
                 'private' => 1,
-                'tags' => 'private secret'
+                'tags' => 'private secret',
+                'shorturl' => 'EokDtA',
             ),
             ),
             $this->linkDb->getLinkFromUrl('https://private.tld')
             $this->linkDb->getLinkFromUrl('https://private.tld')
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160225_205548',
+                'id' => 1,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'),
                 'title' => 'Public stuff',
                 'title' => 'Public stuff',
                 'url' => 'http://public.tld',
                 'url' => 'http://public.tld',
                 'description' => '',
                 'description' => '',
                 'private' => 0,
                 'private' => 0,
-                'tags' => 'public hello world'
+                'tags' => 'public hello world',
+                'shorturl' => 'Er9ddA',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://public.tld')
             $this->linkDb->getLinkFromUrl('http://public.tld')
         );
         );
@@ -271,23 +311,28 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
 
 
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20001010_105536',
+                'id' => 0,
+                // Note that TZ in the import file is ignored.
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20001010_135536'),
                 'title' => 'Secret stuff',
                 'title' => 'Secret stuff',
                 'url' => 'https://private.tld',
                 'url' => 'https://private.tld',
                 'description' => "Super-secret stuff you're not supposed to know about",
                 'description' => "Super-secret stuff you're not supposed to know about",
                 'private' => 1,
                 'private' => 1,
-                'tags' => 'private secret'
+                'tags' => 'private secret',
+                'shorturl' => 'EokDtA',
             ),
             ),
             $this->linkDb->getLinkFromUrl('https://private.tld')
             $this->linkDb->getLinkFromUrl('https://private.tld')
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             array(
             array(
-                'linkdate' => '20160225_205548',
+                'id' => 1,
+                'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'),
                 'title' => 'Public stuff',
                 'title' => 'Public stuff',
                 'url' => 'http://public.tld',
                 'url' => 'http://public.tld',
                 'description' => '',
                 'description' => '',
                 'private' => 0,
                 'private' => 0,
-                'tags' => 'public hello world'
+                'tags' => 'public hello world',
+                'shorturl' => 'Er9ddA',
             ),
             ),
             $this->linkDb->getLinkFromUrl('http://public.tld')
             $this->linkDb->getLinkFromUrl('http://public.tld')
         );
         );
@@ -309,11 +354,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(
         $this->assertEquals(
             0,
             0,
-            $this->linkDb['20001010_105536']['private']
+            $this->linkDb[0]['private']
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             0,
             0,
-            $this->linkDb['20160225_205548']['private']
+            $this->linkDb[1]['private']
         );
         );
     }
     }
 
 
@@ -333,11 +378,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(2, count_private($this->linkDb));
         $this->assertEquals(2, count_private($this->linkDb));
         $this->assertEquals(
         $this->assertEquals(
             1,
             1,
-            $this->linkDb['20001010_105536']['private']
+            $this->linkDb['0']['private']
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             1,
             1,
-            $this->linkDb['20160225_205548']['private']
+            $this->linkDb['1']['private']
         );
         );
     }
     }
 
 
@@ -359,13 +404,12 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(2, count_private($this->linkDb));
         $this->assertEquals(2, count_private($this->linkDb));
         $this->assertEquals(
         $this->assertEquals(
             1,
             1,
-            $this->linkDb['20001010_105536']['private']
+            $this->linkDb[0]['private']
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             1,
             1,
-            $this->linkDb['20160225_205548']['private']
+            $this->linkDb[1]['private']
         );
         );
-
         // re-import as public, enable overwriting
         // re-import as public, enable overwriting
         $post = array(
         $post = array(
             'privacy' => 'public',
             'privacy' => 'public',
@@ -380,11 +424,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(
         $this->assertEquals(
             0,
             0,
-            $this->linkDb['20001010_105536']['private']
+            $this->linkDb[0]['private']
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             0,
             0,
-            $this->linkDb['20160225_205548']['private']
+            $this->linkDb[1]['private']
         );
         );
     }
     }
 
 
@@ -406,11 +450,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(
         $this->assertEquals(
             0,
             0,
-            $this->linkDb['20001010_105536']['private']
+            $this->linkDb['0']['private']
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             0,
             0,
-            $this->linkDb['20160225_205548']['private']
+            $this->linkDb['1']['private']
         );
         );
 
 
         // re-import as private, enable overwriting
         // re-import as private, enable overwriting
@@ -427,11 +471,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(2, count_private($this->linkDb));
         $this->assertEquals(2, count_private($this->linkDb));
         $this->assertEquals(
         $this->assertEquals(
             1,
             1,
-            $this->linkDb['20001010_105536']['private']
+            $this->linkDb['0']['private']
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             1,
             1,
-            $this->linkDb['20160225_205548']['private']
+            $this->linkDb['1']['private']
         );
         );
     }
     }
 
 
@@ -480,11 +524,11 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(
         $this->assertEquals(
             'tag1 tag2 tag3 private secret',
             'tag1 tag2 tag3 private secret',
-            $this->linkDb['20001010_105536']['tags']
+            $this->linkDb['0']['tags']
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             'tag1 tag2 tag3 public hello world',
             'tag1 tag2 tag3 public hello world',
-            $this->linkDb['20160225_205548']['tags']
+            $this->linkDb['1']['tags']
         );
         );
     }
     }
 
 
@@ -507,16 +551,16 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(
         $this->assertEquals(
             'tag1&amp; tag2 &quot;tag3&quot; private secret',
             'tag1&amp; tag2 &quot;tag3&quot; private secret',
-            $this->linkDb['20001010_105536']['tags']
+            $this->linkDb['0']['tags']
         );
         );
         $this->assertEquals(
         $this->assertEquals(
             'tag1&amp; tag2 &quot;tag3&quot; public hello world',
             'tag1&amp; tag2 &quot;tag3&quot; public hello world',
-            $this->linkDb['20160225_205548']['tags']
+            $this->linkDb['1']['tags']
         );
         );
     }
     }
 
 
     /**
     /**
-     * Ensure each imported bookmark has a unique linkdate
+     * Ensure each imported bookmark has a unique id
      *
      *
      * See https://github.com/shaarli/Shaarli/issues/351
      * See https://github.com/shaarli/Shaarli/issues/351
      */
      */
@@ -531,16 +575,16 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase
         $this->assertEquals(3, count($this->linkDb));
         $this->assertEquals(3, count($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(0, count_private($this->linkDb));
         $this->assertEquals(
         $this->assertEquals(
-            '20160225_205548',
-            $this->linkDb['20160225_205548']['linkdate']
+            0,
+            $this->linkDb[0]['id']
         );
         );
         $this->assertEquals(
         $this->assertEquals(
-            '20160225_205549',
-            $this->linkDb['20160225_205549']['linkdate']
+            1,
+            $this->linkDb[1]['id']
         );
         );
         $this->assertEquals(
         $this->assertEquals(
-            '20160225_205550',
-            $this->linkDb['20160225_205550']['linkdate']
+            2,
+            $this->linkDb[2]['id']
         );
         );
     }
     }
 }
 }

+ 98 - 0
tests/Updater/UpdaterTest.php

@@ -214,6 +214,7 @@ $GLOBALS[\'privateLinkByDefault\'] = true;';
         $refDB = new ReferenceLinkDB();
         $refDB = new ReferenceLinkDB();
         $refDB->write(self::$testDatastore);
         $refDB->write(self::$testDatastore);
         $linkDB = new LinkDB(self::$testDatastore, true, false);
         $linkDB = new LinkDB(self::$testDatastore, true, false);
+
         $this->assertEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
         $this->assertEmpty($linkDB->filterSearch(array('searchtags' => 'exclude')));
         $updater = new Updater(array(), $linkDB, $this->conf, true);
         $updater = new Updater(array(), $linkDB, $this->conf, true);
         $updater->updateMethodRenameDashTags();
         $updater->updateMethodRenameDashTags();
@@ -287,4 +288,101 @@ $GLOBALS[\'privateLinkByDefault\'] = true;';
         $this->assertEquals(escape($redirectorUrl), $this->conf->get('redirector.url'));
         $this->assertEquals(escape($redirectorUrl), $this->conf->get('redirector.url'));
         unlink($sandbox .'.json.php');
         unlink($sandbox .'.json.php');
     }
     }
+
+    /**
+     * Test updateMethodDatastoreIds().
+     */
+    public function testDatastoreIds()
+    {
+        $links = array(
+            '20121206_182539' => array(
+                'linkdate' => '20121206_182539',
+                'title' => 'Geek and Poke',
+                'url' => 'http://geek-and-poke.com/',
+                'description' => 'desc',
+                'tags' => 'dev cartoon tag1  tag2   tag3  tag4   ',
+                'updated' => '20121206_190301',
+                'private' => false,
+            ),
+            '20121206_172539' => array(
+                'linkdate' => '20121206_172539',
+                'title' => 'UserFriendly - Samba',
+                'url' => 'http://ars.userfriendly.org/cartoons/?id=20010306',
+                'description' => '',
+                'tags' => 'samba cartoon web',
+                'private' => false,
+            ),
+            '20121206_142300' => array(
+                'linkdate' => '20121206_142300',
+                'title' => 'UserFriendly - Web Designer',
+                'url' => 'http://ars.userfriendly.org/cartoons/?id=20121206',
+                'description' => 'Naming conventions... #private',
+                'tags' => 'samba cartoon web',
+                'private' => true,
+            ),
+        );
+        $refDB = new ReferenceLinkDB();
+        $refDB->setLinks($links);
+        $refDB->write(self::$testDatastore);
+        $linkDB = new LinkDB(self::$testDatastore, true, false);
+
+        $checksum = hash_file('sha1', self::$testDatastore);
+
+        $this->conf->set('resource.data_dir', 'sandbox');
+        $this->conf->set('resource.datastore', self::$testDatastore);
+
+        $updater = new Updater(array(), $linkDB, $this->conf, true);
+        $this->assertTrue($updater->updateMethodDatastoreIds());
+
+        $linkDB = new LinkDB(self::$testDatastore, true, false);
+
+        $backup = glob($this->conf->get('resource.data_dir') . '/datastore.'. date('YmdH') .'*.php');
+        $backup = $backup[0];
+
+        $this->assertFileExists($backup);
+        $this->assertEquals($checksum, hash_file('sha1', $backup));
+        unlink($backup);
+
+        $this->assertEquals(3, count($linkDB));
+        $this->assertTrue(isset($linkDB[0]));
+        $this->assertFalse(isset($linkDB[0]['linkdate']));
+        $this->assertEquals(0, $linkDB[0]['id']);
+        $this->assertEquals('UserFriendly - Web Designer', $linkDB[0]['title']);
+        $this->assertEquals('http://ars.userfriendly.org/cartoons/?id=20121206', $linkDB[0]['url']);
+        $this->assertEquals('Naming conventions... #private', $linkDB[0]['description']);
+        $this->assertEquals('samba cartoon web', $linkDB[0]['tags']);
+        $this->assertTrue($linkDB[0]['private']);
+        $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_142300'), $linkDB[0]['created']);
+
+        $this->assertTrue(isset($linkDB[1]));
+        $this->assertFalse(isset($linkDB[1]['linkdate']));
+        $this->assertEquals(1, $linkDB[1]['id']);
+        $this->assertEquals('UserFriendly - Samba', $linkDB[1]['title']);
+        $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_172539'), $linkDB[1]['created']);
+
+        $this->assertTrue(isset($linkDB[2]));
+        $this->assertFalse(isset($linkDB[2]['linkdate']));
+        $this->assertEquals(2, $linkDB[2]['id']);
+        $this->assertEquals('Geek and Poke', $linkDB[2]['title']);
+        $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_182539'), $linkDB[2]['created']);
+        $this->assertEquals(DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_190301'), $linkDB[2]['updated']);
+    }
+
+    /**
+     * Test updateMethodDatastoreIds() with the update already applied: nothing to do.
+     */
+    public function testDatastoreIdsNothingToDo()
+    {
+        $refDB = new ReferenceLinkDB();
+        $refDB->write(self::$testDatastore);
+        $linkDB = new LinkDB(self::$testDatastore, true, false);
+
+        $this->conf->set('resource.data_dir', 'sandbox');
+        $this->conf->set('resource.datastore', self::$testDatastore);
+
+        $checksum = hash_file('sha1', self::$testDatastore);
+        $updater = new Updater(array(), $linkDB, $this->conf, true);
+        $this->assertTrue($updater->updateMethodDatastoreIds());
+        $this->assertEquals($checksum, hash_file('sha1', self::$testDatastore));
+    }
 }
 }

+ 20 - 5
tests/plugins/PluginIssoTest.php

@@ -47,12 +47,14 @@ class PluginIssoTest extends PHPUnit_Framework_TestCase
         $conf->set('plugins.ISSO_SERVER', 'value');
         $conf->set('plugins.ISSO_SERVER', 'value');
 
 
         $str = 'http://randomstr.com/test';
         $str = 'http://randomstr.com/test';
+        $date = '20161118_100001';
         $data = array(
         $data = array(
             'title' => $str,
             'title' => $str,
             'links' => array(
             'links' => array(
                 array(
                 array(
+                    'id' => 12,
                     'url' => $str,
                     'url' => $str,
-                    'linkdate' => 'abc',
+                    'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date),
                 )
                 )
             )
             )
         );
         );
@@ -65,7 +67,14 @@ class PluginIssoTest extends PHPUnit_Framework_TestCase
 
 
         // plugin data
         // plugin data
         $this->assertEquals(1, count($data['plugin_end_zone']));
         $this->assertEquals(1, count($data['plugin_end_zone']));
-        $this->assertNotFalse(strpos($data['plugin_end_zone'][0], 'abc'));
+        $this->assertNotFalse(strpos(
+            $data['plugin_end_zone'][0],
+            'data-isso-id="'. $data['links'][0]['id'] .'"'
+        ));
+        $this->assertNotFalse(strpos(
+            $data['plugin_end_zone'][0],
+            'data-title="'. $data['links'][0]['id'] .'"'
+        ));
         $this->assertNotFalse(strpos($data['plugin_end_zone'][0], 'embed.min.js'));
         $this->assertNotFalse(strpos($data['plugin_end_zone'][0], 'embed.min.js'));
     }
     }
 
 
@@ -78,16 +87,20 @@ class PluginIssoTest extends PHPUnit_Framework_TestCase
         $conf->set('plugins.ISSO_SERVER', 'value');
         $conf->set('plugins.ISSO_SERVER', 'value');
 
 
         $str = 'http://randomstr.com/test';
         $str = 'http://randomstr.com/test';
+        $date1 = '20161118_100001';
+        $date2 = '20161118_100002';
         $data = array(
         $data = array(
             'title' => $str,
             'title' => $str,
             'links' => array(
             'links' => array(
                 array(
                 array(
+                    'id' => 12,
                     'url' => $str,
                     'url' => $str,
-                    'linkdate' => 'abc',
+                    'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date1),
                 ),
                 ),
                 array(
                 array(
+                    'id' => 13,
                     'url' => $str . '2',
                     'url' => $str . '2',
-                    'linkdate' => 'abc2',
+                    'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date2),
                 ),
                 ),
             )
             )
         );
         );
@@ -106,12 +119,14 @@ class PluginIssoTest extends PHPUnit_Framework_TestCase
         $conf->set('plugins.ISSO_SERVER', 'value');
         $conf->set('plugins.ISSO_SERVER', 'value');
 
 
         $str = 'http://randomstr.com/test';
         $str = 'http://randomstr.com/test';
+        $date = '20161118_100001';
         $data = array(
         $data = array(
             'title' => $str,
             'title' => $str,
             'links' => array(
             'links' => array(
                 array(
                 array(
+                    'id' => 12,
                     'url' => $str,
                     'url' => $str,
-                    'linkdate' => 'abc',
+                    'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date),
                 )
                 )
             ),
             ),
             'search_term' => $str
             'search_term' => $str

+ 47 - 14
tests/utils/ReferenceLinkDB.php

@@ -4,7 +4,7 @@
  */
  */
 class ReferenceLinkDB
 class ReferenceLinkDB
 {
 {
-    public static $NB_LINKS_TOTAL = 7;
+    public static $NB_LINKS_TOTAL = 8;
 
 
     private $_links = array();
     private $_links = array();
     private $_publicCount = 0;
     private $_publicCount = 0;
@@ -16,66 +16,87 @@ class ReferenceLinkDB
     public function __construct()
     public function __construct()
     {
     {
         $this->addLink(
         $this->addLink(
+            41,
             'Link title: @website',
             'Link title: @website',
             '?WDWyig',
             '?WDWyig',
             'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag',
             'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag',
             0,
             0,
-            '20150310_114651',
-            'sTuff'
+            DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'),
+            'sTuff',
+            null,
+            'WDWyig'
         );
         );
 
 
         $this->addLink(
         $this->addLink(
+            42,
+            'Note: I have a big ID but an old date',
+            '?WDWyig',
+            'Used to test links reordering.',
+            0,
+            DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20100310_101010'),
+            'ut'
+        );
+
+        $this->addLink(
+            8,
             'Free as in Freedom 2.0 @website',
             'Free as in Freedom 2.0 @website',
             'https://static.fsf.org/nosvn/faif-2.0.pdf',
             'https://static.fsf.org/nosvn/faif-2.0.pdf',
             'Richard Stallman and the Free Software Revolution. Read this. #hashtag',
             'Richard Stallman and the Free Software Revolution. Read this. #hashtag',
             0,
             0,
-            '20150310_114633',
+            DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'),
             'free gnu software stallman -exclude stuff hashtag',
             'free gnu software stallman -exclude stuff hashtag',
-            '20160803_093033'
+            DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160803_093033')
         );
         );
 
 
         $this->addLink(
         $this->addLink(
+            7,
             'MediaGoblin',
             'MediaGoblin',
             'http://mediagoblin.org/',
             'http://mediagoblin.org/',
             'A free software media publishing platform #hashtagOther',
             'A free software media publishing platform #hashtagOther',
             0,
             0,
-            '20130614_184135',
-            'gnu media web .hidden hashtag'
+            DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20130614_184135'),
+            'gnu media web .hidden hashtag',
+            null,
+            'IuWvgA'
         );
         );
 
 
         $this->addLink(
         $this->addLink(
+            6,
             'w3c-markup-validator',
             'w3c-markup-validator',
             'https://dvcs.w3.org/hg/markup-validator/summary',
             'https://dvcs.w3.org/hg/markup-validator/summary',
             'Mercurial repository for the W3C Validator #private',
             'Mercurial repository for the W3C Validator #private',
             1,
             1,
-            '20141125_084734',
+            DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20141125_084734'),
             'css html w3c web Mercurial'
             'css html w3c web Mercurial'
         );
         );
 
 
         $this->addLink(
         $this->addLink(
+            4,
             'UserFriendly - Web Designer',
             'UserFriendly - Web Designer',
             'http://ars.userfriendly.org/cartoons/?id=20121206',
             'http://ars.userfriendly.org/cartoons/?id=20121206',
             'Naming conventions... #private',
             'Naming conventions... #private',
             0,
             0,
-            '20121206_142300',
+            DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_142300'),
             'dev cartoon web'
             'dev cartoon web'
         );
         );
 
 
         $this->addLink(
         $this->addLink(
+            1,
             'UserFriendly - Samba',
             'UserFriendly - Samba',
             'http://ars.userfriendly.org/cartoons/?id=20010306',
             'http://ars.userfriendly.org/cartoons/?id=20010306',
             'Tropical printing',
             'Tropical printing',
             0,
             0,
-            '20121206_172539',
+            DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_172539'),
             'samba cartoon web'
             'samba cartoon web'
         );
         );
 
 
         $this->addLink(
         $this->addLink(
+            0,
             'Geek and Poke',
             'Geek and Poke',
             'http://geek-and-poke.com/',
             'http://geek-and-poke.com/',
             '',
             '',
             1,
             1,
-            '20121206_182539',
+            DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_182539'),
             'dev cartoon tag1  tag2   tag3  tag4   '
             'dev cartoon tag1  tag2   tag3  tag4   '
         );
         );
     }
     }
@@ -83,18 +104,20 @@ class ReferenceLinkDB
     /**
     /**
      * Adds a new link
      * Adds a new link
      */
      */
-    protected function addLink($title, $url, $description, $private, $date, $tags, $updated = '')
+    protected function addLink($id, $title, $url, $description, $private, $date, $tags, $updated = '', $shorturl = '')
     {
     {
         $link = array(
         $link = array(
+            'id' => $id,
             'title' => $title,
             'title' => $title,
             'url' => $url,
             'url' => $url,
             'description' => $description,
             'description' => $description,
             'private' => $private,
             'private' => $private,
-            'linkdate' => $date,
             'tags' => $tags,
             'tags' => $tags,
+            'created' => $date,
             'updated' => $updated,
             'updated' => $updated,
+            'shorturl' => $shorturl ? $shorturl : smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id),
         );
         );
-        $this->_links[$date] = $link;
+        $this->_links[$id] = $link;
 
 
         if ($private) {
         if ($private) {
             $this->_privateCount++;
             $this->_privateCount++;
@@ -142,4 +165,14 @@ class ReferenceLinkDB
     {
     {
         return $this->_links;
         return $this->_links;
     }
     }
+
+    /**
+     * Setter to override link creation.
+     *
+     * @param array $links List of links.
+     */
+    public function setLinks($links)
+    {
+        $this->_links = $links;
+    }
 }
 }

+ 2 - 2
tpl/daily.html

@@ -49,13 +49,13 @@
                     {$link=$value}
                     {$link=$value}
                     <div class="dailyEntry">
                     <div class="dailyEntry">
                         <div class="dailyEntryPermalink">
                         <div class="dailyEntryPermalink">
-                            <a href="?{$link.linkdate|smallHash}">
+                            <a href="?{$value.shorturl}">
                                 <img src="../images/squiggle2.png" width="25" height="26" title="permalink" alt="permalink">
                                 <img src="../images/squiggle2.png" width="25" height="26" title="permalink" alt="permalink">
                             </a>
                             </a>
                         </div>
                         </div>
                         {if="!$hide_timestamps || isLoggedIn()"}
                         {if="!$hide_timestamps || isLoggedIn()"}
                             <div class="dailyEntryLinkdate">
                             <div class="dailyEntryLinkdate">
-                                <a href="?{$link.linkdate|smallHash}">{function="strftime('%c', $link.timestamp)"}</a>
+                                <a href="?{$value.shorturl}">{function="strftime('%c', $link.timestamp)"}</a>
                             </div>
                             </div>
                         {/if}
                         {/if}
                         {if="$link.tags"}
                         {if="$link.tags"}

+ 3 - 0
tpl/editlink.html

@@ -16,6 +16,9 @@
     <div id="editlinkform">
     <div id="editlinkform">
         <form method="post" name="linkform">
         <form method="post" name="linkform">
             <input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
             <input type="hidden" name="lf_linkdate" value="{$link.linkdate}">
+          {if="isset($link.id)"}
+	          <input type="hidden" name="lf_id" value="{$link.id}">
+          {/if}
             <label for="lf_url"><i>URL</i></label><br><input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input"><br>
             <label for="lf_url"><i>URL</i></label><br><input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input"><br>
             <label for="lf_title"><i>Title</i></label><br><input type="text" name="lf_title" id="lf_title" value="{$link.title}" class="lf_input"><br>
             <label for="lf_title"><i>Title</i></label><br><input type="text" name="lf_title" id="lf_title" value="{$link.title}" class="lf_input"><br>
             <label for="lf_description"><i>Description</i></label><br><textarea name="lf_description" id="lf_description" rows="4" cols="25">{$link.description}</textarea><br>
             <label for="lf_description"><i>Description</i></label><br><textarea name="lf_description" id="lf_description" rows="4" cols="25">{$link.description}</textarea><br>

+ 3 - 3
tpl/linklist.html

@@ -81,11 +81,11 @@
                 {if="isLoggedIn()"}
                 {if="isLoggedIn()"}
                     <div class="linkeditbuttons">
                     <div class="linkeditbuttons">
                         <form method="GET" class="buttoneditform">
                         <form method="GET" class="buttoneditform">
-                            <input type="hidden" name="edit_link" value="{$value.linkdate}">
+                            <input type="hidden" name="edit_link" value="{$value.id}">
                             <input type="image" alt="Edit" src="images/edit_icon.png#" title="Edit" class="button_edit">
                             <input type="image" alt="Edit" src="images/edit_icon.png#" title="Edit" class="button_edit">
                         </form><br>
                         </form><br>
                         <form method="POST" class="buttoneditform">
                         <form method="POST" class="buttoneditform">
-                            <input type="hidden" name="lf_linkdate" value="{$value.linkdate}">
+                            <input type="hidden" name="lf_linkdate" value="{$value.id}">
                             <input type="hidden" name="token" value="{$token}">
                             <input type="hidden" name="token" value="{$token}">
                             <input type="hidden" name="delete_link">
                             <input type="hidden" name="delete_link">
                             <input type="image" alt="Delete" src="images/delete_icon.png#" title="Delete"
                             <input type="image" alt="Delete" src="images/delete_icon.png#" title="Delete"
@@ -101,7 +101,7 @@
                 {if="!$hide_timestamps || isLoggedIn()"}
                 {if="!$hide_timestamps || isLoggedIn()"}
                     {$updated=$value.updated_timestamp ? 'Edited: '. strftime('%c', $value.updated_timestamp) : 'Permalink'}
                     {$updated=$value.updated_timestamp ? 'Edited: '. strftime('%c', $value.updated_timestamp) : 'Permalink'}
                     <span class="linkdate" title="Permalink">
                     <span class="linkdate" title="Permalink">
-                        <a href="?{$value.linkdate|smallHash}">
+                        <a href="?{$value.shorturl}">
                             <span title="{$updated}">
                             <span title="{$updated}">
                                 {function="strftime('%c', $value.timestamp)"}
                                 {function="strftime('%c', $value.timestamp)"}
                                 {if="$value.updated_timestamp"}*{/if}
                                 {if="$value.updated_timestamp"}*{/if}