diff --git a/application/FeedBuilder.php b/application/FeedBuilder.php index 4036a7cc..fedd90e6 100644 --- a/application/FeedBuilder.php +++ b/application/FeedBuilder.php @@ -143,7 +143,7 @@ public function buildData() */ 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. if ($link['url'][0] === '?' && strlen($link['url']) === 7) { $link['url'] = $pageaddr . $link['url']; @@ -156,12 +156,12 @@ protected function buildItem($link, $pageaddr) $link['description'] = format_description($link['description'], '', $pageaddr); $link['description'] .= PHP_EOL .'— '. $permalink; - $pubDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); + $pubDate = $link['created']; $link['pub_iso_date'] = $this->getIsoDate($pubDate); // atom:entry elements MUST contain exactly one atom:updated element. 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); } else { $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);; diff --git a/application/LinkDB.php b/application/LinkDB.php index c8b162b6..1e13286a 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php @@ -6,15 +6,15 @@ * * Example: * $myLinks = new LinkDB(); - * echo $myLinks['20110826_161819']['title']; + * echo $myLinks[350]['title']; * foreach ($myLinks as $link) * echo $link['title'].' at url '.$link['url'].'; description:'.$link['description']; * * Available keys: + * - id: primary key, incremental integer identifier (persistent) * - 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 * - tags: tags attached to this entry (separated by spaces) * - title Title of the link @@ -22,11 +22,25 @@ * Can be absolute or relative. * Relative URLs are permalinks (e.g.'?m-ukcw') * - real_url Absolute processed URL. + * - shorturl Permalink smallhash * * Implements 3 interfaces: * - ArrayAccess: behaves like an associative array; * - Countable: there is a count() method; * - 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 { @@ -47,11 +61,17 @@ class LinkDB implements Iterator, Countable, ArrayAccess // - value: associative array (keys: title, description...) 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; - // 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; // Position in the $this->keys array (for the Iterator interface) @@ -121,14 +141,26 @@ public function offsetSet($offset, $value) if (!$this->loggedIn) { 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->urls[$value['url']]=$offset; + $this->urls[$value['url']] = $offset; + $this->ids[$value['id']] = $offset; } /** @@ -136,7 +168,7 @@ public function offsetSet($offset, $value) */ public function offsetExists($offset) { - return array_key_exists($offset, $this->links); + return array_key_exists($this->getLinkOffset($offset), $this->links); } /** @@ -148,9 +180,11 @@ public function offsetUnset($offset) // TODO: raise an exception 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->links[$offset]); + unset($this->ids[$realOffset]); + unset($this->links[$realOffset]); } /** @@ -158,7 +192,8 @@ public function offsetUnset($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 @@ public function offsetGet($offset) */ public function current() { - return $this->links[$this->keys[$this->position]]; + return $this[$this->keys[$this->position]]; } /** @@ -192,8 +227,7 @@ public function next() */ public function rewind() { - $this->keys = array_keys($this->links); - rsort($this->keys); + $this->keys = array_keys($this->ids); $this->position = 0; } @@ -219,6 +253,7 @@ private function check() // Create a dummy database for example $this->links = array(); $link = array( + 'id' => 1, 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', 'url'=>'https://github.com/shaarli/Shaarli/wiki', 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. @@ -227,20 +262,23 @@ private function check() You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', 'private'=>0, - 'linkdate'=> date('Ymd_His'), + 'created'=> new DateTime(), 'tags'=>'opensource software' ); - $this->links[$link['linkdate']] = $link; + $link['shorturl'] = link_small_hash($link['created'], $link['id']); + $this->links[1] = $link; $link = array( + 'id' => 0, 'title'=>'My secret stuff... - Pastebin.com', 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', '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 $this->write(); @@ -251,7 +289,6 @@ private function check() */ private function read() { - // Public links are hidden and user not logged in => nothing to show if ($this->hidePublicLinks && !$this->loggedIn) { $this->links = array(); @@ -269,23 +306,13 @@ private function read() 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']; - } + $toremove = array(); + foreach ($this->links as $key => &$link) { + if (! $this->loggedIn && $link['private'] != 0) { + // Transition for not upgraded databases. + $toremove[] = $key; + continue; } - foreach ($toremove as $linkdate) { - unset($this->links[$linkdate]); - } - } - - $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. sanitizeLink($link); @@ -307,7 +334,24 @@ private function read() else { $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 @@ public function filterSearch($filterRequest = array(), $casesensitive = false, $ $request = ''; } - $linkFilter = new LinkFilter($this->links); + $linkFilter = new LinkFilter($this); return $linkFilter->filter($type, $request, $casesensitive, $privateonly); } @@ -467,12 +511,64 @@ public function allTags() public function days() { $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); sort($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; + } } diff --git a/application/LinkFilter.php b/application/LinkFilter.php index d4fe28df..daa6d9cc 100644 --- a/application/LinkFilter.php +++ b/application/LinkFilter.php @@ -33,12 +33,12 @@ class LinkFilter public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}'; /** - * @var array all available links. + * @var LinkDB all available links. */ private $links; /** - * @param array $links initialization. + * @param LinkDB $links initialization. */ public function __construct($links) { @@ -94,18 +94,16 @@ public function filter($type, $request, $casesensitive = false, $privateonly = f private function noFilter($privateonly = false) { if (! $privateonly) { - krsort($this->links); return $this->links; } $out = array(); - foreach ($this->links as $value) { + foreach ($this->links as $key => $value) { if ($value['private']) { - $out[$value['linkdate']] = $value; + $out[$key] = $value; } } - krsort($out); return $out; } @@ -121,10 +119,10 @@ private function noFilter($privateonly = false) private function filterSmallHash($smallHash) { $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 - $filtered[$l['linkdate']] = $l; + $filtered[$key] = $l; return $filtered; } } @@ -188,7 +186,7 @@ private function filterFulltext($searchterms, $privateonly = false) $keys = array('title', 'description', 'url', 'tags'); // Iterate over every stored link. - foreach ($this->links as $link) { + foreach ($this->links as $id => $link) { // ignore non private links when 'privatonly' is on. if (! $link['private'] && $privateonly === true) { @@ -222,11 +220,10 @@ private function filterFulltext($searchterms, $privateonly = false) } if ($found) { - $filtered[$link['linkdate']] = $link; + $filtered[$id] = $link; } } - krsort($filtered); return $filtered; } @@ -256,7 +253,7 @@ public function filterTags($tags, $casesensitive = false, $privateonly = false) return $filtered; } - foreach ($this->links as $link) { + foreach ($this->links as $key => $link) { // ignore non private links when 'privatonly' is on. if (! $link['private'] && $privateonly === true) { continue; @@ -278,10 +275,9 @@ public function filterTags($tags, $casesensitive = false, $privateonly = false) } if ($found) { - $filtered[$link['linkdate']] = $link; + $filtered[$key] = $link; } } - krsort($filtered); return $filtered; } @@ -304,13 +300,14 @@ public function filterDay($day) } $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); } /** diff --git a/application/LinkUtils.php b/application/LinkUtils.php index 9d9ae3cb..cf58f808 100644 --- a/application/LinkUtils.php +++ b/application/LinkUtils.php @@ -169,3 +169,16 @@ function space2nbsp($text) function format_description($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); +} diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php index dd21f05b..e7148d00 100644 --- a/application/NetscapeBookmarkUtils.php +++ b/application/NetscapeBookmarkUtils.php @@ -38,7 +38,7 @@ public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $in if ($link['private'] == 0 && $selection == 'private') { continue; } - $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); + $date = $link['created']; $link['timestamp'] = $date->getTimestamp(); $link['taglist'] = str_replace(' ', ',', $link['tags']); @@ -147,7 +147,6 @@ public static function import($post, $files, $linkDb, $pagecache) 'url' => $bkm['uri'], 'description' => $bkm['note'], 'private' => $private, - 'linkdate'=> '', 'tags' => $bkm['tags'] ); @@ -161,25 +160,22 @@ public static function import($post, $files, $linkDb, $pagecache) } // 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++; $overwriteCount++; continue; } - // Add a new link + // Add a new link - @ used for UNIX timestamps $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++; } diff --git a/application/Updater.php b/application/Updater.php index 36eddd4f..f0d02814 100644 --- a/application/Updater.php +++ b/application/Updater.php @@ -138,10 +138,10 @@ public function updateMethodMergeDeprecatedConfigFile() public function updateMethodRenameDashTags() { $linklist = $this->linkDB->filterSearch(); - foreach ($linklist as $link) { + foreach ($linklist as $key => $link) { $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']); $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')); return true; @@ -215,6 +215,47 @@ public function updateMethodEscapeUnescapedConfig() } 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..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; + } } /** diff --git a/application/Utils.php b/application/Utils.php index 0166ee2a..0a5b476e 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -31,7 +31,11 @@ function logm($logFile, $clientIp, $message) * - are NOT cryptographically secure (they CAN be forged) * * 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. * diff --git a/index.php b/index.php index 5366cb0e..fdbdfaa2 100644 --- a/index.php +++ b/index.php @@ -564,24 +564,19 @@ function showDailyRSS($conf) { ); /* 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. $today = date('Ymd'); $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])) { $days[$day] = array(); } - $days[$day][] = $linkdate; + $days[$day][] = $link; } if (count($days) > $nb_of_days) { @@ -601,24 +596,18 @@ function showDailyRSS($conf) { echo ''. $pageaddr .''. PHP_EOL; // For each day. - foreach ($days as $day => $linkdates) { + foreach ($days as $day => $links) { $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. - // Build the HTML body of this RSS entry. - $links = array(); - // 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: @@ -680,8 +669,7 @@ function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager) $linksToDisplay[$key]['taglist']=$taglist; $linksToDisplay[$key]['formatedDescription'] = format_description($link['description'], $conf->get('redirector.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. @@ -831,7 +819,7 @@ function renderPage($conf, $pluginManager) // Get only links which have a thumbnail. foreach($links as $link) { - $permalink='?'.escape(smallHash($link['linkdate'])); + $permalink='?'.$link['shorturl']; $thumb=lazyThumbnail($conf, $link['url'],$permalink); 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. if (isset($_POST['save_edit'])) { - $linkdate = $_POST['lf_linkdate']; - $updated = isset($LINKSDB[$linkdate]) ? strval(date('Ymd_His')) : false; - // Go away! if (! tokenOk($_POST['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. $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags'])); // Remove first '-' char in tags. @@ -1268,14 +1271,17 @@ function renderPage($conf, $pluginManager) } $link = array( + 'id' => $id, 'title' => trim($_POST['lf_title']), 'url' => $url, 'description' => $_POST['lf_description'], 'private' => (isset($_POST['lf_private']) ? 1 : 0), - 'linkdate' => $linkdate, + 'created' => $created, '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 ($link['title'] == '') { $link['title'] = $link['url']; @@ -1283,7 +1289,7 @@ function renderPage($conf, $pluginManager) $pluginManager->executeHooks('save_link', $link); - $LINKSDB[$linkdate] = $link; + $LINKSDB[$id] = $link; $LINKSDB->save($conf->get('resource.page_cache')); pubsubhub($conf); @@ -1296,7 +1302,7 @@ function renderPage($conf, $pluginManager) $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?'; $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link')); // 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. header('Location: '. $location); exit; @@ -1307,8 +1313,10 @@ function renderPage($conf, $pluginManager) { // If we are called from the bookmarklet, we must close the popup: if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) { echo ''; exit; } + $link = $LINKSDB[(int) escape($_POST['lf_id'])]; $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')); header('Location: '.$returnurl); // After canceling, redirect to the page the user was on. exit; @@ -1318,14 +1326,17 @@ function renderPage($conf, $pluginManager) if (isset($_POST['delete_link'])) { if (!tokenOk($_POST['token'])) die('Wrong token.'); + // We do not need to ask for confirmation: // - confirmation is handled by JavaScript // - 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'])); - unset($LINKSDB[$linkdate]); + $pluginManager->executeHooks('delete_link', $LINKSDB[$id]); + + unset($LINKSDB[$id]); $LINKSDB->save('resource.page_cache'); // save to disk // 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. 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. + $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT); $data = array( 'link' => $link, 'link_is_new' => false, @@ -1389,10 +1402,10 @@ function renderPage($conf, $pluginManager) $link_is_new = false; // Check if URL is not already in database (in this case, we will edit the existing link) $link = $LINKSDB->getLinkFromUrl($url); - if (!$link) + if (! $link) { $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). $title = empty($_GET['title']) ? '' : escape($_GET['title']); // Get description if it was provided in URL (by the bookmarklet). [Bronco added that] @@ -1416,7 +1429,7 @@ function renderPage($conf, $pluginManager) } if ($url == '') { - $url = '?' . smallHash($linkdate); + $url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); $title = 'Note: '; } $url = escape($url); @@ -1430,6 +1443,8 @@ function renderPage($conf, $pluginManager) 'tags' => $tags, 'private' => $private ); + } else { + $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT); } $data = array( @@ -1635,18 +1650,15 @@ function buildLinkList($PAGE,$LINKSDB, $conf, $pluginManager) $link['description'] = format_description($link['description'], $conf->get('redirector.url')); $classLi = ($i % 2) != 0 ? '' : 'publicLinkHightLight'; $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'])) { - $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['updated']); - $link['updated_timestamp'] = $date->getTimestamp(); + $link['updated_timestamp'] = $link['updated']->getTimestamp(); } else { $link['updated_timestamp'] = ''; } $taglist = explode(' ', $link['tags']); uasort($taglist, 'strcasecmp'); $link['taglist'] = $taglist; - $link['shorturl'] = smallHash($link['linkdate']); // Check for both signs of a note: starting with ? and 7 chars long. if ($link['url'][0] === '?' && strlen($link['url']) === 7) { diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php index ffb7cfac..ce16645f 100644 --- a/plugins/isso/isso.php +++ b/plugins/isso/isso.php @@ -41,9 +41,9 @@ function hook_isso_render_linklist($data, $conf) // Only display comments for permalinks. if (count($data['links']) == 1 && empty($data['search_tags']) && empty($data['search_term'])) { $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; // Hackish way to include this CSS file only when necessary. diff --git a/tests/FeedBuilderTest.php b/tests/FeedBuilderTest.php index d7839402..06a44506 100644 --- a/tests/FeedBuilderTest.php +++ b/tests/FeedBuilderTest.php @@ -84,8 +84,9 @@ public function testRSSBuildData() $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); // 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['url']); $this->assertRegExp('/Tue, 10 Mar 2015 11:46:51 \+\d{4}/', $link['pub_iso_date']); @@ -99,14 +100,14 @@ public function testRSSBuildData() $this->assertEquals('sTuff', $link['taglist'][0]); // 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. - $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 - $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 @@ public function testAtomBuildData() $data = $feedBuilder->buildData(); $this->assertEquals(ReferenceLinkDB::$NB_LINKS_TOTAL, count($data['links'])); $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('/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 @@ public function testBuildDataFiltered() $data = $feedBuilder->buildData(); $this->assertEquals(1, count($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 @@ public function testBuildDataCount() $data = $feedBuilder->buildData(); $this->assertEquals(1, count($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 @@ public function testBuildDataPermalinks() $this->assertTrue($data['usepermalinks']); // First link is a permalink $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['url']); $this->assertContains('Direct link', $link['description']); $this->assertContains('http://host.tld/?WDWyig', $link['description']); // Second link is a direct link $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->assertContains('Direct link', $link['description']); $this->assertContains('https://static.fsf.org/nosvn/faif-2.0.pdf', $link['description']); diff --git a/tests/LinkDBTest.php b/tests/LinkDBTest.php index 9d79386c..1f62a34a 100644 --- a/tests/LinkDBTest.php +++ b/tests/LinkDBTest.php @@ -186,14 +186,15 @@ public function testSave() $dbSize = sizeof($testDB); $link = array( + 'id' => 42, 'title'=>'an additional link', 'url'=>'http://dum.my', 'description'=>'One more', 'private'=>0, - 'linkdate'=>'20150518_190000', + 'created'=> DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150518_190000'), 'tags'=>'unit test' ); - $testDB[$link['linkdate']] = $link; + $testDB[$link['id']] = $link; $testDB->save('tests'); $testDB = new LinkDB(self::$testDatastore, true, false); @@ -238,12 +239,12 @@ public function testCountHiddenPublic() public function testDays() { $this->assertEquals( - array('20121206', '20130614', '20150310'), + array('20100310', '20121206', '20130614', '20150310'), self::$publicLinkDB->days() ); $this->assertEquals( - array('20121206', '20130614', '20141125', '20150310'), + array('20100310', '20121206', '20130614', '20141125', '20150310'), self::$privateLinkDB->days() ); } @@ -290,10 +291,11 @@ public function testAllTags() 'stallman' => 1, 'free' => 1, '-exclude' => 1, - // The DB contains a link with `sTuff` and another one with `stuff` tag. - // They need to be grouped with the first case found (`sTuff`). - 'sTuff' => 2, 'hashtag' => 2, + // The DB contains a link with `sTuff` and another one with `stuff` tag. + // They need to be grouped with the first case found - order by date DESC: `sTuff`. + 'sTuff' => 2, + 'ut' => 1, ), self::$publicLinkDB->allTags() ); @@ -321,6 +323,7 @@ public function testAllTags() 'tag2' => 1, 'tag3' => 1, 'tag4' => 1, + 'ut' => 1, ), self::$privateLinkDB->allTags() ); @@ -411,6 +414,11 @@ public function testFilterHashValid() 1, count(self::$publicLinkDB->filterHash($request)) ); + $request = smallHash('20150310_114633' . 8); + $this->assertEquals( + 1, + count(self::$publicLinkDB->filterHash($request)) + ); } /** @@ -433,4 +441,23 @@ public function testFilterHashInValid() { 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); + } + } } diff --git a/tests/LinkFilterTest.php b/tests/LinkFilterTest.php index 7d45fc59..21d680a5 100644 --- a/tests/LinkFilterTest.php +++ b/tests/LinkFilterTest.php @@ -159,7 +159,7 @@ public function testFilterSmallHash() $this->assertEquals( 'MediaGoblin', - $links['20130614_184135']['title'] + $links[7]['title'] ); } @@ -286,7 +286,7 @@ public function testExcludeSearch() ); $this->assertEquals( - 6, + 7, count(self::$linkFilter->filter(LinkFilter::$FILTER_TEXT, '-revolution')) ); } @@ -346,7 +346,7 @@ public function testTagFilterWithExclusion() ); $this->assertEquals( - 6, + 7, count(self::$linkFilter->filter(LinkFilter::$FILTER_TAG, '-free')) ); } diff --git a/tests/NetscapeBookmarkUtils/BookmarkExportTest.php b/tests/NetscapeBookmarkUtils/BookmarkExportTest.php index cc54ab9f..6a47bbb9 100644 --- a/tests/NetscapeBookmarkUtils/BookmarkExportTest.php +++ b/tests/NetscapeBookmarkUtils/BookmarkExportTest.php @@ -50,7 +50,7 @@ public function testFilterAndFormatAll() $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'all', false, ''); $this->assertEquals(self::$refDb->countLinks(), sizeof($links)); foreach ($links as $link) { - $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); + $date = $link['created']; $this->assertEquals( $date->getTimestamp(), $link['timestamp'] @@ -70,7 +70,7 @@ public function testFilterAndFormatPrivate() $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'private', false, ''); $this->assertEquals(self::$refDb->countPrivateLinks(), sizeof($links)); foreach ($links as $link) { - $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); + $date = $link['created']; $this->assertEquals( $date->getTimestamp(), $link['timestamp'] @@ -90,7 +90,7 @@ public function testFilterAndFormatPublic() $links = NetscapeBookmarkUtils::filterAndFormat(self::$linkDb, 'public', false, ''); $this->assertEquals(self::$refDb->countPublicLinks(), sizeof($links)); foreach ($links as $link) { - $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); + $date = $link['created']; $this->assertEquals( $date->getTimestamp(), $link['timestamp'] diff --git a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php index f0ad500f..0ca07eac 100644 --- a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php +++ b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php @@ -42,6 +42,18 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase */ 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 */ @@ -55,6 +67,11 @@ protected function setUp() $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 */ @@ -98,18 +115,19 @@ public function testImportInternetExplorerEncoding() $this->assertEquals( array( - 'linkdate' => '20160618_173944', + 'id' => 0, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160618_203944'), 'title' => 'Hg Init a Mercurial tutorial by Joel Spolsky', 'url' => 'http://hginit.com/', 'description' => '', 'private' => 0, - 'tags' => '' + 'tags' => '', + 'shorturl' => 'La37cg', ), $this->linkDb->getLinkFromUrl('http://hginit.com/') ); } - /** * Import bookmarks nested in a folder hierarchy */ @@ -126,89 +144,105 @@ public function testImportNested() $this->assertEquals( array( - 'linkdate' => '20160225_205541', + 'id' => 0, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235541'), 'title' => 'Nested 1', 'url' => 'http://nest.ed/1', 'description' => '', 'private' => 0, - 'tags' => 'tag1 tag2' + 'tags' => 'tag1 tag2', + 'shorturl' => 'KyDNKA', ), $this->linkDb->getLinkFromUrl('http://nest.ed/1') ); $this->assertEquals( array( - 'linkdate' => '20160225_205542', + 'id' => 1, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235542'), 'title' => 'Nested 1-1', 'url' => 'http://nest.ed/1-1', 'description' => '', 'private' => 0, - 'tags' => 'folder1 tag1 tag2' + 'tags' => 'folder1 tag1 tag2', + 'shorturl' => 'T2LnXg', ), $this->linkDb->getLinkFromUrl('http://nest.ed/1-1') ); $this->assertEquals( array( - 'linkdate' => '20160225_205547', + 'id' => 2, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235547'), 'title' => 'Nested 1-2', 'url' => 'http://nest.ed/1-2', 'description' => '', 'private' => 0, - 'tags' => 'folder1 tag3 tag4' + 'tags' => 'folder1 tag3 tag4', + 'shorturl' => '46SZxA', ), $this->linkDb->getLinkFromUrl('http://nest.ed/1-2') ); $this->assertEquals( array( - 'linkdate' => '20160202_172222', + 'id' => 3, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160202_202222'), 'title' => 'Nested 2-1', 'url' => 'http://nest.ed/2-1', 'description' => 'First link of the second section', 'private' => 1, - 'tags' => 'folder2' + 'tags' => 'folder2', + 'shorturl' => '4UHOSw', ), $this->linkDb->getLinkFromUrl('http://nest.ed/2-1') ); $this->assertEquals( array( - 'linkdate' => '20160119_200227', + 'id' => 4, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160119_230227'), 'title' => 'Nested 2-2', 'url' => 'http://nest.ed/2-2', 'description' => 'Second link of the second section', 'private' => 1, - 'tags' => 'folder2' + 'tags' => 'folder2', + 'shorturl' => 'yfzwbw', ), $this->linkDb->getLinkFromUrl('http://nest.ed/2-2') ); $this->assertEquals( array( - 'linkdate' => '20160202_172223', + 'id' => 5, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160202_202222'), 'title' => 'Nested 3-1', 'url' => 'http://nest.ed/3-1', 'description' => '', 'private' => 0, - 'tags' => 'folder3 folder3-1 tag3' + 'tags' => 'folder3 folder3-1 tag3', + 'shorturl' => 'UwxIUQ', ), $this->linkDb->getLinkFromUrl('http://nest.ed/3-1') ); $this->assertEquals( array( - 'linkdate' => '20160119_200228', + 'id' => 6, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160119_230227'), 'title' => 'Nested 3-2', 'url' => 'http://nest.ed/3-2', 'description' => '', 'private' => 0, - 'tags' => 'folder3 folder3-1' + 'tags' => 'folder3 folder3-1', + 'shorturl' => 'p8dyZg', ), $this->linkDb->getLinkFromUrl('http://nest.ed/3-2') ); $this->assertEquals( array( - 'linkdate' => '20160229_081541', + 'id' => 7, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160229_111541'), 'title' => 'Nested 2', 'url' => 'http://nest.ed/2', 'description' => '', 'private' => 0, - 'tags' => 'tag4' + 'tags' => 'tag4', + 'shorturl' => 'Gt3Uug', ), $this->linkDb->getLinkFromUrl('http://nest.ed/2') ); @@ -227,28 +261,34 @@ public function testImportDefaultPrivacyNoPost() .' 2 links imported, 0 links overwritten, 0 links skipped.', NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->pagecache) ); + $this->assertEquals(2, count($this->linkDb)); $this->assertEquals(1, count_private($this->linkDb)); $this->assertEquals( 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', 'url' => 'https://private.tld', 'description' => "Super-secret stuff you're not supposed to know about", 'private' => 1, - 'tags' => 'private secret' + 'tags' => 'private secret', + 'shorturl' => 'EokDtA', ), $this->linkDb->getLinkFromUrl('https://private.tld') ); $this->assertEquals( array( - 'linkdate' => '20160225_205548', + 'id' => 1, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'), 'title' => 'Public stuff', 'url' => 'http://public.tld', 'description' => '', 'private' => 0, - 'tags' => 'public hello world' + 'tags' => 'public hello world', + 'shorturl' => 'Er9ddA', ), $this->linkDb->getLinkFromUrl('http://public.tld') ); @@ -271,23 +311,28 @@ public function testImportKeepPrivacy() $this->assertEquals( 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', 'url' => 'https://private.tld', 'description' => "Super-secret stuff you're not supposed to know about", 'private' => 1, - 'tags' => 'private secret' + 'tags' => 'private secret', + 'shorturl' => 'EokDtA', ), $this->linkDb->getLinkFromUrl('https://private.tld') ); $this->assertEquals( array( - 'linkdate' => '20160225_205548', + 'id' => 1, + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160225_235548'), 'title' => 'Public stuff', 'url' => 'http://public.tld', 'description' => '', 'private' => 0, - 'tags' => 'public hello world' + 'tags' => 'public hello world', + 'shorturl' => 'Er9ddA', ), $this->linkDb->getLinkFromUrl('http://public.tld') ); @@ -309,11 +354,11 @@ public function testImportAsPublic() $this->assertEquals(0, count_private($this->linkDb)); $this->assertEquals( 0, - $this->linkDb['20001010_105536']['private'] + $this->linkDb[0]['private'] ); $this->assertEquals( 0, - $this->linkDb['20160225_205548']['private'] + $this->linkDb[1]['private'] ); } @@ -333,11 +378,11 @@ public function testImportAsPrivate() $this->assertEquals(2, count_private($this->linkDb)); $this->assertEquals( 1, - $this->linkDb['20001010_105536']['private'] + $this->linkDb['0']['private'] ); $this->assertEquals( 1, - $this->linkDb['20160225_205548']['private'] + $this->linkDb['1']['private'] ); } @@ -359,13 +404,12 @@ public function testOverwriteAsPublic() $this->assertEquals(2, count_private($this->linkDb)); $this->assertEquals( 1, - $this->linkDb['20001010_105536']['private'] + $this->linkDb[0]['private'] ); $this->assertEquals( 1, - $this->linkDb['20160225_205548']['private'] + $this->linkDb[1]['private'] ); - // re-import as public, enable overwriting $post = array( 'privacy' => 'public', @@ -380,11 +424,11 @@ public function testOverwriteAsPublic() $this->assertEquals(0, count_private($this->linkDb)); $this->assertEquals( 0, - $this->linkDb['20001010_105536']['private'] + $this->linkDb[0]['private'] ); $this->assertEquals( 0, - $this->linkDb['20160225_205548']['private'] + $this->linkDb[1]['private'] ); } @@ -406,11 +450,11 @@ public function testOverwriteAsPrivate() $this->assertEquals(0, count_private($this->linkDb)); $this->assertEquals( 0, - $this->linkDb['20001010_105536']['private'] + $this->linkDb['0']['private'] ); $this->assertEquals( 0, - $this->linkDb['20160225_205548']['private'] + $this->linkDb['1']['private'] ); // re-import as private, enable overwriting @@ -427,11 +471,11 @@ public function testOverwriteAsPrivate() $this->assertEquals(2, count_private($this->linkDb)); $this->assertEquals( 1, - $this->linkDb['20001010_105536']['private'] + $this->linkDb['0']['private'] ); $this->assertEquals( 1, - $this->linkDb['20160225_205548']['private'] + $this->linkDb['1']['private'] ); } @@ -480,11 +524,11 @@ public function testSetDefaultTags() $this->assertEquals(0, count_private($this->linkDb)); $this->assertEquals( 'tag1 tag2 tag3 private secret', - $this->linkDb['20001010_105536']['tags'] + $this->linkDb['0']['tags'] ); $this->assertEquals( 'tag1 tag2 tag3 public hello world', - $this->linkDb['20160225_205548']['tags'] + $this->linkDb['1']['tags'] ); } @@ -507,16 +551,16 @@ public function testSanitizeDefaultTags() $this->assertEquals(0, count_private($this->linkDb)); $this->assertEquals( 'tag1& tag2 "tag3" private secret', - $this->linkDb['20001010_105536']['tags'] + $this->linkDb['0']['tags'] ); $this->assertEquals( 'tag1& tag2 "tag3" 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 */ @@ -531,16 +575,16 @@ public function testImportSameDate() $this->assertEquals(3, count($this->linkDb)); $this->assertEquals(0, count_private($this->linkDb)); $this->assertEquals( - '20160225_205548', - $this->linkDb['20160225_205548']['linkdate'] + 0, + $this->linkDb[0]['id'] ); $this->assertEquals( - '20160225_205549', - $this->linkDb['20160225_205549']['linkdate'] + 1, + $this->linkDb[1]['id'] ); $this->assertEquals( - '20160225_205550', - $this->linkDb['20160225_205550']['linkdate'] + 2, + $this->linkDb[2]['id'] ); } } diff --git a/tests/Updater/UpdaterTest.php b/tests/Updater/UpdaterTest.php index 0d0ad922..4948fe52 100644 --- a/tests/Updater/UpdaterTest.php +++ b/tests/Updater/UpdaterTest.php @@ -214,6 +214,7 @@ public function testRenameDashTags() $refDB = new ReferenceLinkDB(); $refDB->write(self::$testDatastore); $linkDB = new LinkDB(self::$testDatastore, true, false); + $this->assertEmpty($linkDB->filterSearch(array('searchtags' => 'exclude'))); $updater = new Updater(array(), $linkDB, $this->conf, true); $updater->updateMethodRenameDashTags(); @@ -287,4 +288,101 @@ public function testEscapeConfig() $this->assertEquals(escape($redirectorUrl), $this->conf->get('redirector.url')); 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)); + } } diff --git a/tests/plugins/PluginIssoTest.php b/tests/plugins/PluginIssoTest.php index 1f545c7d..6b7904dd 100644 --- a/tests/plugins/PluginIssoTest.php +++ b/tests/plugins/PluginIssoTest.php @@ -47,12 +47,14 @@ function testIssoDisplayed() $conf->set('plugins.ISSO_SERVER', 'value'); $str = 'http://randomstr.com/test'; + $date = '20161118_100001'; $data = array( 'title' => $str, 'links' => array( array( + 'id' => 12, 'url' => $str, - 'linkdate' => 'abc', + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date), ) ) ); @@ -65,7 +67,14 @@ function testIssoDisplayed() // plugin data $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')); } @@ -78,16 +87,20 @@ function testIssoMultipleLinks() $conf->set('plugins.ISSO_SERVER', 'value'); $str = 'http://randomstr.com/test'; + $date1 = '20161118_100001'; + $date2 = '20161118_100002'; $data = array( 'title' => $str, 'links' => array( array( + 'id' => 12, 'url' => $str, - 'linkdate' => 'abc', + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date1), ), array( + 'id' => 13, 'url' => $str . '2', - 'linkdate' => 'abc2', + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date2), ), ) ); @@ -106,12 +119,14 @@ function testIssoNotDisplayedWhenSearch() $conf->set('plugins.ISSO_SERVER', 'value'); $str = 'http://randomstr.com/test'; + $date = '20161118_100001'; $data = array( 'title' => $str, 'links' => array( array( + 'id' => 12, 'url' => $str, - 'linkdate' => 'abc', + 'created' => DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $date), ) ), 'search_term' => $str diff --git a/tests/utils/ReferenceLinkDB.php b/tests/utils/ReferenceLinkDB.php index abca4656..36d58c68 100644 --- a/tests/utils/ReferenceLinkDB.php +++ b/tests/utils/ReferenceLinkDB.php @@ -4,7 +4,7 @@ */ class ReferenceLinkDB { - public static $NB_LINKS_TOTAL = 7; + public static $NB_LINKS_TOTAL = 8; private $_links = array(); private $_publicCount = 0; @@ -16,66 +16,87 @@ class ReferenceLinkDB public function __construct() { $this->addLink( + 41, 'Link title: @website', '?WDWyig', 'Stallman has a beard and is part of the Free Software Foundation (or not). Seriously, read this. #hashtag', 0, - '20150310_114651', - 'sTuff' + DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114651'), + 'sTuff', + null, + 'WDWyig' ); $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', 'https://static.fsf.org/nosvn/faif-2.0.pdf', 'Richard Stallman and the Free Software Revolution. Read this. #hashtag', 0, - '20150310_114633', + DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20150310_114633'), 'free gnu software stallman -exclude stuff hashtag', - '20160803_093033' + DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20160803_093033') ); $this->addLink( + 7, 'MediaGoblin', 'http://mediagoblin.org/', 'A free software media publishing platform #hashtagOther', 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( + 6, 'w3c-markup-validator', 'https://dvcs.w3.org/hg/markup-validator/summary', 'Mercurial repository for the W3C Validator #private', 1, - '20141125_084734', + DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20141125_084734'), 'css html w3c web Mercurial' ); $this->addLink( + 4, 'UserFriendly - Web Designer', 'http://ars.userfriendly.org/cartoons/?id=20121206', 'Naming conventions... #private', 0, - '20121206_142300', + DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_142300'), 'dev cartoon web' ); $this->addLink( + 1, 'UserFriendly - Samba', 'http://ars.userfriendly.org/cartoons/?id=20010306', 'Tropical printing', 0, - '20121206_172539', + DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_172539'), 'samba cartoon web' ); $this->addLink( + 0, 'Geek and Poke', 'http://geek-and-poke.com/', '', 1, - '20121206_182539', + DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, '20121206_182539'), 'dev cartoon tag1 tag2 tag3 tag4 ' ); } @@ -83,18 +104,20 @@ public function __construct() /** * 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( + 'id' => $id, 'title' => $title, 'url' => $url, 'description' => $description, 'private' => $private, - 'linkdate' => $date, 'tags' => $tags, + 'created' => $date, 'updated' => $updated, + 'shorturl' => $shorturl ? $shorturl : smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id), ); - $this->_links[$date] = $link; + $this->_links[$id] = $link; if ($private) { $this->_privateCount++; @@ -142,4 +165,14 @@ public function getLinks() { return $this->_links; } + + /** + * Setter to override link creation. + * + * @param array $links List of links. + */ + public function setLinks($links) + { + $this->_links = $links; + } } diff --git a/tpl/daily.html b/tpl/daily.html index b82ad483..eba0af3b 100644 --- a/tpl/daily.html +++ b/tpl/daily.html @@ -49,13 +49,13 @@ {$link=$value} - + {if="!$hide_timestamps || isLoggedIn()"} - {function="strftime('%c', $link.timestamp)"} + {function="strftime('%c', $link.timestamp)"} {/if} {if="$link.tags"} diff --git a/tpl/editlink.html b/tpl/editlink.html index 9e7621db..870cc168 100644 --- a/tpl/editlink.html +++ b/tpl/editlink.html @@ -16,6 +16,9 @@ + {if="isset($link.id)"} + + {/if} URL Title Description{$link.description} diff --git a/tpl/linklist.html b/tpl/linklist.html index 3fe86deb..0f1a5e8c 100644 --- a/tpl/linklist.html +++ b/tpl/linklist.html @@ -81,11 +81,11 @@ {if="isLoggedIn()"} - + - + - + {function="strftime('%c', $value.timestamp)"} {if="$value.updated_timestamp"}*{/if}