diff options
-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 | } |