aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/legacy
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-10-13 12:05:08 +0200
committerArthurHoaro <arthur@hoa.ro>2020-10-13 12:05:08 +0200
commitb6f678a5a1d15acf284ebcec16c905e976671ce1 (patch)
tree33c7da831482ed79c44896ef19c73c72ada84f2e /application/legacy
parentb14687036b9b800681197f51fdc47e62f0c88e2e (diff)
parent1c1520b6b98ab20201bfe15577782a52320339df (diff)
downloadShaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.tar.gz
Shaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.tar.zst
Shaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.zip
Merge branch 'v0.12' into latest
Diffstat (limited to 'application/legacy')
-rw-r--r--application/legacy/LegacyController.php162
-rw-r--r--application/legacy/LegacyLinkDB.php582
-rw-r--r--application/legacy/LegacyLinkFilter.php451
-rw-r--r--application/legacy/LegacyRouter.php63
-rw-r--r--application/legacy/LegacyUpdater.php618
-rw-r--r--application/legacy/UnknowLegacyRouteException.php9
6 files changed, 1885 insertions, 0 deletions
diff --git a/application/legacy/LegacyController.php b/application/legacy/LegacyController.php
new file mode 100644
index 00000000..826604e7
--- /dev/null
+++ b/application/legacy/LegacyController.php
@@ -0,0 +1,162 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Legacy;
6
7use Shaarli\Feed\FeedBuilder;
8use Shaarli\Front\Controller\Visitor\ShaarliVisitorController;
9use Slim\Http\Request;
10use Slim\Http\Response;
11
12/**
13 * We use this to maintain legacy routes, and redirect requests to the corresponding Slim route.
14 * Only public routes, and both `?addlink` and `?post` were kept here.
15 * Other routes will just display the linklist.
16 *
17 * @deprecated
18 */
19class LegacyController extends ShaarliVisitorController
20{
21 /** @var string[] Both `?post` and `?addlink` do not use `?do=` format. */
22 public const LEGACY_GET_ROUTES = [
23 'post',
24 'addlink',
25 ];
26
27 /**
28 * This method will call `$action` method, which will redirect to corresponding Slim route.
29 */
30 public function process(Request $request, Response $response, string $action): Response
31 {
32 if (!method_exists($this, $action)) {
33 throw new UnknowLegacyRouteException();
34 }
35
36 return $this->{$action}($request, $response);
37 }
38
39 /** Legacy route: ?post= */
40 public function post(Request $request, Response $response): Response
41 {
42 $route = '/admin/shaare';
43 $buildParameters = function (?array $parameters, bool $encode) {
44 if ($encode) {
45 $parameters = array_map('urlencode', $parameters);
46 }
47
48 return count($parameters) > 0 ? '?' . http_build_query($parameters) : '';
49 };
50
51
52 if (!$this->container->loginManager->isLoggedIn()) {
53 $parameters = $buildParameters($request->getQueryParams(), true);
54 return $this->redirect($response, '/login?returnurl='. $this->getBasePath() . $route . $parameters);
55 }
56
57 $parameters = $buildParameters($request->getQueryParams(), false);
58
59 return $this->redirect($response, $route . $parameters);
60 }
61
62 /** Legacy route: ?addlink= */
63 protected function addlink(Request $request, Response $response): Response
64 {
65 $route = '/admin/add-shaare';
66
67 if (!$this->container->loginManager->isLoggedIn()) {
68 return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route);
69 }
70
71 return $this->redirect($response, $route);
72 }
73
74 /** Legacy route: ?do=login */
75 protected function login(Request $request, Response $response): Response
76 {
77 $returnUrl = $request->getQueryParam('returnurl');
78
79 return $this->redirect($response, '/login' . ($returnUrl ? '?returnurl=' . $returnUrl : ''));
80 }
81
82 /** Legacy route: ?do=logout */
83 protected function logout(Request $request, Response $response): Response
84 {
85 return $this->redirect($response, '/admin/logout');
86 }
87
88 /** Legacy route: ?do=picwall */
89 protected function picwall(Request $request, Response $response): Response
90 {
91 return $this->redirect($response, '/picture-wall');
92 }
93
94 /** Legacy route: ?do=tagcloud */
95 protected function tagcloud(Request $request, Response $response): Response
96 {
97 return $this->redirect($response, '/tags/cloud');
98 }
99
100 /** Legacy route: ?do=taglist */
101 protected function taglist(Request $request, Response $response): Response
102 {
103 return $this->redirect($response, '/tags/list');
104 }
105
106 /** Legacy route: ?do=daily */
107 protected function daily(Request $request, Response $response): Response
108 {
109 $dayParam = !empty($request->getParam('day')) ? '?day=' . escape($request->getParam('day')) : '';
110
111 return $this->redirect($response, '/daily' . $dayParam);
112 }
113
114 /** Legacy route: ?do=rss */
115 protected function rss(Request $request, Response $response): Response
116 {
117 return $this->feed($request, $response, FeedBuilder::$FEED_RSS);
118 }
119
120 /** Legacy route: ?do=atom */
121 protected function atom(Request $request, Response $response): Response
122 {
123 return $this->feed($request, $response, FeedBuilder::$FEED_ATOM);
124 }
125
126 /** Legacy route: ?do=opensearch */
127 protected function opensearch(Request $request, Response $response): Response
128 {
129 return $this->redirect($response, '/open-search');
130 }
131
132 /** Legacy route: ?do=dailyrss */
133 protected function dailyrss(Request $request, Response $response): Response
134 {
135 return $this->redirect($response, '/daily-rss');
136 }
137
138 /** Legacy route: ?do=feed */
139 protected function feed(Request $request, Response $response, string $feedType): Response
140 {
141 $parameters = count($request->getQueryParams()) > 0 ? '?' . http_build_query($request->getQueryParams()) : '';
142
143 return $this->redirect($response, '/feed/' . $feedType . $parameters);
144 }
145
146 /** Legacy route: ?do=configure */
147 protected function configure(Request $request, Response $response): Response
148 {
149 $route = '/admin/configure';
150
151 if (!$this->container->loginManager->isLoggedIn()) {
152 return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route);
153 }
154
155 return $this->redirect($response, $route);
156 }
157
158 protected function getBasePath(): string
159 {
160 return $this->container->basePath ?: '';
161 }
162}
diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php
new file mode 100644
index 00000000..7bf76fd4
--- /dev/null
+++ b/application/legacy/LegacyLinkDB.php
@@ -0,0 +1,582 @@
1<?php
2
3namespace Shaarli\Legacy;
4
5use ArrayAccess;
6use Countable;
7use DateTime;
8use Iterator;
9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
10use Shaarli\Exceptions\IOException;
11use Shaarli\FileUtils;
12use Shaarli\Render\PageCacheManager;
13
14/**
15 * Data storage for bookmarks.
16 *
17 * This object behaves like an associative array.
18 *
19 * Example:
20 * $myLinks = new LinkDB();
21 * echo $myLinks[350]['title'];
22 * foreach ($myLinks as $link)
23 * echo $link['title'].' at url '.$link['url'].'; description:'.$link['description'];
24 *
25 * Available keys:
26 * - id: primary key, incremental integer identifier (persistent)
27 * - description: description of the entry
28 * - created: creation date of this entry, DateTime object.
29 * - updated: last modification date of this entry, DateTime object.
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
33 * - url URL of the link. Used for displayable bookmarks.
34 * Can be absolute or relative in the database but the relative bookmarks
35 * will be converted to absolute ones in templates.
36 * - real_url Raw URL in stored in the DB (absolute or relative).
37 * - shorturl Permalink smallhash
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.
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
53 * - Import bookmarks containing: link #3 (2013-01-01)
54 * - New DB: link #1 (2010-01-01) link #2 (2016-01-01) link #3 (2013-01-01)
55 * - Real order: #2 #3 #1
56 *
57 * @deprecated
58 */
59class LegacyLinkDB implements Iterator, Countable, ArrayAccess
60{
61 // Links are stored as a PHP serialized string
62 private $datastore;
63
64 // Link date storage format
65 const LINK_DATE_FORMAT = 'Ymd_His';
66
67 // List of bookmarks (associative array)
68 // - key: link date (e.g. "20110823_124546"),
69 // - value: associative array (keys: title, description...)
70 private $links;
71
72 // List of all recorded URLs (key=url, value=link offset)
73 // for fast reserve search (url-->link offset)
74 private $urls;
75
76 /**
77 * @var array List of all bookmarks IDS mapped with their array offset.
78 * Map: id->offset.
79 */
80 protected $ids;
81
82 // List of offset keys (for the Iterator interface implementation)
83 private $keys;
84
85 // Position in the $this->keys array (for the Iterator interface)
86 private $position;
87
88 // Is the user logged in? (used to filter private bookmarks)
89 private $loggedIn;
90
91 // Hide public bookmarks
92 private $hidePublicLinks;
93
94 /**
95 * Creates a new LinkDB
96 *
97 * Checks if the datastore exists; else, attempts to create a dummy one.
98 *
99 * @param string $datastore datastore file path.
100 * @param boolean $isLoggedIn is the user logged in?
101 * @param boolean $hidePublicLinks if true all bookmarks are private.
102 */
103 public function __construct(
104 $datastore,
105 $isLoggedIn,
106 $hidePublicLinks
107 ) {
108
109 $this->datastore = $datastore;
110 $this->loggedIn = $isLoggedIn;
111 $this->hidePublicLinks = $hidePublicLinks;
112 $this->check();
113 $this->read();
114 }
115
116 /**
117 * Countable - Counts elements of an object
118 */
119 public function count()
120 {
121 return count($this->links);
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"
130 if (!$this->loggedIn) {
131 die(t('You are not authorized to add a link.'));
132 }
133 if (!isset($value['id']) || empty($value['url'])) {
134 die(t('Internal Error: A link should always have an id and URL.'));
135 }
136 if (($offset !== null && !is_int($offset)) || !is_int($value['id'])) {
137 die(t('You must specify an integer as a key.'));
138 }
139 if ($offset !== null && $offset !== $value['id']) {
140 die(t('Array offset and link ID must be equal.'));
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);
149 }
150 $this->links[$offset] = $value;
151 $this->urls[$value['url']] = $offset;
152 $this->ids[$value['id']] = $offset;
153 }
154
155 /**
156 * ArrayAccess - Whether or not an offset exists
157 */
158 public function offsetExists($offset)
159 {
160 return array_key_exists($this->getLinkOffset($offset), $this->links);
161 }
162
163 /**
164 * ArrayAccess - Unsets an offset
165 */
166 public function offsetUnset($offset)
167 {
168 if (!$this->loggedIn) {
169 // TODO: raise an exception
170 die('You are not authorized to delete a link.');
171 }
172 $realOffset = $this->getLinkOffset($offset);
173 $url = $this->links[$realOffset]['url'];
174 unset($this->urls[$url]);
175 unset($this->ids[$realOffset]);
176 unset($this->links[$realOffset]);
177 }
178
179 /**
180 * ArrayAccess - Returns the value at specified offset
181 */
182 public function offsetGet($offset)
183 {
184 $realOffset = $this->getLinkOffset($offset);
185 return isset($this->links[$realOffset]) ? $this->links[$realOffset] : null;
186 }
187
188 /**
189 * Iterator - Returns the current element
190 */
191 public function current()
192 {
193 return $this[$this->keys[$this->position]];
194 }
195
196 /**
197 * Iterator - Returns the key of the current element
198 */
199 public function key()
200 {
201 return $this->keys[$this->position];
202 }
203
204 /**
205 * Iterator - Moves forward to next element
206 */
207 public function next()
208 {
209 ++$this->position;
210 }
211
212 /**
213 * Iterator - Rewinds the Iterator to the first element
214 *
215 * Entries are sorted by date (latest first)
216 */
217 public function rewind()
218 {
219 $this->keys = array_keys($this->ids);
220 $this->position = 0;
221 }
222
223 /**
224 * Iterator - Checks if current position is valid
225 */
226 public function valid()
227 {
228 return isset($this->keys[$this->position]);
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 */
236 private function check()
237 {
238 if (file_exists($this->datastore)) {
239 return;
240 }
241
242 // Create a dummy database for example
243 $this->links = array();
244 $link = array(
245 'id' => 1,
246 'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'),
247 'url' => 'https://shaarli.readthedocs.io',
248 'description' => t(
249 'Welcome to Shaarli! This is your first public bookmark. '
250 . 'To edit or delete me, you must first login.
251
252To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
253
254You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
255 ),
256 'private' => 0,
257 'created' => new DateTime(),
258 'tags' => 'opensource software',
259 'sticky' => false,
260 );
261 $link['shorturl'] = link_small_hash($link['created'], $link['id']);
262 $this->links[1] = $link;
263
264 $link = array(
265 'id' => 0,
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',
272 'sticky' => false,
273 );
274 $link['shorturl'] = link_small_hash($link['created'], $link['id']);
275 $this->links[0] = $link;
276
277 // Write database to disk
278 $this->write();
279 }
280
281 /**
282 * Reads database from disk to memory
283 */
284 private function read()
285 {
286 // Public bookmarks are hidden and user not logged in => nothing to show
287 if ($this->hidePublicLinks && !$this->loggedIn) {
288 $this->links = array();
289 return;
290 }
291
292 $this->urls = [];
293 $this->ids = [];
294 $this->links = FileUtils::readFlatDB($this->datastore, []);
295
296 $toremove = array();
297 foreach ($this->links as $key => &$link) {
298 if (!$this->loggedIn && $link['private'] != 0) {
299 // Transition for not upgraded databases.
300 unset($this->links[$key]);
301 continue;
302 }
303
304 // Sanitize data fields.
305 sanitizeLink($link);
306
307 // Remove private tags if the user is not logged in.
308 if (!$this->loggedIn) {
309 $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']);
310 }
311
312 $link['real_url'] = $link['url'];
313
314 $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
315
316 // To be able to load bookmarks before running the update, and prepare the update
317 if (!isset($link['created'])) {
318 $link['id'] = $link['linkdate'];
319 $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
320 if (!empty($link['updated'])) {
321 $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']);
322 }
323 $link['shorturl'] = smallHash($link['linkdate']);
324 }
325
326 $this->urls[$link['url']] = $key;
327 $this->ids[$link['id']] = $key;
328 }
329 }
330
331 /**
332 * Saves the database from memory to disk
333 *
334 * @throws IOException the datastore is not writable
335 */
336 private function write()
337 {
338 $this->reorder();
339 FileUtils::writeFlatDB($this->datastore, $this->links);
340 }
341
342 /**
343 * Saves the database from memory to disk
344 *
345 * @param string $pageCacheDir page cache directory
346 */
347 public function save($pageCacheDir)
348 {
349 if (!$this->loggedIn) {
350 // TODO: raise an Exception instead
351 die('You are not authorized to change the database.');
352 }
353
354 $this->write();
355
356 $pageCacheManager = new PageCacheManager($pageCacheDir, $this->loggedIn);
357 $pageCacheManager->invalidateCaches();
358 }
359
360 /**
361 * Returns the link for a given URL, or False if it does not exist.
362 *
363 * @param string $url URL to search for
364 *
365 * @return mixed the existing link if it exists, else 'false'
366 */
367 public function getLinkFromUrl($url)
368 {
369 if (isset($this->urls[$url])) {
370 return $this->links[$this->urls[$url]];
371 }
372 return false;
373 }
374
375 /**
376 * Returns the shaare corresponding to a smallHash.
377 *
378 * @param string $request QUERY_STRING server parameter.
379 *
380 * @return array $filtered array containing permalink data.
381 *
382 * @throws BookmarkNotFoundException if the smallhash is malformed or doesn't match any link.
383 */
384 public function filterHash($request)
385 {
386 $request = substr($request, 0, 6);
387 $linkFilter = new LegacyLinkFilter($this->links);
388 return $linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, $request);
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 */
398 public function filterDay($request)
399 {
400 $linkFilter = new LegacyLinkFilter($this->links);
401 return $linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, $request);
402 }
403
404 /**
405 * Filter bookmarks according to search parameters.
406 *
407 * @param array $filterRequest Search request content. Supported keys:
408 * - searchtags: list of tags
409 * - searchterm: term search
410 * @param bool $casesensitive Optional: Perform case sensitive filter
411 * @param string $visibility return only all/private/public bookmarks
412 * @param bool $untaggedonly return only untagged bookmarks
413 *
414 * @return array filtered bookmarks, all bookmarks if no suitable filter was provided.
415 */
416 public function filterSearch(
417 $filterRequest = array(),
418 $casesensitive = false,
419 $visibility = 'all',
420 $untaggedonly = false
421 ) {
422
423 // Filter link database according to parameters.
424 $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
425 $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
426
427 // Search tags + fullsearch - blank string parameter will return all bookmarks.
428 $type = LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT; // == "vuotext"
429 $request = [$searchtags, $searchterm];
430
431 $linkFilter = new LegacyLinkFilter($this);
432 return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly);
433 }
434
435 /**
436 * Returns the list tags appearing in the bookmarks with the given tags
437 *
438 * @param array $filteringTags tags selecting the bookmarks to consider
439 * @param string $visibility process only all/private/public bookmarks
440 *
441 * @return array tag => linksCount
442 */
443 public function linksCountPerTag($filteringTags = [], $visibility = 'all')
444 {
445 $links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
446 $tags = [];
447 $caseMapping = [];
448 foreach ($links as $link) {
449 foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
450 if (empty($tag)) {
451 continue;
452 }
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)]]++;
459 }
460 }
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 */
471 $keys = array_keys($tags);
472 $tmpTags = array_combine($keys, $keys);
473 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
474 return $tags;
475 }
476
477 /**
478 * Rename or delete a tag across all bookmarks.
479 *
480 * @param string $from Tag to rename
481 * @param string $to New tag. If none is provided, the from tag will be deleted
482 *
483 * @return array|bool List of altered bookmarks or false on error
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);
493 foreach ($linksToAlter as $key => &$value) {
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
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 {
515 $linkDays = array();
516 foreach ($this->links as $link) {
517 $linkDays[$link['created']->format('Ymd')] = 0;
518 }
519 $linkDays = array_keys($linkDays);
520 sort($linkDays);
521
522 return $linkDays;
523 }
524
525 /**
526 * Reorder bookmarks by creation date (newest first).
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.
536 usort($this->links, function ($a, $b) use ($order) {
537 if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) {
538 return $a['sticky'] ? -1 : 1;
539 }
540 if ($a['created'] == $b['created']) {
541 return $a['id'] < $b['id'] ? 1 * $order : -1 * $order;
542 }
543 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
544 });
545
546 $this->urls = [];
547 $this->ids = [];
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 /**
569 * Returns a link offset in bookmarks array from its unique ID.
570 *
571 * @param int $id Persistent ID of a link.
572 *
573 * @return int Real offset in local array, or null if doesn't exist.
574 */
575 protected function getLinkOffset($id)
576 {
577 if (isset($this->ids[$id])) {
578 return $this->ids[$id];
579 }
580 return null;
581 }
582}
diff --git a/application/legacy/LegacyLinkFilter.php b/application/legacy/LegacyLinkFilter.php
new file mode 100644
index 00000000..7cf93d60
--- /dev/null
+++ b/application/legacy/LegacyLinkFilter.php
@@ -0,0 +1,451 @@
1<?php
2
3namespace Shaarli\Legacy;
4
5use Exception;
6use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
7
8/**
9 * Class LinkFilter.
10 *
11 * Perform search and filter operation on link data list.
12 *
13 * @deprecated
14 */
15class LegacyLinkFilter
16{
17 /**
18 * @var string permalinks.
19 */
20 public static $FILTER_HASH = 'permalink';
21
22 /**
23 * @var string text search.
24 */
25 public static $FILTER_TEXT = 'fulltext';
26
27 /**
28 * @var string tag filter.
29 */
30 public static $FILTER_TAG = 'tags';
31
32 /**
33 * @var string filter by day.
34 */
35 public static $FILTER_DAY = 'FILTER_DAY';
36
37 /**
38 * @var string Allowed characters for hashtags (regex syntax).
39 */
40 public static $HASHTAG_CHARS = '\p{Pc}\p{N}\p{L}\p{Mn}';
41
42 /**
43 * @var LegacyLinkDB all available links.
44 */
45 private $links;
46
47 /**
48 * @param LegacyLinkDB $links initialization.
49 */
50 public function __construct($links)
51 {
52 $this->links = $links;
53 }
54
55 /**
56 * Filter links according to parameters.
57 *
58 * @param string $type Type of filter (eg. tags, permalink, etc.).
59 * @param mixed $request Filter content.
60 * @param bool $casesensitive Optional: Perform case sensitive filter if true.
61 * @param string $visibility Optional: return only all/private/public links
62 * @param string $untaggedonly Optional: return only untagged links. Applies only if $type includes FILTER_TAG
63 *
64 * @return array filtered link list.
65 */
66 public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false)
67 {
68 if (!in_array($visibility, ['all', 'public', 'private'])) {
69 $visibility = 'all';
70 }
71
72 switch ($type) {
73 case self::$FILTER_HASH:
74 return $this->filterSmallHash($request);
75 case self::$FILTER_TAG | self::$FILTER_TEXT: // == "vuotext"
76 $noRequest = empty($request) || (empty($request[0]) && empty($request[1]));
77 if ($noRequest) {
78 if ($untaggedonly) {
79 return $this->filterUntagged($visibility);
80 }
81 return $this->noFilter($visibility);
82 }
83 if ($untaggedonly) {
84 $filtered = $this->filterUntagged($visibility);
85 } else {
86 $filtered = $this->links;
87 }
88 if (!empty($request[0])) {
89 $filtered = (new LegacyLinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
90 }
91 if (!empty($request[1])) {
92 $filtered = (new LegacyLinkFilter($filtered))->filterFulltext($request[1], $visibility);
93 }
94 return $filtered;
95 case self::$FILTER_TEXT:
96 return $this->filterFulltext($request, $visibility);
97 case self::$FILTER_TAG:
98 if ($untaggedonly) {
99 return $this->filterUntagged($visibility);
100 } else {
101 return $this->filterTags($request, $casesensitive, $visibility);
102 }
103 case self::$FILTER_DAY:
104 return $this->filterDay($request);
105 default:
106 return $this->noFilter($visibility);
107 }
108 }
109
110 /**
111 * Unknown filter, but handle private only.
112 *
113 * @param string $visibility Optional: return only all/private/public links
114 *
115 * @return array filtered links.
116 */
117 private function noFilter($visibility = 'all')
118 {
119 if ($visibility === 'all') {
120 return $this->links;
121 }
122
123 $out = array();
124 foreach ($this->links as $key => $value) {
125 if ($value['private'] && $visibility === 'private') {
126 $out[$key] = $value;
127 } elseif (!$value['private'] && $visibility === 'public') {
128 $out[$key] = $value;
129 }
130 }
131
132 return $out;
133 }
134
135 /**
136 * Returns the shaare corresponding to a smallHash.
137 *
138 * @param string $smallHash permalink hash.
139 *
140 * @return array $filtered array containing permalink data.
141 *
142 * @throws BookmarkNotFoundException if the smallhash doesn't match any link.
143 */
144 private function filterSmallHash($smallHash)
145 {
146 $filtered = array();
147 foreach ($this->links as $key => $l) {
148 if ($smallHash == $l['shorturl']) {
149 // Yes, this is ugly and slow
150 $filtered[$key] = $l;
151 return $filtered;
152 }
153 }
154
155 if (empty($filtered)) {
156 throw new BookmarkNotFoundException();
157 }
158
159 return $filtered;
160 }
161
162 /**
163 * Returns the list of links corresponding to a full-text search
164 *
165 * Searches:
166 * - in the URLs, title and description;
167 * - are case-insensitive;
168 * - terms surrounded by quotes " are exact terms search.
169 * - terms starting with a dash - are excluded (except exact terms).
170 *
171 * Example:
172 * print_r($mydb->filterFulltext('hollandais'));
173 *
174 * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8')
175 * - allows to perform searches on Unicode text
176 * - see https://github.com/shaarli/Shaarli/issues/75 for examples
177 *
178 * @param string $searchterms search query.
179 * @param string $visibility Optional: return only all/private/public links.
180 *
181 * @return array search results.
182 */
183 private function filterFulltext($searchterms, $visibility = 'all')
184 {
185 if (empty($searchterms)) {
186 return $this->noFilter($visibility);
187 }
188
189 $filtered = array();
190 $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
191 $exactRegex = '/"([^"]+)"/';
192 // Retrieve exact search terms.
193 preg_match_all($exactRegex, $search, $exactSearch);
194 $exactSearch = array_values(array_filter($exactSearch[1]));
195
196 // Remove exact search terms to get AND terms search.
197 $explodedSearchAnd = explode(' ', trim(preg_replace($exactRegex, '', $search)));
198 $explodedSearchAnd = array_values(array_filter($explodedSearchAnd));
199
200 // Filter excluding terms and update andSearch.
201 $excludeSearch = array();
202 $andSearch = array();
203 foreach ($explodedSearchAnd as $needle) {
204 if ($needle[0] == '-' && strlen($needle) > 1) {
205 $excludeSearch[] = substr($needle, 1);
206 } else {
207 $andSearch[] = $needle;
208 }
209 }
210
211 $keys = array('title', 'description', 'url', 'tags');
212
213 // Iterate over every stored link.
214 foreach ($this->links as $id => $link) {
215 // ignore non private links when 'privatonly' is on.
216 if ($visibility !== 'all') {
217 if (!$link['private'] && $visibility === 'private') {
218 continue;
219 } elseif ($link['private'] && $visibility === 'public') {
220 continue;
221 }
222 }
223
224 // Concatenate link fields to search across fields.
225 // Adds a '\' separator for exact search terms.
226 $content = '';
227 foreach ($keys as $key) {
228 $content .= mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8') . '\\';
229 }
230
231 // Be optimistic
232 $found = true;
233
234 // First, we look for exact term search
235 for ($i = 0; $i < count($exactSearch) && $found; $i++) {
236 $found = strpos($content, $exactSearch[$i]) !== false;
237 }
238
239 // Iterate over keywords, if keyword is not found,
240 // no need to check for the others. We want all or nothing.
241 for ($i = 0; $i < count($andSearch) && $found; $i++) {
242 $found = strpos($content, $andSearch[$i]) !== false;
243 }
244
245 // Exclude terms.
246 for ($i = 0; $i < count($excludeSearch) && $found; $i++) {
247 $found = strpos($content, $excludeSearch[$i]) === false;
248 }
249
250 if ($found) {
251 $filtered[$id] = $link;
252 }
253 }
254
255 return $filtered;
256 }
257
258 /**
259 * generate a regex fragment out of a tag
260 *
261 * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
262 *
263 * @return string generated regex fragment
264 */
265 private static function tag2regex($tag)
266 {
267 $len = strlen($tag);
268 if (!$len || $tag === "-" || $tag === "*") {
269 // nothing to search, return empty regex
270 return '';
271 }
272 if ($tag[0] === "-") {
273 // query is negated
274 $i = 1; // use offset to start after '-' character
275 $regex = '(?!'; // create negative lookahead
276 } else {
277 $i = 0; // start at first character
278 $regex = '(?='; // use positive lookahead
279 }
280 $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
281 // iterate over string, separating it into placeholder and content
282 for (; $i < $len; $i++) {
283 if ($tag[$i] === '*') {
284 // placeholder found
285 $regex .= '[^ ]*?';
286 } else {
287 // regular characters
288 $offset = strpos($tag, '*', $i);
289 if ($offset === false) {
290 // no placeholder found, set offset to end of string
291 $offset = $len;
292 }
293 // subtract one, as we want to get before the placeholder or end of string
294 $offset -= 1;
295 // we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
296 $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
297 // move $i on
298 $i = $offset;
299 }
300 }
301 $regex .= '(?:$| ))'; // after the tag may only be a space or the end
302 return $regex;
303 }
304
305 /**
306 * Returns the list of links associated with a given list of tags
307 *
308 * You can specify one or more tags, separated by space or a comma, e.g.
309 * print_r($mydb->filterTags('linux programming'));
310 *
311 * @param string $tags list of tags separated by commas or blank spaces.
312 * @param bool $casesensitive ignore case if false.
313 * @param string $visibility Optional: return only all/private/public links.
314 *
315 * @return array filtered links.
316 */
317 public function filterTags($tags, $casesensitive = false, $visibility = 'all')
318 {
319 // get single tags (we may get passed an array, even though the docs say different)
320 $inputTags = $tags;
321 if (!is_array($tags)) {
322 // we got an input string, split tags
323 $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
324 }
325
326 if (!count($inputTags)) {
327 // no input tags
328 return $this->noFilter($visibility);
329 }
330
331 // build regex from all tags
332 $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
333 if (!$casesensitive) {
334 // make regex case insensitive
335 $re .= 'i';
336 }
337
338 // create resulting array
339 $filtered = array();
340
341 // iterate over each link
342 foreach ($this->links as $key => $link) {
343 // check level of visibility
344 // ignore non private links when 'privateonly' is on.
345 if ($visibility !== 'all') {
346 if (!$link['private'] && $visibility === 'private') {
347 continue;
348 } elseif ($link['private'] && $visibility === 'public') {
349 continue;
350 }
351 }
352 $search = $link['tags']; // build search string, start with tags of current link
353 if (strlen(trim($link['description'])) && strpos($link['description'], '#') !== false) {
354 // description given and at least one possible tag found
355 $descTags = array();
356 // find all tags in the form of #tag in the description
357 preg_match_all(
358 '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
359 $link['description'],
360 $descTags
361 );
362 if (count($descTags[1])) {
363 // there were some tags in the description, add them to the search string
364 $search .= ' ' . implode(' ', $descTags[1]);
365 }
366 };
367 // match regular expression with search string
368 if (!preg_match($re, $search)) {
369 // this entry does _not_ match our regex
370 continue;
371 }
372 $filtered[$key] = $link;
373 }
374 return $filtered;
375 }
376
377 /**
378 * Return only links without any tag.
379 *
380 * @param string $visibility return only all/private/public links.
381 *
382 * @return array filtered links.
383 */
384 public function filterUntagged($visibility)
385 {
386 $filtered = [];
387 foreach ($this->links as $key => $link) {
388 if ($visibility !== 'all') {
389 if (!$link['private'] && $visibility === 'private') {
390 continue;
391 } elseif ($link['private'] && $visibility === 'public') {
392 continue;
393 }
394 }
395
396 if (empty(trim($link['tags']))) {
397 $filtered[$key] = $link;
398 }
399 }
400
401 return $filtered;
402 }
403
404 /**
405 * Returns the list of articles for a given day, chronologically sorted
406 *
407 * Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g.
408 * print_r($mydb->filterDay('20120125'));
409 *
410 * @param string $day day to filter.
411 *
412 * @return array all link matching given day.
413 *
414 * @throws Exception if date format is invalid.
415 */
416 public function filterDay($day)
417 {
418 if (!checkDateFormat('Ymd', $day)) {
419 throw new Exception('Invalid date format');
420 }
421
422 $filtered = array();
423 foreach ($this->links as $key => $l) {
424 if ($l['created']->format('Ymd') == $day) {
425 $filtered[$key] = $l;
426 }
427 }
428
429 // sort by date ASC
430 return array_reverse($filtered, true);
431 }
432
433 /**
434 * Convert a list of tags (str) to an array. Also
435 * - handle case sensitivity.
436 * - accepts spaces commas as separator.
437 *
438 * @param string $tags string containing a list of tags.
439 * @param bool $casesensitive will convert everything to lowercase if false.
440 *
441 * @return array filtered tags string.
442 */
443 public static function tagsStrToArray($tags, $casesensitive)
444 {
445 // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
446 $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
447 $tagsOut = str_replace(',', ' ', $tagsOut);
448
449 return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
450 }
451}
diff --git a/application/legacy/LegacyRouter.php b/application/legacy/LegacyRouter.php
new file mode 100644
index 00000000..0449c7e1
--- /dev/null
+++ b/application/legacy/LegacyRouter.php
@@ -0,0 +1,63 @@
1<?php
2
3namespace Shaarli\Legacy;
4
5/**
6 * Class Router
7 *
8 * (only displayable pages here)
9 *
10 * @deprecated
11 */
12class LegacyRouter
13{
14 public static $AJAX_THUMB_UPDATE = 'ajax_thumb_update';
15
16 public static $PAGE_LOGIN = 'login';
17
18 public static $PAGE_PICWALL = 'picwall';
19
20 public static $PAGE_TAGCLOUD = 'tag.cloud';
21
22 public static $PAGE_TAGLIST = 'tag.list';
23
24 public static $PAGE_DAILY = 'daily';
25
26 public static $PAGE_FEED_ATOM = 'feed.atom';
27
28 public static $PAGE_FEED_RSS = 'feed.rss';
29
30 public static $PAGE_TOOLS = 'tools';
31
32 public static $PAGE_CHANGEPASSWORD = 'changepasswd';
33
34 public static $PAGE_CONFIGURE = 'configure';
35
36 public static $PAGE_CHANGETAG = 'changetag';
37
38 public static $PAGE_ADDLINK = 'addlink';
39
40 public static $PAGE_EDITLINK = 'editlink';
41
42 public static $PAGE_DELETELINK = 'delete_link';
43
44 public static $PAGE_CHANGE_VISIBILITY = 'change_visibility';
45
46 public static $PAGE_PINLINK = 'pin';
47
48 public static $PAGE_EXPORT = 'export';
49
50 public static $PAGE_IMPORT = 'import';
51
52 public static $PAGE_OPENSEARCH = 'opensearch';
53
54 public static $PAGE_LINKLIST = 'linklist';
55
56 public static $PAGE_PLUGINSADMIN = 'pluginadmin';
57
58 public static $PAGE_SAVE_PLUGINSADMIN = 'save_pluginadmin';
59
60 public static $PAGE_THUMBS_UPDATE = 'thumbs_update';
61
62 public static $GET_TOKEN = 'token';
63}
diff --git a/application/legacy/LegacyUpdater.php b/application/legacy/LegacyUpdater.php
new file mode 100644
index 00000000..0ab3a55b
--- /dev/null
+++ b/application/legacy/LegacyUpdater.php
@@ -0,0 +1,618 @@
1<?php
2
3namespace Shaarli\Legacy;
4
5use Exception;
6use RainTPL;
7use ReflectionClass;
8use ReflectionException;
9use ReflectionMethod;
10use Shaarli\ApplicationUtils;
11use Shaarli\Bookmark\Bookmark;
12use Shaarli\Bookmark\BookmarkArray;
13use Shaarli\Bookmark\BookmarkFilter;
14use Shaarli\Bookmark\BookmarkIO;
15use Shaarli\Bookmark\LinkDB;
16use Shaarli\Config\ConfigJson;
17use Shaarli\Config\ConfigManager;
18use Shaarli\Config\ConfigPhp;
19use Shaarli\Exceptions\IOException;
20use Shaarli\Thumbnailer;
21use Shaarli\Updater\Exception\UpdaterException;
22
23/**
24 * Class updater.
25 * Used to update stuff when a new Shaarli's version is reached.
26 * Update methods are ran only once, and the stored in a JSON file.
27 *
28 * @deprecated
29 */
30class LegacyUpdater
31{
32 /**
33 * @var array Updates which are already done.
34 */
35 protected $doneUpdates;
36
37 /**
38 * @var LegacyLinkDB instance.
39 */
40 protected $linkDB;
41
42 /**
43 * @var ConfigManager $conf Configuration Manager instance.
44 */
45 protected $conf;
46
47 /**
48 * @var bool True if the user is logged in, false otherwise.
49 */
50 protected $isLoggedIn;
51
52 /**
53 * @var array $_SESSION
54 */
55 protected $session;
56
57 /**
58 * @var ReflectionMethod[] List of current class methods.
59 */
60 protected $methods;
61
62 /**
63 * Object constructor.
64 *
65 * @param array $doneUpdates Updates which are already done.
66 * @param LegacyLinkDB $linkDB LinkDB instance.
67 * @param ConfigManager $conf Configuration Manager instance.
68 * @param boolean $isLoggedIn True if the user is logged in.
69 * @param array $session $_SESSION (by reference)
70 *
71 * @throws ReflectionException
72 */
73 public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
74 {
75 $this->doneUpdates = $doneUpdates;
76 $this->linkDB = $linkDB;
77 $this->conf = $conf;
78 $this->isLoggedIn = $isLoggedIn;
79 $this->session = &$session;
80
81 // Retrieve all update methods.
82 $class = new ReflectionClass($this);
83 $this->methods = $class->getMethods();
84 }
85
86 /**
87 * Run all new updates.
88 * Update methods have to start with 'updateMethod' and return true (on success).
89 *
90 * @return array An array containing ran updates.
91 *
92 * @throws UpdaterException If something went wrong.
93 */
94 public function update()
95 {
96 $updatesRan = array();
97
98 // If the user isn't logged in, exit without updating.
99 if ($this->isLoggedIn !== true) {
100 return $updatesRan;
101 }
102
103 if ($this->methods === null) {
104 throw new UpdaterException(t('Couldn\'t retrieve updater class methods.'));
105 }
106
107 foreach ($this->methods as $method) {
108 // Not an update method or already done, pass.
109 if (!startsWith($method->getName(), 'updateMethod')
110 || in_array($method->getName(), $this->doneUpdates)
111 ) {
112 continue;
113 }
114
115 try {
116 $method->setAccessible(true);
117 $res = $method->invoke($this);
118 // Update method must return true to be considered processed.
119 if ($res === true) {
120 $updatesRan[] = $method->getName();
121 }
122 } catch (Exception $e) {
123 throw new UpdaterException($method, $e);
124 }
125 }
126
127 $this->doneUpdates = array_merge($this->doneUpdates, $updatesRan);
128
129 return $updatesRan;
130 }
131
132 /**
133 * @return array Updates methods already processed.
134 */
135 public function getDoneUpdates()
136 {
137 return $this->doneUpdates;
138 }
139
140 /**
141 * Move deprecated options.php to config.php.
142 *
143 * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
144 * options.php is not supported anymore.
145 */
146 public function updateMethodMergeDeprecatedConfigFile()
147 {
148 if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
149 include $this->conf->get('resource.data_dir') . '/options.php';
150
151 // Load GLOBALS into config
152 $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
153 $allowedKeys[] = 'config';
154 foreach ($GLOBALS as $key => $value) {
155 if (in_array($key, $allowedKeys)) {
156 $this->conf->set($key, $value);
157 }
158 }
159 $this->conf->write($this->isLoggedIn);
160 unlink($this->conf->get('resource.data_dir') . '/options.php');
161 }
162
163 return true;
164 }
165
166 /**
167 * Move old configuration in PHP to the new config system in JSON format.
168 *
169 * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
170 * It will also convert legacy setting keys to the new ones.
171 */
172 public function updateMethodConfigToJson()
173 {
174 // JSON config already exists, nothing to do.
175 if ($this->conf->getConfigIO() instanceof ConfigJson) {
176 return true;
177 }
178
179 $configPhp = new ConfigPhp();
180 $configJson = new ConfigJson();
181 $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
182 rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
183 $this->conf->setConfigIO($configJson);
184 $this->conf->reload();
185
186 $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
187 foreach (ConfigPhp::$ROOT_KEYS as $key) {
188 $this->conf->set($legacyMap[$key], $oldConfig[$key]);
189 }
190
191 // Set sub config keys (config and plugins)
192 $subConfig = array('config', 'plugins');
193 foreach ($subConfig as $sub) {
194 foreach ($oldConfig[$sub] as $key => $value) {
195 if (isset($legacyMap[$sub . '.' . $key])) {
196 $configKey = $legacyMap[$sub . '.' . $key];
197 } else {
198 $configKey = $sub . '.' . $key;
199 }
200 $this->conf->set($configKey, $value);
201 }
202 }
203
204 try {
205 $this->conf->write($this->isLoggedIn);
206 return true;
207 } catch (IOException $e) {
208 error_log($e->getMessage());
209 return false;
210 }
211 }
212
213 /**
214 * Escape settings which have been manually escaped in every request in previous versions:
215 * - general.title
216 * - general.header_link
217 * - redirector.url
218 *
219 * @return bool true if the update is successful, false otherwise.
220 */
221 public function updateMethodEscapeUnescapedConfig()
222 {
223 try {
224 $this->conf->set('general.title', escape($this->conf->get('general.title')));
225 $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
226 $this->conf->write($this->isLoggedIn);
227 } catch (Exception $e) {
228 error_log($e->getMessage());
229 return false;
230 }
231 return true;
232 }
233
234 /**
235 * Update the database to use the new ID system, which replaces linkdate primary keys.
236 * Also, creation and update dates are now DateTime objects (done by LinkDB).
237 *
238 * Since this update is very sensitve (changing the whole database), the datastore will be
239 * automatically backed up into the file datastore.<datetime>.php.
240 *
241 * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
242 * which will be saved by this method.
243 *
244 * @return bool true if the update is successful, false otherwise.
245 */
246 public function updateMethodDatastoreIds()
247 {
248 $first = 'update';
249 foreach ($this->linkDB as $key => $link) {
250 $first = $key;
251 break;
252 }
253
254 // up to date database
255 if (is_int($first)) {
256 return true;
257 }
258
259 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
260 copy($this->conf->get('resource.datastore'), $save);
261
262 $links = array();
263 foreach ($this->linkDB as $offset => $value) {
264 $links[] = $value;
265 unset($this->linkDB[$offset]);
266 }
267 $links = array_reverse($links);
268 $cpt = 0;
269 foreach ($links as $l) {
270 unset($l['linkdate']);
271 $l['id'] = $cpt;
272 $this->linkDB[$cpt++] = $l;
273 }
274
275 $this->linkDB->save($this->conf->get('resource.page_cache'));
276 $this->linkDB->reorder();
277
278 return true;
279 }
280
281 /**
282 * Rename tags starting with a '-' to work with tag exclusion search.
283 */
284 public function updateMethodRenameDashTags()
285 {
286 $linklist = $this->linkDB->filterSearch();
287 foreach ($linklist as $key => $link) {
288 $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
289 $link['tags'] = implode(' ', array_unique(BookmarkFilter::tagsStrToArray($link['tags'], true)));
290 $this->linkDB[$key] = $link;
291 }
292 $this->linkDB->save($this->conf->get('resource.page_cache'));
293 return true;
294 }
295
296 /**
297 * Initialize API settings:
298 * - api.enabled: true
299 * - api.secret: generated secret
300 */
301 public function updateMethodApiSettings()
302 {
303 if ($this->conf->exists('api.secret')) {
304 return true;
305 }
306
307 $this->conf->set('api.enabled', true);
308 $this->conf->set(
309 'api.secret',
310 generate_api_secret(
311 $this->conf->get('credentials.login'),
312 $this->conf->get('credentials.salt')
313 )
314 );
315 $this->conf->write($this->isLoggedIn);
316 return true;
317 }
318
319 /**
320 * New setting: theme name. If the default theme is used, nothing to do.
321 *
322 * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
323 * and the current theme is set as default in the theme setting.
324 *
325 * @return bool true if the update is successful, false otherwise.
326 */
327 public function updateMethodDefaultTheme()
328 {
329 // raintpl_tpl isn't the root template directory anymore.
330 // We run the update only if this folder still contains the template files.
331 $tplDir = $this->conf->get('resource.raintpl_tpl');
332 $tplFile = $tplDir . '/linklist.html';
333 if (!file_exists($tplFile)) {
334 return true;
335 }
336
337 $parent = dirname($tplDir);
338 $this->conf->set('resource.raintpl_tpl', $parent);
339 $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
340 $this->conf->write($this->isLoggedIn);
341
342 // Dependency injection gore
343 RainTPL::$tpl_dir = $tplDir;
344
345 return true;
346 }
347
348 /**
349 * Move the file to inc/user.css to data/user.css.
350 *
351 * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
352 *
353 * @return bool true if the update is successful, false otherwise.
354 */
355 public function updateMethodMoveUserCss()
356 {
357 if (!is_file('inc/user.css')) {
358 return true;
359 }
360
361 return rename('inc/user.css', 'data/user.css');
362 }
363
364 /**
365 * * `markdown_escape` is a new setting, set to true as default.
366 *
367 * If the markdown plugin was already enabled, escaping is disabled to avoid
368 * breaking existing entries.
369 */
370 public function updateMethodEscapeMarkdown()
371 {
372 if ($this->conf->exists('security.markdown_escape')) {
373 return true;
374 }
375
376 if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
377 $this->conf->set('security.markdown_escape', false);
378 } else {
379 $this->conf->set('security.markdown_escape', true);
380 }
381 $this->conf->write($this->isLoggedIn);
382
383 return true;
384 }
385
386 /**
387 * Add 'http://' to Piwik URL the setting is set.
388 *
389 * @return bool true if the update is successful, false otherwise.
390 */
391 public function updateMethodPiwikUrl()
392 {
393 if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
394 return true;
395 }
396
397 $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL'));
398 $this->conf->write($this->isLoggedIn);
399
400 return true;
401 }
402
403 /**
404 * Use ATOM feed as default.
405 */
406 public function updateMethodAtomDefault()
407 {
408 if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) {
409 return true;
410 }
411
412 $this->conf->set('feed.show_atom', true);
413 $this->conf->write($this->isLoggedIn);
414
415 return true;
416 }
417
418 /**
419 * Update updates.check_updates_branch setting.
420 *
421 * If the current major version digit matches the latest branch
422 * major version digit, we set the branch to `latest`,
423 * otherwise we'll check updates on the `stable` branch.
424 *
425 * No update required for the dev version.
426 *
427 * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
428 *
429 * FIXME! This needs to be removed when we switch to first digit major version
430 * instead of the second one since the versionning process will change.
431 */
432 public function updateMethodCheckUpdateRemoteBranch()
433 {
434 if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
435 return true;
436 }
437
438 // Get latest branch major version digit
439 $latestVersion = ApplicationUtils::getLatestGitVersionCode(
440 'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
441 5
442 );
443 if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
444 return false;
445 }
446 $latestMajor = $matches[1];
447
448 // Get current major version digit
449 preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
450 $currentMajor = $matches[1];
451
452 if ($currentMajor === $latestMajor) {
453 $branch = 'latest';
454 } else {
455 $branch = 'stable';
456 }
457 $this->conf->set('updates.check_updates_branch', $branch);
458 $this->conf->write($this->isLoggedIn);
459 return true;
460 }
461
462 /**
463 * Reset history store file due to date format change.
464 */
465 public function updateMethodResetHistoryFile()
466 {
467 if (is_file($this->conf->get('resource.history'))) {
468 unlink($this->conf->get('resource.history'));
469 }
470 return true;
471 }
472
473 /**
474 * Save the datastore -> the link order is now applied when bookmarks are saved.
475 */
476 public function updateMethodReorderDatastore()
477 {
478 $this->linkDB->save($this->conf->get('resource.page_cache'));
479 return true;
480 }
481
482 /**
483 * Change privateonly session key to visibility.
484 */
485 public function updateMethodVisibilitySession()
486 {
487 if (isset($_SESSION['privateonly'])) {
488 unset($_SESSION['privateonly']);
489 $_SESSION['visibility'] = 'private';
490 }
491 return true;
492 }
493
494 /**
495 * Add download size and timeout to the configuration file
496 *
497 * @return bool true if the update is successful, false otherwise.
498 */
499 public function updateMethodDownloadSizeAndTimeoutConf()
500 {
501 if ($this->conf->exists('general.download_max_size')
502 && $this->conf->exists('general.download_timeout')
503 ) {
504 return true;
505 }
506
507 if (!$this->conf->exists('general.download_max_size')) {
508 $this->conf->set('general.download_max_size', 1024 * 1024 * 4);
509 }
510
511 if (!$this->conf->exists('general.download_timeout')) {
512 $this->conf->set('general.download_timeout', 30);
513 }
514
515 $this->conf->write($this->isLoggedIn);
516 return true;
517 }
518
519 /**
520 * * Move thumbnails management to WebThumbnailer, coming with new settings.
521 */
522 public function updateMethodWebThumbnailer()
523 {
524 if ($this->conf->exists('thumbnails.mode')) {
525 return true;
526 }
527
528 $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
529 $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
530 $this->conf->set('thumbnails.width', 125);
531 $this->conf->set('thumbnails.height', 90);
532 $this->conf->remove('thumbnail');
533 $this->conf->write(true);
534
535 if ($thumbnailsEnabled) {
536 $this->session['warnings'][] = t(
537 t('You have enabled or changed thumbnails mode.') .
538 '<a href="./admin/thumbnails">' . t('Please synchronize them.') . '</a>'
539 );
540 }
541
542 return true;
543 }
544
545 /**
546 * Set sticky = false on all bookmarks
547 *
548 * @return bool true if the update is successful, false otherwise.
549 */
550 public function updateMethodSetSticky()
551 {
552 foreach ($this->linkDB as $key => $link) {
553 if (isset($link['sticky'])) {
554 return true;
555 }
556 $link['sticky'] = false;
557 $this->linkDB[$key] = $link;
558 }
559
560 $this->linkDB->save($this->conf->get('resource.page_cache'));
561
562 return true;
563 }
564
565 /**
566 * Remove redirector settings.
567 */
568 public function updateMethodRemoveRedirector()
569 {
570 $this->conf->remove('redirector');
571 $this->conf->write(true);
572 return true;
573 }
574
575 /**
576 * Migrate the legacy arrays to Bookmark objects.
577 * Also make a backup of the datastore.
578 */
579 public function updateMethodMigrateDatabase()
580 {
581 $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '_1.php';
582 if (! copy($this->conf->get('resource.datastore'), $save)) {
583 die('Could not backup the datastore.');
584 }
585
586 $linksArray = new BookmarkArray();
587 foreach ($this->linkDB as $key => $link) {
588 $linksArray[$key] = (new Bookmark())->fromArray($link);
589 }
590 $linksIo = new BookmarkIO($this->conf);
591 $linksIo->write($linksArray);
592
593 return true;
594 }
595
596 /**
597 * Write the `formatter` setting in config file.
598 * Use markdown if the markdown plugin is enabled, the default one otherwise.
599 * Also remove markdown plugin setting as it is now integrated to the core.
600 */
601 public function updateMethodFormatterSetting()
602 {
603 if (!$this->conf->exists('formatter') || $this->conf->get('formatter') === 'default') {
604 $enabledPlugins = $this->conf->get('general.enabled_plugins');
605 if (($pos = array_search('markdown', $enabledPlugins)) !== false) {
606 $formatter = 'markdown';
607 unset($enabledPlugins[$pos]);
608 $this->conf->set('general.enabled_plugins', array_values($enabledPlugins));
609 } else {
610 $formatter = 'default';
611 }
612 $this->conf->set('formatter', $formatter);
613 $this->conf->write(true);
614 }
615
616 return true;
617 }
618}
diff --git a/application/legacy/UnknowLegacyRouteException.php b/application/legacy/UnknowLegacyRouteException.php
new file mode 100644
index 00000000..ae1518ad
--- /dev/null
+++ b/application/legacy/UnknowLegacyRouteException.php
@@ -0,0 +1,9 @@
1<?php
2
3declare(strict_types=1);
4
5namespace Shaarli\Legacy;
6
7class UnknowLegacyRouteException extends \Exception
8{
9}