aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
authorArthur <arthur@hoa.ro>2016-12-12 03:15:32 +0100
committerGitHub <noreply@github.com>2016-12-12 03:15:32 +0100
commit9cf93bcfc53c36e0dd59fcfc717ac483ee74b35a (patch)
tree505cd68f1d07e0b4d6aedcd49c31368760798c62 /application
parenta0d079141eb155c263ebfaa1aad2629382223e31 (diff)
parentd592daea8343bb4dfecff5d97e93699581ccc58c (diff)
downloadShaarli-9cf93bcfc53c36e0dd59fcfc717ac483ee74b35a.tar.gz
Shaarli-9cf93bcfc53c36e0dd59fcfc717ac483ee74b35a.tar.zst
Shaarli-9cf93bcfc53c36e0dd59fcfc717ac483ee74b35a.zip
Merge pull request #697 from ArthurHoaro/feature/ids-bis
Link ID refactoring
Diffstat (limited to 'application')
-rw-r--r--application/FeedBuilder.php6
-rw-r--r--application/LinkDB.php184
-rw-r--r--application/LinkFilter.php37
-rw-r--r--application/LinkUtils.php13
-rw-r--r--application/NetscapeBookmarkUtils.php26
-rw-r--r--application/Updater.php45
-rw-r--r--application/Utils.php6
7 files changed, 232 insertions, 85 deletions
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 @@ class FeedBuilder
143 */ 143 */
144 protected function buildItem($link, $pageaddr) 144 protected function buildItem($link, $pageaddr)
145 { 145 {
146 $link['guid'] = $pageaddr .'?'. smallHash($link['linkdate']); 146 $link['guid'] = $pageaddr .'?'. $link['shorturl'];
147 // Check for both signs of a note: starting with ? and 7 chars long. 147 // Check for both signs of a note: starting with ? and 7 chars long.
148 if ($link['url'][0] === '?' && strlen($link['url']) === 7) { 148 if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
149 $link['url'] = $pageaddr . $link['url']; 149 $link['url'] = $pageaddr . $link['url'];
@@ -156,12 +156,12 @@ class FeedBuilder
156 $link['description'] = format_description($link['description'], '', $pageaddr); 156 $link['description'] = format_description($link['description'], '', $pageaddr);
157 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink; 157 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;
158 158
159 $pubDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); 159 $pubDate = $link['created'];
160 $link['pub_iso_date'] = $this->getIsoDate($pubDate); 160 $link['pub_iso_date'] = $this->getIsoDate($pubDate);
161 161
162 // atom:entry elements MUST contain exactly one atom:updated element. 162 // atom:entry elements MUST contain exactly one atom:updated element.
163 if (!empty($link['updated'])) { 163 if (!empty($link['updated'])) {
164 $upDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['updated']); 164 $upDate = $link['updated'];
165 $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM); 165 $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
166 } else { 166 } else {
167 $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);; 167 $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 @@
6 * 6 *
7 * Example: 7 * Example:
8 * $myLinks = new LinkDB(); 8 * $myLinks = new LinkDB();
9 * echo $myLinks['20110826_161819']['title']; 9 * echo $myLinks[350]['title'];
10 * foreach ($myLinks as $link) 10 * foreach ($myLinks as $link)
11 * echo $link['title'].' at url '.$link['url'].'; description:'.$link['description']; 11 * echo $link['title'].' at url '.$link['url'].'; description:'.$link['description'];
12 * 12 *
13 * Available keys: 13 * Available keys:
14 * - id: primary key, incremental integer identifier (persistent)
14 * - description: description of the entry 15 * - description: description of the entry
15 * - linkdate: creation date of this entry, format: YYYYMMDD_HHMMSS 16 * - created: creation date of this entry, DateTime object.
16 * (e.g.'20110914_192317') 17 * - updated: last modification date of this entry, DateTime object.
17 * - updated: last modification date of this entry, format: YYYYMMDD_HHMMSS
18 * - private: Is this link private? 0=no, other value=yes 18 * - private: Is this link private? 0=no, other value=yes
19 * - tags: tags attached to this entry (separated by spaces) 19 * - tags: tags attached to this entry (separated by spaces)
20 * - title Title of the link 20 * - title Title of the link
@@ -22,11 +22,25 @@
22 * Can be absolute or relative. 22 * Can be absolute or relative.
23 * Relative URLs are permalinks (e.g.'?m-ukcw') 23 * Relative URLs are permalinks (e.g.'?m-ukcw')
24 * - real_url Absolute processed URL. 24 * - real_url Absolute processed URL.
25 * - shorturl Permalink smallhash
25 * 26 *
26 * Implements 3 interfaces: 27 * Implements 3 interfaces:
27 * - ArrayAccess: behaves like an associative array; 28 * - ArrayAccess: behaves like an associative array;
28 * - Countable: there is a count() method; 29 * - Countable: there is a count() method;
29 * - Iterator: usable in foreach () loops. 30 * - Iterator: usable in foreach () loops.
31 *
32 * ID mechanism:
33 * ArrayAccess is implemented in a way that will allow to access a link
34 * with the unique identifier ID directly with $link[ID].
35 * Note that it's not the real key of the link array attribute.
36 * This mechanism is in place to have persistent link IDs,
37 * even though the internal array is reordered by date.
38 * Example:
39 * - DB: link #1 (2010-01-01) link #2 (2016-01-01)
40 * - Order: #2 #1
41 * - Import links containing: link #3 (2013-01-01)
42 * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01)
43 * - Real order: #2 #3 #1
30 */ 44 */
31class LinkDB implements Iterator, Countable, ArrayAccess 45class LinkDB implements Iterator, Countable, ArrayAccess
32{ 46{
@@ -47,11 +61,17 @@ class LinkDB implements Iterator, Countable, ArrayAccess
47 // - value: associative array (keys: title, description...) 61 // - value: associative array (keys: title, description...)
48 private $links; 62 private $links;
49 63
50 // List of all recorded URLs (key=url, value=linkdate) 64 // List of all recorded URLs (key=url, value=link offset)
51 // for fast reserve search (url-->linkdate) 65 // for fast reserve search (url-->link offset)
52 private $urls; 66 private $urls;
53 67
54 // List of linkdate keys (for the Iterator interface implementation) 68 /**
69 * @var array List of all links IDS mapped with their array offset.
70 * Map: id->offset.
71 */
72 protected $ids;
73
74 // List of offset keys (for the Iterator interface implementation)
55 private $keys; 75 private $keys;
56 76
57 // Position in the $this->keys array (for the Iterator interface) 77 // Position in the $this->keys array (for the Iterator interface)
@@ -121,14 +141,26 @@ class LinkDB implements Iterator, Countable, ArrayAccess
121 if (!$this->loggedIn) { 141 if (!$this->loggedIn) {
122 die('You are not authorized to add a link.'); 142 die('You are not authorized to add a link.');
123 } 143 }
124 if (empty($value['linkdate']) || empty($value['url'])) { 144 if (!isset($value['id']) || empty($value['url'])) {
125 die('Internal Error: A link should always have a linkdate and URL.'); 145 die('Internal Error: A link should always have an id and URL.');
126 } 146 }
127 if (empty($offset)) { 147 if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) {
128 die('You must specify a key.'); 148 die('You must specify an integer as a key.');
149 }
150 if (! empty($offset) && $offset !== $value['id']) {
151 die('Array offset and link ID must be equal.');
152 }
153
154 // If the link exists, we reuse the real offset, otherwise new entry
155 $existing = $this->getLinkOffset($offset);
156 if ($existing !== null) {
157 $offset = $existing;
158 } else {
159 $offset = count($this->links);
129 } 160 }
130 $this->links[$offset] = $value; 161 $this->links[$offset] = $value;
131 $this->urls[$value['url']]=$offset; 162 $this->urls[$value['url']] = $offset;
163 $this->ids[$value['id']] = $offset;
132 } 164 }
133 165
134 /** 166 /**
@@ -136,7 +168,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
136 */ 168 */
137 public function offsetExists($offset) 169 public function offsetExists($offset)
138 { 170 {
139 return array_key_exists($offset, $this->links); 171 return array_key_exists($this->getLinkOffset($offset), $this->links);
140 } 172 }
141 173
142 /** 174 /**
@@ -148,9 +180,11 @@ class LinkDB implements Iterator, Countable, ArrayAccess
148 // TODO: raise an exception 180 // TODO: raise an exception
149 die('You are not authorized to delete a link.'); 181 die('You are not authorized to delete a link.');
150 } 182 }
151 $url = $this->links[$offset]['url']; 183 $realOffset = $this->getLinkOffset($offset);
184 $url = $this->links[$realOffset]['url'];
152 unset($this->urls[$url]); 185 unset($this->urls[$url]);
153 unset($this->links[$offset]); 186 unset($this->ids[$realOffset]);
187 unset($this->links[$realOffset]);
154 } 188 }
155 189
156 /** 190 /**
@@ -158,7 +192,8 @@ class LinkDB implements Iterator, Countable, ArrayAccess
158 */ 192 */
159 public function offsetGet($offset) 193 public function offsetGet($offset)
160 { 194 {
161 return isset($this->links[$offset]) ? $this->links[$offset] : null; 195 $realOffset = $this->getLinkOffset($offset);
196 return isset($this->links[$realOffset]) ? $this->links[$realOffset] : null;
162 } 197 }
163 198
164 /** 199 /**
@@ -166,7 +201,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
166 */ 201 */
167 public function current() 202 public function current()
168 { 203 {
169 return $this->links[$this->keys[$this->position]]; 204 return $this[$this->keys[$this->position]];
170 } 205 }
171 206
172 /** 207 /**
@@ -192,8 +227,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
192 */ 227 */
193 public function rewind() 228 public function rewind()
194 { 229 {
195 $this->keys = array_keys($this->links); 230 $this->keys = array_keys($this->ids);
196 rsort($this->keys);
197 $this->position = 0; 231 $this->position = 0;
198 } 232 }
199 233
@@ -219,6 +253,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess
219 // Create a dummy database for example 253 // Create a dummy database for example
220 $this->links = array(); 254 $this->links = array();
221 $link = array( 255 $link = array(
256 'id' => 1,
222 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', 257 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
223 'url'=>'https://github.com/shaarli/Shaarli/wiki', 258 'url'=>'https://github.com/shaarli/Shaarli/wiki',
224 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. 259 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
@@ -227,20 +262,23 @@ To learn how to use Shaarli, consult the link "Help/documentation" at the bottom
227 262
228You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', 263You use the community supported version of the original Shaarli project, by Sebastien Sauvage.',
229 'private'=>0, 264 'private'=>0,
230 'linkdate'=> date('Ymd_His'), 265 'created'=> new DateTime(),
231 'tags'=>'opensource software' 266 'tags'=>'opensource software'
232 ); 267 );
233 $this->links[$link['linkdate']] = $link; 268 $link['shorturl'] = link_small_hash($link['created'], $link['id']);
269 $this->links[1] = $link;
234 270
235 $link = array( 271 $link = array(
272 'id' => 0,
236 'title'=>'My secret stuff... - Pastebin.com', 273 'title'=>'My secret stuff... - Pastebin.com',
237 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', 274 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
238 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', 275 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.',
239 'private'=>1, 276 'private'=>1,
240 'linkdate'=> date('Ymd_His', strtotime('-1 minute')), 277 'created'=> new DateTime('1 minute ago'),
241 'tags'=>'secretstuff' 278 'tags'=>'secretstuff',
242 ); 279 );
243 $this->links[$link['linkdate']] = $link; 280 $link['shorturl'] = link_small_hash($link['created'], $link['id']);
281 $this->links[0] = $link;
244 282
245 // Write database to disk 283 // Write database to disk
246 $this->write(); 284 $this->write();
@@ -251,7 +289,6 @@ You use the community supported version of the original Shaarli project, by Seba
251 */ 289 */
252 private function read() 290 private function read()
253 { 291 {
254
255 // Public links are hidden and user not logged in => nothing to show 292 // Public links are hidden and user not logged in => nothing to show
256 if ($this->hidePublicLinks && !$this->loggedIn) { 293 if ($this->hidePublicLinks && !$this->loggedIn) {
257 $this->links = array(); 294 $this->links = array();
@@ -269,23 +306,13 @@ You use the community supported version of the original Shaarli project, by Seba
269 strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); 306 strlen(self::$phpPrefix), -strlen(self::$phpSuffix)))));
270 } 307 }
271 308
272 // If user is not logged in, filter private links. 309 $toremove = array();
273 if (!$this->loggedIn) { 310 foreach ($this->links as $key => &$link) {
274 $toremove = array(); 311 if (! $this->loggedIn && $link['private'] != 0) {
275 foreach ($this->links as $link) { 312 // Transition for not upgraded databases.
276 if ($link['private'] != 0) { 313 $toremove[] = $key;
277 $toremove[] = $link['linkdate']; 314 continue;
278 }
279 }
280 foreach ($toremove as $linkdate) {
281 unset($this->links[$linkdate]);
282 } 315 }
283 }
284
285 $this->urls = array();
286 foreach ($this->links as &$link) {
287 // Keep the list of the mapping URLs-->linkdate up-to-date.
288 $this->urls[$link['url']] = $link['linkdate'];
289 316
290 // Sanitize data fields. 317 // Sanitize data fields.
291 sanitizeLink($link); 318 sanitizeLink($link);
@@ -307,7 +334,24 @@ You use the community supported version of the original Shaarli project, by Seba
307 else { 334 else {
308 $link['real_url'] = $link['url']; 335 $link['real_url'] = $link['url'];
309 } 336 }
337
338 // To be able to load links before running the update, and prepare the update
339 if (! isset($link['created'])) {
340 $link['id'] = $link['linkdate'];
341 $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
342 if (! empty($link['updated'])) {
343 $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']);
344 }
345 $link['shorturl'] = smallHash($link['linkdate']);
346 }
347 }
348
349 // If user is not logged in, filter private links.
350 foreach ($toremove as $offset) {
351 unset($this->links[$offset]);
310 } 352 }
353
354 $this->reorder();
311 } 355 }
312 356
313 /** 357 /**
@@ -430,7 +474,7 @@ You use the community supported version of the original Shaarli project, by Seba
430 $request = ''; 474 $request = '';
431 } 475 }
432 476
433 $linkFilter = new LinkFilter($this->links); 477 $linkFilter = new LinkFilter($this);
434 return $linkFilter->filter($type, $request, $casesensitive, $privateonly); 478 return $linkFilter->filter($type, $request, $casesensitive, $privateonly);
435 } 479 }
436 480
@@ -467,12 +511,64 @@ You use the community supported version of the original Shaarli project, by Seba
467 public function days() 511 public function days()
468 { 512 {
469 $linkDays = array(); 513 $linkDays = array();
470 foreach (array_keys($this->links) as $day) { 514 foreach ($this->links as $link) {
471 $linkDays[substr($day, 0, 8)] = 0; 515 $linkDays[$link['created']->format('Ymd')] = 0;
472 } 516 }
473 $linkDays = array_keys($linkDays); 517 $linkDays = array_keys($linkDays);
474 sort($linkDays); 518 sort($linkDays);
475 519
476 return $linkDays; 520 return $linkDays;
477 } 521 }
522
523 /**
524 * Reorder links by creation date (newest first).
525 *
526 * Also update the urls and ids mapping arrays.
527 *
528 * @param string $order ASC|DESC
529 */
530 public function reorder($order = 'DESC')
531 {
532 $order = $order === 'ASC' ? -1 : 1;
533 // Reorder array by dates.
534 usort($this->links, function($a, $b) use ($order) {
535 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
536 });
537
538 $this->urls = array();
539 $this->ids = array();
540 foreach ($this->links as $key => $link) {
541 $this->urls[$link['url']] = $key;
542 $this->ids[$link['id']] = $key;
543 }
544 }
545
546 /**
547 * Return the next key for link creation.
548 * E.g. If the last ID is 597, the next will be 598.
549 *
550 * @return int next ID.
551 */
552 public function getNextId()
553 {
554 if (!empty($this->ids)) {
555 return max(array_keys($this->ids)) + 1;
556 }
557 return 0;
558 }
559
560 /**
561 * Returns a link offset in links array from its unique ID.
562 *
563 * @param int $id Persistent ID of a link.
564 *
565 * @return int Real offset in local array, or null if doesn't exist.
566 */
567 protected function getLinkOffset($id)
568 {
569 if (isset($this->ids[$id])) {
570 return $this->ids[$id];
571 }
572 return null;
573 }
478} 574}
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
33 public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}'; 33 public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
34 34
35 /** 35 /**
36 * @var array all available links. 36 * @var LinkDB all available links.
37 */ 37 */
38 private $links; 38 private $links;
39 39
40 /** 40 /**
41 * @param array $links initialization. 41 * @param LinkDB $links initialization.
42 */ 42 */
43 public function __construct($links) 43 public function __construct($links)
44 { 44 {
@@ -94,18 +94,16 @@ class LinkFilter
94 private function noFilter($privateonly = false) 94 private function noFilter($privateonly = false)
95 { 95 {
96 if (! $privateonly) { 96 if (! $privateonly) {
97 krsort($this->links);
98 return $this->links; 97 return $this->links;
99 } 98 }
100 99
101 $out = array(); 100 $out = array();
102 foreach ($this->links as $value) { 101 foreach ($this->links as $key => $value) {
103 if ($value['private']) { 102 if ($value['private']) {
104 $out[$value['linkdate']] = $value; 103 $out[$key] = $value;
105 } 104 }
106 } 105 }
107 106
108 krsort($out);
109 return $out; 107 return $out;
110 } 108 }
111 109
@@ -121,10 +119,10 @@ class LinkFilter
121 private function filterSmallHash($smallHash) 119 private function filterSmallHash($smallHash)
122 { 120 {
123 $filtered = array(); 121 $filtered = array();
124 foreach ($this->links as $l) { 122 foreach ($this->links as $key => $l) {
125 if ($smallHash == smallHash($l['linkdate'])) { 123 if ($smallHash == $l['shorturl']) {
126 // Yes, this is ugly and slow 124 // Yes, this is ugly and slow
127 $filtered[$l['linkdate']] = $l; 125 $filtered[$key] = $l;
128 return $filtered; 126 return $filtered;
129 } 127 }
130 } 128 }
@@ -188,7 +186,7 @@ class LinkFilter
188 $keys = array('title', 'description', 'url', 'tags'); 186 $keys = array('title', 'description', 'url', 'tags');
189 187
190 // Iterate over every stored link. 188 // Iterate over every stored link.
191 foreach ($this->links as $link) { 189 foreach ($this->links as $id => $link) {
192 190
193 // ignore non private links when 'privatonly' is on. 191 // ignore non private links when 'privatonly' is on.
194 if (! $link['private'] && $privateonly === true) { 192 if (! $link['private'] && $privateonly === true) {
@@ -222,11 +220,10 @@ class LinkFilter
222 } 220 }
223 221
224 if ($found) { 222 if ($found) {
225 $filtered[$link['linkdate']] = $link; 223 $filtered[$id] = $link;
226 } 224 }
227 } 225 }
228 226
229 krsort($filtered);
230 return $filtered; 227 return $filtered;
231 } 228 }
232 229
@@ -256,7 +253,7 @@ class LinkFilter
256 return $filtered; 253 return $filtered;
257 } 254 }
258 255
259 foreach ($this->links as $link) { 256 foreach ($this->links as $key => $link) {
260 // ignore non private links when 'privatonly' is on. 257 // ignore non private links when 'privatonly' is on.
261 if (! $link['private'] && $privateonly === true) { 258 if (! $link['private'] && $privateonly === true) {
262 continue; 259 continue;
@@ -278,10 +275,9 @@ class LinkFilter
278 } 275 }
279 276
280 if ($found) { 277 if ($found) {
281 $filtered[$link['linkdate']] = $link; 278 $filtered[$key] = $link;
282 } 279 }
283 } 280 }
284 krsort($filtered);
285 return $filtered; 281 return $filtered;
286 } 282 }
287 283
@@ -304,13 +300,14 @@ class LinkFilter
304 } 300 }
305 301
306 $filtered = array(); 302 $filtered = array();
307 foreach ($this->links as $l) { 303 foreach ($this->links as $key => $l) {
308 if (startsWith($l['linkdate'], $day)) { 304 if ($l['created']->format('Ymd') == $day) {
309 $filtered[$l['linkdate']] = $l; 305 $filtered[$key] = $l;
310 } 306 }
311 } 307 }
312 ksort($filtered); 308
313 return $filtered; 309 // sort by date ASC
310 return array_reverse($filtered, true);
314 } 311 }
315 312
316 /** 313 /**
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)
169function format_description($description, $redirector = '', $indexUrl = '') { 169function format_description($description, $redirector = '', $indexUrl = '') {
170 return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl))); 170 return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl)));
171} 171}
172
173/**
174 * Generate a small hash for a link.
175 *
176 * @param DateTime $date Link creation date.
177 * @param int $id Link ID.
178 *
179 * @return string the small hash generated from link data.
180 */
181function link_small_hash($date, $id)
182{
183 return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id);
184}
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 @@ class NetscapeBookmarkUtils
38 if ($link['private'] == 0 && $selection == 'private') { 38 if ($link['private'] == 0 && $selection == 'private') {
39 continue; 39 continue;
40 } 40 }
41 $date = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $link['linkdate']); 41 $date = $link['created'];
42 $link['timestamp'] = $date->getTimestamp(); 42 $link['timestamp'] = $date->getTimestamp();
43 $link['taglist'] = str_replace(' ', ',', $link['tags']); 43 $link['taglist'] = str_replace(' ', ',', $link['tags']);
44 44
@@ -147,7 +147,6 @@ class NetscapeBookmarkUtils
147 'url' => $bkm['uri'], 147 'url' => $bkm['uri'],
148 'description' => $bkm['note'], 148 'description' => $bkm['note'],
149 'private' => $private, 149 'private' => $private,
150 'linkdate'=> '',
151 'tags' => $bkm['tags'] 150 'tags' => $bkm['tags']
152 ); 151 );
153 152
@@ -161,25 +160,22 @@ class NetscapeBookmarkUtils
161 } 160 }
162 161
163 // Overwrite an existing link, keep its date 162 // Overwrite an existing link, keep its date
164 $newLink['linkdate'] = $existingLink['linkdate']; 163 $newLink['id'] = $existingLink['id'];
165 $linkDb[$existingLink['linkdate']] = $newLink; 164 $newLink['created'] = $existingLink['created'];
165 $newLink['updated'] = new DateTime();
166 $linkDb[$existingLink['id']] = $newLink;
166 $importCount++; 167 $importCount++;
167 $overwriteCount++; 168 $overwriteCount++;
168 continue; 169 continue;
169 } 170 }
170 171
171 // Add a new link 172 // Add a new link - @ used for UNIX timestamps
172 $newLinkDate = new DateTime('@'.strval($bkm['time'])); 173 $newLinkDate = new DateTime('@'.strval($bkm['time']));
173 while (!empty($linkDb[$newLinkDate->format(LinkDB::LINK_DATE_FORMAT)])) { 174 $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
174 // Ensure the date/time is not already used 175 $newLink['created'] = $newLinkDate;
175 // - this hack is necessary as the date/time acts as a primary key 176 $newLink['id'] = $linkDb->getNextId();
176 // - apply 1 second increments until an unused index is found 177 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
177 // See https://github.com/shaarli/Shaarli/issues/351 178 $linkDb[$newLink['id']] = $newLink;
178 $newLinkDate->add(new DateInterval('PT1S'));
179 }
180 $linkDbDate = $newLinkDate->format(LinkDB::LINK_DATE_FORMAT);
181 $newLink['linkdate'] = $linkDbDate;
182 $linkDb[$linkDbDate] = $newLink;
183 $importCount++; 179 $importCount++;
184 } 180 }
185 181
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 @@ class Updater
138 public function updateMethodRenameDashTags() 138 public function updateMethodRenameDashTags()
139 { 139 {
140 $linklist = $this->linkDB->filterSearch(); 140 $linklist = $this->linkDB->filterSearch();
141 foreach ($linklist as $link) { 141 foreach ($linklist as $key => $link) {
142 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']); 142 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
143 $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true))); 143 $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
144 $this->linkDB[$link['linkdate']] = $link; 144 $this->linkDB[$key] = $link;
145 } 145 }
146 $this->linkDB->save($this->conf->get('resource.page_cache')); 146 $this->linkDB->save($this->conf->get('resource.page_cache'));
147 return true; 147 return true;
@@ -215,6 +215,47 @@ class Updater
215 } 215 }
216 return true; 216 return true;
217 } 217 }
218
219 /**
220 * Update the database to use the new ID system, which replaces linkdate primary keys.
221 * Also, creation and update dates are now DateTime objects (done by LinkDB).
222 *
223 * Since this update is very sensitve (changing the whole database), the datastore will be
224 * automatically backed up into the file datastore.<datetime>.php.
225 *
226 * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
227 * which will be saved by this method.
228 *
229 * @return bool true if the update is successful, false otherwise.
230 */
231 public function updateMethodDatastoreIds()
232 {
233 // up to date database
234 if (isset($this->linkDB[0])) {
235 return true;
236 }
237
238 $save = $this->conf->get('resource.data_dir') .'/datastore.'. date('YmdHis') .'.php';
239 copy($this->conf->get('resource.datastore'), $save);
240
241 $links = array();
242 foreach ($this->linkDB as $offset => $value) {
243 $links[] = $value;
244 unset($this->linkDB[$offset]);
245 }
246 $links = array_reverse($links);
247 $cpt = 0;
248 foreach ($links as $l) {
249 unset($l['linkdate']);
250 $l['id'] = $cpt;
251 $this->linkDB[$cpt++] = $l;
252 }
253
254 $this->linkDB->save($this->conf->get('resource.page_cache'));
255 $this->linkDB->reorder();
256
257 return true;
258 }
218} 259}
219 260
220/** 261/**
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)
31 * - are NOT cryptographically secure (they CAN be forged) 31 * - are NOT cryptographically secure (they CAN be forged)
32 * 32 *
33 * In Shaarli, they are used as a tinyurl-like link to individual entries, 33 * In Shaarli, they are used as a tinyurl-like link to individual entries,
34 * e.g. smallHash('20111006_131924') --> yZH23w 34 * built once with the combination of the date and item ID.
35 * e.g. smallHash('20111006_131924' . 142) --> eaWxtQ
36 *
37 * @warning before v0.8.1, smallhashes were built only with the date,
38 * and their value has been preserved.
35 * 39 *
36 * @param string $text Create a hash from this text. 40 * @param string $text Create a hash from this text.
37 * 41 *