]> git.immae.eu Git - github/shaarli/Shaarli.git/blame - application/legacy/LegacyLinkDB.php
Manually fix remaining PHPCS errors
[github/shaarli/Shaarli.git] / application / legacy / LegacyLinkDB.php
CommitLineData
ca74886f 1<?php
f3d2f257 2
336a28fa 3namespace Shaarli\Legacy;
f24896b2
V
4
5use ArrayAccess;
6use Countable;
7use DateTime;
8use Iterator;
336a28fa 9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
f3d2f257 10use Shaarli\Exceptions\IOException;
c2cd15da 11use Shaarli\Helper\FileUtils;
b0428aa9 12use Shaarli\Render\PageCacheManager;
f3d2f257 13
ca74886f 14/**
336a28fa 15 * Data storage for bookmarks.
ca74886f
V
16 *
17 * This object behaves like an associative array.
18 *
19 * Example:
20 * $myLinks = new LinkDB();
29d10882 21 * echo $myLinks[350]['title'];
ca74886f
V
22 * foreach ($myLinks as $link)
23 * echo $link['title'].' at url '.$link['url'].'; description:'.$link['description'];
24 *
25 * Available keys:
29d10882 26 * - id: primary key, incremental integer identifier (persistent)
ca74886f 27 * - description: description of the entry
29d10882
A
28 * - created: creation date of this entry, DateTime object.
29 * - updated: last modification date of this entry, DateTime object.
ca74886f
V
30 * - private: Is this link private? 0=no, other value=yes
31 * - tags: tags attached to this entry (separated by spaces)
32 * - title Title of the link
336a28fa
A
33 * - url URL of the link. Used for displayable bookmarks.
34 * Can be absolute or relative in the database but the relative bookmarks
520d2957
A
35 * will be converted to absolute ones in templates.
36 * - real_url Raw URL in stored in the DB (absolute or relative).
d592daea 37 * - shorturl Permalink smallhash
ca74886f
V
38 *
39 * Implements 3 interfaces:
40 * - ArrayAccess: behaves like an associative array;
41 * - Countable: there is a count() method;
42 * - Iterator: usable in foreach () loops.
29d10882
A
43 *
44 * ID mechanism:
45 * ArrayAccess is implemented in a way that will allow to access a link
46 * with the unique identifier ID directly with $link[ID].
47 * Note that it's not the real key of the link array attribute.
48 * This mechanism is in place to have persistent link IDs,
49 * even though the internal array is reordered by date.
50 * Example:
51 * - DB: link #1 (2010-01-01) link #2 (2016-01-01)
52 * - Order: #2 #1
336a28fa 53 * - Import bookmarks containing: link #3 (2013-01-01)
29d10882
A
54 * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01)
55 * - Real order: #2 #3 #1
336a28fa
A
56 *
57 * @deprecated
ca74886f 58 */
336a28fa 59class LegacyLinkDB implements Iterator, Countable, ArrayAccess
ca74886f 60{
9c8752a2 61 // Links are stored as a PHP serialized string
628b97cb 62 private $datastore;
9c8752a2 63
205a4277 64 // Link date storage format
b99e00f7 65 public const LINK_DATE_FORMAT = 'Ymd_His';
205a4277 66
336a28fa 67 // List of bookmarks (associative array)
ca74886f
V
68 // - key: link date (e.g. "20110823_124546"),
69 // - value: associative array (keys: title, description...)
628b97cb 70 private $links;
ca74886f 71
29d10882
A
72 // List of all recorded URLs (key=url, value=link offset)
73 // for fast reserve search (url-->link offset)
628b97cb 74 private $urls;
ca74886f 75
29d10882 76 /**
336a28fa 77 * @var array List of all bookmarks IDS mapped with their array offset.
29d10882
A
78 * Map: id->offset.
79 */
80 protected $ids;
81
82 // List of offset keys (for the Iterator interface implementation)
628b97cb 83 private $keys;
ca74886f 84
628b97cb
V
85 // Position in the $this->keys array (for the Iterator interface)
86 private $position;
ca74886f 87
336a28fa 88 // Is the user logged in? (used to filter private bookmarks)
628b97cb 89 private $loggedIn;
ca74886f 90
336a28fa 91 // Hide public bookmarks
628b97cb 92 private $hidePublicLinks;
9f15ca9e 93
ca74886f
V
94 /**
95 * Creates a new LinkDB
96 *
97 * Checks if the datastore exists; else, attempts to create a dummy one.
98 *
6696729b
V
99 * @param string $datastore datastore file path.
100 * @param boolean $isLoggedIn is the user logged in?
336a28fa 101 * @param boolean $hidePublicLinks if true all bookmarks are private.
ca74886f 102 */
735ed4a9 103 public function __construct(
043eae70
A
104 $datastore,
105 $isLoggedIn,
520d2957 106 $hidePublicLinks
f211e417 107 ) {
9f962705 108
628b97cb
V
109 $this->datastore = $datastore;
110 $this->loggedIn = $isLoggedIn;
111 $this->hidePublicLinks = $hidePublicLinks;
f21abf32
V
112 $this->check();
113 $this->read();
ca74886f
V
114 }
115
116 /**
117 * Countable - Counts elements of an object
118 */
119 public function count()
120 {
628b97cb 121 return count($this->links);
ca74886f
V
122 }
123
124 /**
125 * ArrayAccess - Assigns a value to the specified offset
126 */
127 public function offsetSet($offset, $value)
128 {
129 // TODO: use exceptions instead of "die"
628b97cb 130 if (!$this->loggedIn) {
12266213 131 die(t('You are not authorized to add a link.'));
ca74886f 132 }
29d10882 133 if (!isset($value['id']) || empty($value['url'])) {
12266213 134 die(t('Internal Error: A link should always have an id and URL.'));
ca74886f 135 }
f24896b2 136 if (($offset !== null && !is_int($offset)) || !is_int($value['id'])) {
12266213 137 die(t('You must specify an integer as a key.'));
29d10882 138 }
bc5f1597 139 if ($offset !== null && $offset !== $value['id']) {
12266213 140 die(t('Array offset and link ID must be equal.'));
29d10882
A
141 }
142
143 // If the link exists, we reuse the real offset, otherwise new entry
144 $existing = $this->getLinkOffset($offset);
145 if ($existing !== null) {
146 $offset = $existing;
147 } else {
148 $offset = count($this->links);
ca74886f 149 }
628b97cb 150 $this->links[$offset] = $value;
29d10882
A
151 $this->urls[$value['url']] = $offset;
152 $this->ids[$value['id']] = $offset;
ca74886f
V
153 }
154
155 /**
156 * ArrayAccess - Whether or not an offset exists
157 */
158 public function offsetExists($offset)
159 {
29d10882 160 return array_key_exists($this->getLinkOffset($offset), $this->links);
ca74886f
V
161 }
162
163 /**
164 * ArrayAccess - Unsets an offset
165 */
166 public function offsetUnset($offset)
167 {
628b97cb 168 if (!$this->loggedIn) {
ca74886f
V
169 // TODO: raise an exception
170 die('You are not authorized to delete a link.');
171 }
29d10882
A
172 $realOffset = $this->getLinkOffset($offset);
173 $url = $this->links[$realOffset]['url'];
628b97cb 174 unset($this->urls[$url]);
29d10882
A
175 unset($this->ids[$realOffset]);
176 unset($this->links[$realOffset]);
ca74886f
V
177 }
178
179 /**
180 * ArrayAccess - Returns the value at specified offset
181 */
182 public function offsetGet($offset)
183 {
29d10882
A
184 $realOffset = $this->getLinkOffset($offset);
185 return isset($this->links[$realOffset]) ? $this->links[$realOffset] : null;
ca74886f
V
186 }
187
188 /**
189 * Iterator - Returns the current element
190 */
735ed4a9 191 public function current()
ca74886f 192 {
29d10882 193 return $this[$this->keys[$this->position]];
ca74886f
V
194 }
195
196 /**
197 * Iterator - Returns the key of the current element
198 */
735ed4a9 199 public function key()
ca74886f 200 {
628b97cb 201 return $this->keys[$this->position];
ca74886f
V
202 }
203
204 /**
205 * Iterator - Moves forward to next element
206 */
735ed4a9 207 public function next()
ca74886f 208 {
628b97cb 209 ++$this->position;
ca74886f
V
210 }
211
212 /**
213 * Iterator - Rewinds the Iterator to the first element
214 *
215 * Entries are sorted by date (latest first)
216 */
735ed4a9 217 public function rewind()
ca74886f 218 {
29d10882 219 $this->keys = array_keys($this->ids);
628b97cb 220 $this->position = 0;
ca74886f
V
221 }
222
223 /**
224 * Iterator - Checks if current position is valid
225 */
735ed4a9 226 public function valid()
ca74886f 227 {
628b97cb 228 return isset($this->keys[$this->position]);
ca74886f
V
229 }
230
231 /**
232 * Checks if the DB directory and file exist
233 *
234 * If no DB file is found, creates a dummy DB.
235 */
f21abf32 236 private function check()
ca74886f 237 {
628b97cb 238 if (file_exists($this->datastore)) {
ca74886f
V
239 return;
240 }
241
242 // Create a dummy database for example
53054b2b
A
243 $this->links = [];
244 $link = [
29d10882 245 'id' => 1,
f24896b2
V
246 'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'),
247 'url' => 'https://shaarli.readthedocs.io',
248 'description' => t(
9d9f6d75 249 'Welcome to Shaarli! This is your first public bookmark. '
f24896b2 250 . 'To edit or delete me, you must first login.
598376d4 251
12266213 252To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
598376d4 253
9d9f6d75
V
254You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
255 ),
f24896b2
V
256 'private' => 0,
257 'created' => new DateTime(),
b790f900
A
258 'tags' => 'opensource software',
259 'sticky' => false,
53054b2b 260 ];
d592daea 261 $link['shorturl'] = link_small_hash($link['created'], $link['id']);
29d10882 262 $this->links[1] = $link;
ca74886f 263
53054b2b 264 $link = [
29d10882 265 'id' => 0,
f24896b2
V
266 'title' => t('My secret stuff... - Pastebin.com'),
267 'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
268 'description' => t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
269 'private' => 1,
270 'created' => new DateTime('1 minute ago'),
271 'tags' => 'secretstuff',
b790f900 272 'sticky' => false,
53054b2b 273 ];
d592daea 274 $link['shorturl'] = link_small_hash($link['created'], $link['id']);
29d10882 275 $this->links[0] = $link;
ca74886f
V
276
277 // Write database to disk
f21abf32 278 $this->write();
ca74886f
V
279 }
280
281 /**
282 * Reads database from disk to memory
283 */
f21abf32 284 private function read()
ca74886f 285 {
336a28fa 286 // Public bookmarks are hidden and user not logged in => nothing to show
628b97cb 287 if ($this->hidePublicLinks && !$this->loggedIn) {
53054b2b 288 $this->links = [];
578a84bd 289 return;
290 }
291
9ec0a611
A
292 $this->urls = [];
293 $this->ids = [];
b2306b0c 294 $this->links = FileUtils::readFlatDB($this->datastore, []);
ca74886f 295
53054b2b 296 $toremove = [];
29d10882 297 foreach ($this->links as $key => &$link) {
f24896b2 298 if (!$this->loggedIn && $link['private'] != 0) {
29d10882 299 // Transition for not upgraded databases.
9ec0a611 300 unset($this->links[$key]);
29d10882 301 continue;
ca74886f 302 }
195acf9f 303
510377d2 304 // Sanitize data fields.
90e5bd65 305 sanitizeLink($link);
195acf9f
A
306
307 // Remove private tags if the user is not logged in.
f24896b2 308 if (!$this->loggedIn) {
9866b408 309 $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']);
195acf9f
A
310 }
311
520d2957 312 $link['real_url'] = $link['url'];
29d10882 313
b790f900
A
314 $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
315
336a28fa 316 // To be able to load bookmarks before running the update, and prepare the update
f24896b2 317 if (!isset($link['created'])) {
29d10882 318 $link['id'] = $link['linkdate'];
d592daea 319 $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
f24896b2 320 if (!empty($link['updated'])) {
d592daea 321 $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']);
29d10882 322 }
d592daea 323 $link['shorturl'] = smallHash($link['linkdate']);
29d10882 324 }
29d10882 325
9ec0a611
A
326 $this->urls[$link['url']] = $key;
327 $this->ids[$link['id']] = $key;
5f85fcd8 328 }
ca74886f
V
329 }
330
2e28269b
V
331 /**
332 * Saves the database from memory to disk
333 *
334 * @throws IOException the datastore is not writable
335 */
f21abf32 336 private function write()
2e28269b 337 {
9ec0a611 338 $this->reorder();
b2306b0c 339 FileUtils::writeFlatDB($this->datastore, $this->links);
2e28269b
V
340 }
341
ca74886f
V
342 /**
343 * Saves the database from memory to disk
01e48f26
V
344 *
345 * @param string $pageCacheDir page cache directory
ca74886f 346 */
f21abf32 347 public function save($pageCacheDir)
ca74886f 348 {
628b97cb 349 if (!$this->loggedIn) {
ca74886f
V
350 // TODO: raise an Exception instead
351 die('You are not authorized to change the database.');
352 }
2e28269b 353
f21abf32 354 $this->write();
2e28269b 355
c4d5be53 356 $pageCacheManager = new PageCacheManager($pageCacheDir, $this->loggedIn);
b0428aa9 357 $pageCacheManager->invalidateCaches();
ca74886f
V
358 }
359
360 /**
361 * Returns the link for a given URL, or False if it does not exist.
ef591e7e
GV
362 *
363 * @param string $url URL to search for
364 *
365 * @return mixed the existing link if it exists, else 'false'
ca74886f
V
366 */
367 public function getLinkFromUrl($url)
368 {
628b97cb
V
369 if (isset($this->urls[$url])) {
370 return $this->links[$this->urls[$url]];
ca74886f
V
371 }
372 return false;
373 }
374
375 /**
528a6f8a 376 * Returns the shaare corresponding to a smallHash.
ca74886f 377 *
528a6f8a
A
378 * @param string $request QUERY_STRING server parameter.
379 *
380 * @return array $filtered array containing permalink data.
381 *
336a28fa 382 * @throws BookmarkNotFoundException if the smallhash is malformed or doesn't match any link.
528a6f8a
A
383 */
384 public function filterHash($request)
385 {
386 $request = substr($request, 0, 6);
336a28fa
A
387 $linkFilter = new LegacyLinkFilter($this->links);
388 return $linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, $request);
528a6f8a
A
389 }
390
391 /**
392 * Returns the list of articles for a given day.
393 *
394 * @param string $request day to filter. Format: YYYYMMDD.
395 *
396 * @return array list of shaare found.
397 */
f211e417
V
398 public function filterDay($request)
399 {
336a28fa
A
400 $linkFilter = new LegacyLinkFilter($this->links);
401 return $linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, $request);
528a6f8a
A
402 }
403
404 /**
336a28fa 405 * Filter bookmarks according to search parameters.
528a6f8a 406 *
6696729b 407 * @param array $filterRequest Search request content. Supported keys:
528a6f8a
A
408 * - searchtags: list of tags
409 * - searchterm: term search
6696729b 410 * @param bool $casesensitive Optional: Perform case sensitive filter
336a28fa
A
411 * @param string $visibility return only all/private/public bookmarks
412 * @param bool $untaggedonly return only untagged bookmarks
ca74886f 413 *
336a28fa 414 * @return array filtered bookmarks, all bookmarks if no suitable filter was provided.
ca74886f 415 */
9d9f6d75 416 public function filterSearch(
53054b2b 417 $filterRequest = [],
9d9f6d75
V
418 $casesensitive = false,
419 $visibility = 'all',
420 $untaggedonly = false
421 ) {
9f962705 422
528a6f8a 423 // Filter link database according to parameters.
7d86f40b
A
424 $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
425 $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
528a6f8a 426
336a28fa
A
427 // Search tags + fullsearch - blank string parameter will return all bookmarks.
428 $type = LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT; // == "vuotext"
7d86f40b 429 $request = [$searchtags, $searchterm];
528a6f8a 430
336a28fa 431 $linkFilter = new LegacyLinkFilter($this);
f210d94f 432 return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly);
ca74886f
V
433 }
434
435 /**
336a28fa 436 * Returns the list tags appearing in the bookmarks with the given tags
f8c5660d 437 *
336a28fa
A
438 * @param array $filteringTags tags selecting the bookmarks to consider
439 * @param string $visibility process only all/private/public bookmarks
f8c5660d
A
440 *
441 * @return array tag => linksCount
ca74886f 442 */
6ccd0b21 443 public function linksCountPerTag($filteringTags = [], $visibility = 'all')
ca74886f 444 {
f8c5660d
A
445 $links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
446 $tags = [];
447 $caseMapping = [];
6ccd0b21 448 foreach ($links as $link) {
4b35853d 449 foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
b1eb5d1d
A
450 if (empty($tag)) {
451 continue;
ca74886f 452 }
b1eb5d1d
A
453 // The first case found will be displayed.
454 if (!isset($caseMapping[strtolower($tag)])) {
455 $caseMapping[strtolower($tag)] = $tag;
456 $tags[$caseMapping[strtolower($tag)]] = 0;
457 }
458 $tags[$caseMapping[strtolower($tag)]]++;
ca74886f
V
459 }
460 }
f8c5660d
A
461
462 /*
463 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
464 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
465 *
466 * So we now use array_multisort() to sort tags by DESC occurrences,
467 * then ASC alphabetically for equal values.
468 *
469 * @see https://github.com/shaarli/Shaarli/issues/1142
470 */
f28396a2
A
471 $keys = array_keys($tags);
472 $tmpTags = array_combine($keys, $keys);
f28396a2 473 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
ca74886f
V
474 return $tags;
475 }
476
3b67b222 477 /**
336a28fa 478 * Rename or delete a tag across all bookmarks.
3b67b222
A
479 *
480 * @param string $from Tag to rename
6696729b 481 * @param string $to New tag. If none is provided, the from tag will be deleted
3b67b222 482 *
336a28fa 483 * @return array|bool List of altered bookmarks or false on error
3b67b222
A
484 */
485 public function renameTag($from, $to)
486 {
487 if (empty($from)) {
488 return false;
489 }
490 $delete = empty($to);
491 // True for case-sensitive tag search.
492 $linksToAlter = $this->filterSearch(['searchtags' => $from], true);
f211e417 493 foreach ($linksToAlter as $key => &$value) {
3b67b222
A
494 $tags = preg_split('/\s+/', trim($value['tags']));
495 if (($pos = array_search($from, $tags)) !== false) {
496 if ($delete) {
497 unset($tags[$pos]); // Remove tag.
498 } else {
499 $tags[$pos] = trim($to);
500 }
501 $value['tags'] = trim(implode(' ', array_unique($tags)));
502 $this[$value['id']] = $value;
503 }
504 }
505
506 return $linksToAlter;
507 }
508
ca74886f
V
509 /**
510 * Returns the list of days containing articles (oldest first)
511 * Output: An array containing days (in format YYYYMMDD).
512 */
513 public function days()
514 {
53054b2b 515 $linkDays = [];
29d10882
A
516 foreach ($this->links as $link) {
517 $linkDays[$link['created']->format('Ymd')] = 0;
ca74886f
V
518 }
519 $linkDays = array_keys($linkDays);
520 sort($linkDays);
510377d2 521
ca74886f
V
522 return $linkDays;
523 }
29d10882
A
524
525 /**
336a28fa 526 * Reorder bookmarks by creation date (newest first).
29d10882
A
527 *
528 * Also update the urls and ids mapping arrays.
529 *
530 * @param string $order ASC|DESC
531 */
532 public function reorder($order = 'DESC')
533 {
534 $order = $order === 'ASC' ? -1 : 1;
535 // Reorder array by dates.
f211e417 536 usort($this->links, function ($a, $b) use ($order) {
4154c25b
A
537 if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) {
538 return $a['sticky'] ? -1 : 1;
539 }
9f962705
A
540 if ($a['created'] == $b['created']) {
541 return $a['id'] < $b['id'] ? 1 * $order : -1 * $order;
542 }
29d10882
A
543 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
544 });
545
9ec0a611
A
546 $this->urls = [];
547 $this->ids = [];
29d10882
A
548 foreach ($this->links as $key => $link) {
549 $this->urls[$link['url']] = $key;
550 $this->ids[$link['id']] = $key;
551 }
552 }
553
554 /**
555 * Return the next key for link creation.
556 * E.g. If the last ID is 597, the next will be 598.
557 *
558 * @return int next ID.
559 */
560 public function getNextId()
561 {
562 if (!empty($this->ids)) {
563 return max(array_keys($this->ids)) + 1;
564 }
565 return 0;
566 }
567
568 /**
336a28fa 569 * Returns a link offset in bookmarks array from its unique ID.
29d10882
A
570 *
571 * @param int $id Persistent ID of a link.
572 *
d592daea 573 * @return int Real offset in local array, or null if doesn't exist.
29d10882
A
574 */
575 protected function getLinkOffset($id)
576 {
577 if (isset($this->ids[$id])) {
578 return $this->ids[$id];
579 }
580 return null;
581 }
ca74886f 582}