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

Link ID refactoring
This commit is contained in:
Arthur 2016-12-12 03:15:32 +01:00 committed by GitHub
commit 9cf93bcfc5
20 changed files with 618 additions and 234 deletions

View file

@ -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 .'<br>&#8212; '. $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);;

View file

@ -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;
}
}

View file

@ -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);
}
/**

View file

@ -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);
}

View file

@ -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++;
}

View file

@ -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.<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;
}
}
/**

View file

@ -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.
*

104
index.php
View file

@ -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 '<copyright>'. $pageaddr .'</copyright>'. 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 '<script>self.close();</script>'; 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) {

View file

@ -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.

View file

@ -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']);

View file

@ -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);
}
}
}

View file

@ -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'))
);
}

View file

@ -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']

View file

@ -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',