diff options
author | ArthurHoaro <arthur@hoa.ro> | 2016-11-28 15:30:17 +0100 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2016-12-12 03:02:01 +0100 |
commit | 29d108820f05615d5c36608fde86f64f0750531a (patch) | |
tree | d3d241f80ffbb8f26d3add96001bbf5f285689a1 | |
parent | bea80e43a3714663b0c32879f7bdf4fd19161b2e (diff) | |
download | Shaarli-29d108820f05615d5c36608fde86f64f0750531a.tar.gz Shaarli-29d108820f05615d5c36608fde86f64f0750531a.tar.zst Shaarli-29d108820f05615d5c36608fde86f64f0750531a.zip |
Link ID refactoring
Links now use an incremental unique numeric identifier.
This ID is persistent and must never change.
ArrayAccess is used to match the link ID with the array keys (see the comment in LinkDB for more details)
Key 'created' added, with creation date as a DateTime object. 'updated' is now also a DateTime.
-rw-r--r-- | application/LinkDB.php | 178 |
1 files changed, 135 insertions, 43 deletions
diff --git a/application/LinkDB.php b/application/LinkDB.php index c8b162b6..e429ab4f 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 |
@@ -27,6 +27,19 @@ | |||
27 | * - ArrayAccess: behaves like an associative array; | 27 | * - ArrayAccess: behaves like an associative array; |
28 | * - Countable: there is a count() method; | 28 | * - Countable: there is a count() method; |
29 | * - Iterator: usable in foreach () loops. | 29 | * - Iterator: usable in foreach () loops. |
30 | * | ||
31 | * ID mechanism: | ||
32 | * ArrayAccess is implemented in a way that will allow to access a link | ||
33 | * with the unique identifier ID directly with $link[ID]. | ||
34 | * Note that it's not the real key of the link array attribute. | ||
35 | * This mechanism is in place to have persistent link IDs, | ||
36 | * even though the internal array is reordered by date. | ||
37 | * Example: | ||
38 | * - DB: link #1 (2010-01-01) link #2 (2016-01-01) | ||
39 | * - Order: #2 #1 | ||
40 | * - Import links containing: link #3 (2013-01-01) | ||
41 | * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01) | ||
42 | * - Real order: #2 #3 #1 | ||
30 | */ | 43 | */ |
31 | class LinkDB implements Iterator, Countable, ArrayAccess | 44 | class LinkDB implements Iterator, Countable, ArrayAccess |
32 | { | 45 | { |
@@ -47,11 +60,17 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
47 | // - value: associative array (keys: title, description...) | 60 | // - value: associative array (keys: title, description...) |
48 | private $links; | 61 | private $links; |
49 | 62 | ||
50 | // List of all recorded URLs (key=url, value=linkdate) | 63 | // List of all recorded URLs (key=url, value=link offset) |
51 | // for fast reserve search (url-->linkdate) | 64 | // for fast reserve search (url-->link offset) |
52 | private $urls; | 65 | private $urls; |
53 | 66 | ||
54 | // List of linkdate keys (for the Iterator interface implementation) | 67 | /** |
68 | * @var array List of all links IDS mapped with their array offset. | ||
69 | * Map: id->offset. | ||
70 | */ | ||
71 | protected $ids; | ||
72 | |||
73 | // List of offset keys (for the Iterator interface implementation) | ||
55 | private $keys; | 74 | private $keys; |
56 | 75 | ||
57 | // Position in the $this->keys array (for the Iterator interface) | 76 | // Position in the $this->keys array (for the Iterator interface) |
@@ -121,14 +140,26 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
121 | if (!$this->loggedIn) { | 140 | if (!$this->loggedIn) { |
122 | die('You are not authorized to add a link.'); | 141 | die('You are not authorized to add a link.'); |
123 | } | 142 | } |
124 | if (empty($value['linkdate']) || empty($value['url'])) { | 143 | if (!isset($value['id']) || empty($value['url'])) { |
125 | die('Internal Error: A link should always have a linkdate and URL.'); | 144 | die('Internal Error: A link should always have an id and URL.'); |
126 | } | 145 | } |
127 | if (empty($offset)) { | 146 | if ((! empty($offset) && ! is_int($offset)) || ! is_int($value['id'])) { |
128 | die('You must specify a key.'); | 147 | die('You must specify an integer as a key.'); |
148 | } | ||
149 | if (! empty($offset) && $offset !== $value['id']) { | ||
150 | die('Array offset and link ID must be equal.'); | ||
151 | } | ||
152 | |||
153 | // If the link exists, we reuse the real offset, otherwise new entry | ||
154 | $existing = $this->getLinkOffset($offset); | ||
155 | if ($existing !== null) { | ||
156 | $offset = $existing; | ||
157 | } else { | ||
158 | $offset = count($this->links); | ||
129 | } | 159 | } |
130 | $this->links[$offset] = $value; | 160 | $this->links[$offset] = $value; |
131 | $this->urls[$value['url']]=$offset; | 161 | $this->urls[$value['url']] = $offset; |
162 | $this->ids[$value['id']] = $offset; | ||
132 | } | 163 | } |
133 | 164 | ||
134 | /** | 165 | /** |
@@ -136,7 +167,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
136 | */ | 167 | */ |
137 | public function offsetExists($offset) | 168 | public function offsetExists($offset) |
138 | { | 169 | { |
139 | return array_key_exists($offset, $this->links); | 170 | return array_key_exists($this->getLinkOffset($offset), $this->links); |
140 | } | 171 | } |
141 | 172 | ||
142 | /** | 173 | /** |
@@ -148,9 +179,11 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
148 | // TODO: raise an exception | 179 | // TODO: raise an exception |
149 | die('You are not authorized to delete a link.'); | 180 | die('You are not authorized to delete a link.'); |
150 | } | 181 | } |
151 | $url = $this->links[$offset]['url']; | 182 | $realOffset = $this->getLinkOffset($offset); |
183 | $url = $this->links[$realOffset]['url']; | ||
152 | unset($this->urls[$url]); | 184 | unset($this->urls[$url]); |
153 | unset($this->links[$offset]); | 185 | unset($this->ids[$realOffset]); |
186 | unset($this->links[$realOffset]); | ||
154 | } | 187 | } |
155 | 188 | ||
156 | /** | 189 | /** |
@@ -158,7 +191,8 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
158 | */ | 191 | */ |
159 | public function offsetGet($offset) | 192 | public function offsetGet($offset) |
160 | { | 193 | { |
161 | return isset($this->links[$offset]) ? $this->links[$offset] : null; | 194 | $realOffset = $this->getLinkOffset($offset); |
195 | return isset($this->links[$realOffset]) ? $this->links[$realOffset] : null; | ||
162 | } | 196 | } |
163 | 197 | ||
164 | /** | 198 | /** |
@@ -166,7 +200,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
166 | */ | 200 | */ |
167 | public function current() | 201 | public function current() |
168 | { | 202 | { |
169 | return $this->links[$this->keys[$this->position]]; | 203 | return $this[$this->keys[$this->position]]; |
170 | } | 204 | } |
171 | 205 | ||
172 | /** | 206 | /** |
@@ -192,8 +226,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
192 | */ | 226 | */ |
193 | public function rewind() | 227 | public function rewind() |
194 | { | 228 | { |
195 | $this->keys = array_keys($this->links); | 229 | $this->keys = array_keys($this->ids); |
196 | rsort($this->keys); | ||
197 | $this->position = 0; | 230 | $this->position = 0; |
198 | } | 231 | } |
199 | 232 | ||
@@ -219,6 +252,7 @@ class LinkDB implements Iterator, Countable, ArrayAccess | |||
219 | // Create a dummy database for example | 252 | // Create a dummy database for example |
220 | $this->links = array(); | 253 | $this->links = array(); |
221 | $link = array( | 254 | $link = array( |
255 | 'id' => 1, | ||
222 | 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', | 256 | 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', |
223 | 'url'=>'https://github.com/shaarli/Shaarli/wiki', | 257 | '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. | 258 | 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. |
@@ -227,20 +261,21 @@ To learn how to use Shaarli, consult the link "Help/documentation" at the bottom | |||
227 | 261 | ||
228 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', | 262 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', |
229 | 'private'=>0, | 263 | 'private'=>0, |
230 | 'linkdate'=> date('Ymd_His'), | 264 | 'created'=> new DateTime(), |
231 | 'tags'=>'opensource software' | 265 | 'tags'=>'opensource software' |
232 | ); | 266 | ); |
233 | $this->links[$link['linkdate']] = $link; | 267 | $this->links[1] = $link; |
234 | 268 | ||
235 | $link = array( | 269 | $link = array( |
270 | 'id' => 0, | ||
236 | 'title'=>'My secret stuff... - Pastebin.com', | 271 | 'title'=>'My secret stuff... - Pastebin.com', |
237 | 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', | 272 | 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', |
238 | 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', | 273 | 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', |
239 | 'private'=>1, | 274 | 'private'=>1, |
240 | 'linkdate'=> date('Ymd_His', strtotime('-1 minute')), | 275 | 'created'=> new DateTime('1 minute ago'), |
241 | 'tags'=>'secretstuff' | 276 | 'tags'=>'secretstuff' |
242 | ); | 277 | ); |
243 | $this->links[$link['linkdate']] = $link; | 278 | $this->links[0] = $link; |
244 | 279 | ||
245 | // Write database to disk | 280 | // Write database to disk |
246 | $this->write(); | 281 | $this->write(); |
@@ -251,7 +286,6 @@ You use the community supported version of the original Shaarli project, by Seba | |||
251 | */ | 286 | */ |
252 | private function read() | 287 | private function read() |
253 | { | 288 | { |
254 | |||
255 | // Public links are hidden and user not logged in => nothing to show | 289 | // Public links are hidden and user not logged in => nothing to show |
256 | if ($this->hidePublicLinks && !$this->loggedIn) { | 290 | if ($this->hidePublicLinks && !$this->loggedIn) { |
257 | $this->links = array(); | 291 | $this->links = array(); |
@@ -269,23 +303,13 @@ You use the community supported version of the original Shaarli project, by Seba | |||
269 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); | 303 | strlen(self::$phpPrefix), -strlen(self::$phpSuffix))))); |
270 | } | 304 | } |
271 | 305 | ||
272 | // If user is not logged in, filter private links. | 306 | $toremove = array(); |
273 | if (!$this->loggedIn) { | 307 | foreach ($this->links as $key => &$link) { |
274 | $toremove = array(); | 308 | if (! $this->loggedIn && $link['private'] != 0) { |
275 | foreach ($this->links as $link) { | 309 | // Transition for not upgraded databases. |
276 | if ($link['private'] != 0) { | 310 | $toremove[] = $key; |
277 | $toremove[] = $link['linkdate']; | 311 | continue; |
278 | } | ||
279 | } | ||
280 | foreach ($toremove as $linkdate) { | ||
281 | unset($this->links[$linkdate]); | ||
282 | } | 312 | } |
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 | 313 | ||
290 | // Sanitize data fields. | 314 | // Sanitize data fields. |
291 | sanitizeLink($link); | 315 | sanitizeLink($link); |
@@ -307,7 +331,23 @@ You use the community supported version of the original Shaarli project, by Seba | |||
307 | else { | 331 | else { |
308 | $link['real_url'] = $link['url']; | 332 | $link['real_url'] = $link['url']; |
309 | } | 333 | } |
334 | |||
335 | // To be able to load links before running the update, and prepare the update | ||
336 | if (! isset($link['created'])) { | ||
337 | $link['id'] = $link['linkdate']; | ||
338 | $link['created'] = DateTime::createFromFormat('Ymd_His', $link['linkdate']); | ||
339 | if (! empty($link['updated'])) { | ||
340 | $link['updated'] = DateTime::createFromFormat('Ymd_His', $link['updated']); | ||
341 | } | ||
342 | } | ||
343 | } | ||
344 | |||
345 | // If user is not logged in, filter private links. | ||
346 | foreach ($toremove as $offset) { | ||
347 | unset($this->links[$offset]); | ||
310 | } | 348 | } |
349 | |||
350 | $this->reorder(); | ||
311 | } | 351 | } |
312 | 352 | ||
313 | /** | 353 | /** |
@@ -430,7 +470,7 @@ You use the community supported version of the original Shaarli project, by Seba | |||
430 | $request = ''; | 470 | $request = ''; |
431 | } | 471 | } |
432 | 472 | ||
433 | $linkFilter = new LinkFilter($this->links); | 473 | $linkFilter = new LinkFilter($this); |
434 | return $linkFilter->filter($type, $request, $casesensitive, $privateonly); | 474 | return $linkFilter->filter($type, $request, $casesensitive, $privateonly); |
435 | } | 475 | } |
436 | 476 | ||
@@ -467,12 +507,64 @@ You use the community supported version of the original Shaarli project, by Seba | |||
467 | public function days() | 507 | public function days() |
468 | { | 508 | { |
469 | $linkDays = array(); | 509 | $linkDays = array(); |
470 | foreach (array_keys($this->links) as $day) { | 510 | foreach ($this->links as $link) { |
471 | $linkDays[substr($day, 0, 8)] = 0; | 511 | $linkDays[$link['created']->format('Ymd')] = 0; |
472 | } | 512 | } |
473 | $linkDays = array_keys($linkDays); | 513 | $linkDays = array_keys($linkDays); |
474 | sort($linkDays); | 514 | sort($linkDays); |
475 | 515 | ||
476 | return $linkDays; | 516 | return $linkDays; |
477 | } | 517 | } |
518 | |||
519 | /** | ||
520 | * Reorder links by creation date (newest first). | ||
521 | * | ||
522 | * Also update the urls and ids mapping arrays. | ||
523 | * | ||
524 | * @param string $order ASC|DESC | ||
525 | */ | ||
526 | public function reorder($order = 'DESC') | ||
527 | { | ||
528 | $order = $order === 'ASC' ? -1 : 1; | ||
529 | // Reorder array by dates. | ||
530 | usort($this->links, function($a, $b) use ($order) { | ||
531 | return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; | ||
532 | }); | ||
533 | |||
534 | $this->urls = array(); | ||
535 | $this->ids = array(); | ||
536 | foreach ($this->links as $key => $link) { | ||
537 | $this->urls[$link['url']] = $key; | ||
538 | $this->ids[$link['id']] = $key; | ||
539 | } | ||
540 | } | ||
541 | |||
542 | /** | ||
543 | * Return the next key for link creation. | ||
544 | * E.g. If the last ID is 597, the next will be 598. | ||
545 | * | ||
546 | * @return int next ID. | ||
547 | */ | ||
548 | public function getNextId() | ||
549 | { | ||
550 | if (!empty($this->ids)) { | ||
551 | return max(array_keys($this->ids)) + 1; | ||
552 | } | ||
553 | return 0; | ||
554 | } | ||
555 | |||
556 | /** | ||
557 | * Returns a link offset in links array from its unique ID. | ||
558 | * | ||
559 | * @param int $id Persistent ID of a link. | ||
560 | * | ||
561 | * @return int Real offset in local array, or null if doesn't exists. | ||
562 | */ | ||
563 | protected function getLinkOffset($id) | ||
564 | { | ||
565 | if (isset($this->ids[$id])) { | ||
566 | return $this->ids[$id]; | ||
567 | } | ||
568 | return null; | ||
569 | } | ||
478 | } | 570 | } |