]> git.immae.eu Git - github/shaarli/Shaarli.git/blame - application/bookmark/BookmarkFileService.php
Merge pull request #1697 from ArthurHoaro/feature/pagination
[github/shaarli/Shaarli.git] / application / bookmark / BookmarkFileService.php
CommitLineData
336a28fa
A
1<?php
2
efb7d21b 3declare(strict_types=1);
336a28fa
A
4
5namespace Shaarli\Bookmark;
6
efb7d21b 7use DateTime;
336a28fa 8use Exception;
fd1ddad9 9use malkusch\lock\mutex\Mutex;
336a28fa 10use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
d6e5f04d 11use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
336a28fa
A
12use Shaarli\Bookmark\Exception\EmptyDataStoreException;
13use Shaarli\Config\ConfigManager;
a39acb25 14use Shaarli\Formatter\BookmarkMarkdownFormatter;
336a28fa
A
15use Shaarli\History;
16use Shaarli\Legacy\LegacyLinkDB;
17use Shaarli\Legacy\LegacyUpdater;
b0428aa9 18use Shaarli\Render\PageCacheManager;
336a28fa
A
19use Shaarli\Updater\UpdaterUtils;
20
21/**
22 * Class BookmarksService
23 *
24 * This is the entry point to manipulate the bookmark DB.
25 * It manipulates loads links from a file data store containing all bookmarks.
26 *
27 * It also triggers the legacy format (bookmarks as arrays) migration.
28 */
29class BookmarkFileService implements BookmarkServiceInterface
30{
31 /** @var Bookmark[] instance */
32 protected $bookmarks;
33
34 /** @var BookmarkIO instance */
35 protected $bookmarksIO;
36
37 /** @var BookmarkFilter */
38 protected $bookmarkFilter;
39
40 /** @var ConfigManager instance */
41 protected $conf;
42
43 /** @var History instance */
44 protected $history;
45
b0428aa9
A
46 /** @var PageCacheManager instance */
47 protected $pageCacheManager;
48
336a28fa
A
49 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
50 protected $isLoggedIn;
51
fd1ddad9
A
52 /** @var Mutex */
53 protected $mutex;
54
336a28fa
A
55 /**
56 * @inheritDoc
57 */
9b8c0a45
A
58 public function __construct(
59 ConfigManager $conf,
60 History $history,
61 Mutex $mutex,
62 bool $isLoggedIn
63 ) {
336a28fa
A
64 $this->conf = $conf;
65 $this->history = $history;
fd1ddad9 66 $this->mutex = $mutex;
c4d5be53 67 $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
fd1ddad9 68 $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex);
336a28fa
A
69 $this->isLoggedIn = $isLoggedIn;
70
71 if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
72 $this->bookmarks = [];
73 } else {
74 try {
75 $this->bookmarks = $this->bookmarksIO->read();
53054b2b 76 } catch (EmptyDataStoreException | DatastoreNotInitializedException $e) {
336a28fa 77 $this->bookmarks = new BookmarkArray();
d6e5f04d 78
c4ad3d4f 79 if ($this->isLoggedIn) {
d6e5f04d
A
80 // Datastore file does not exists, we initialize it with default bookmarks.
81 if ($e instanceof DatastoreNotInitializedException) {
82 $this->initialize();
83 } else {
84 $this->save();
85 }
336a28fa
A
86 }
87 }
88
89 if (! $this->bookmarks instanceof BookmarkArray) {
90 $this->migrate();
91 exit(
53054b2b 92 'Your data store has been migrated, please reload the page.' . PHP_EOL .
336a28fa
A
93 'If this message keeps showing up, please delete data/updates.txt file.'
94 );
95 }
96 }
97
b3bd8c3e 98 $this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf);
336a28fa
A
99 }
100
101 /**
102 * @inheritDoc
103 */
9c04921a 104 public function findByHash(string $hash, string $privateKey = null): Bookmark
336a28fa
A
105 {
106 $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
107 // PHP 7.3 introduced array_key_first() to avoid this hack
108 $first = reset($bookmark);
53054b2b
A
109 if (
110 !$this->isLoggedIn
9c04921a
A
111 && $first->isPrivate()
112 && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key'))
113 ) {
156061d4 114 throw new BookmarkNotFoundException();
336a28fa
A
115 }
116
1a8ac737 117 return $first;
336a28fa
A
118 }
119
120 /**
121 * @inheritDoc
122 */
efb7d21b 123 public function findByUrl(string $url): ?Bookmark
336a28fa
A
124 {
125 return $this->bookmarks->getByUrl($url);
126 }
127
128 /**
129 * @inheritDoc
130 */
a8e210fa 131 public function search(
efb7d21b
A
132 array $request = [],
133 string $visibility = null,
134 bool $caseSensitive = false,
135 bool $untaggedOnly = false,
9b8c0a45
A
136 bool $ignoreSticky = false,
137 array $pagination = []
138 ): SearchResult {
336a28fa
A
139 if ($visibility === null) {
140 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
141 }
142
143 // Filter bookmark database according to parameters.
efb7d21b
A
144 $searchTags = isset($request['searchtags']) ? $request['searchtags'] : '';
145 $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : '';
336a28fa 146
a8e210fa
A
147 if ($ignoreSticky) {
148 $this->bookmarks->reorder('DESC', true);
149 }
150
9b8c0a45 151 $bookmarks = $this->bookmarkFilter->filter(
336a28fa 152 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
efb7d21b 153 [$searchTags, $searchTerm],
336a28fa
A
154 $caseSensitive,
155 $visibility,
156 $untaggedOnly
157 );
9b8c0a45
A
158
159 return SearchResult::getSearchResult(
160 $bookmarks,
161 $pagination['offset'] ?? 0,
162 $pagination['limit'] ?? null,
163 $pagination['allowOutOfBounds'] ?? false
164 );
336a28fa
A
165 }
166
167 /**
168 * @inheritDoc
169 */
efb7d21b 170 public function get(int $id, string $visibility = null): Bookmark
336a28fa
A
171 {
172 if (! isset($this->bookmarks[$id])) {
173 throw new BookmarkNotFoundException();
174 }
175
176 if ($visibility === null) {
a39acb25 177 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
336a28fa
A
178 }
179
180 $bookmark = $this->bookmarks[$id];
53054b2b
A
181 if (
182 ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
336a28fa
A
183 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
184 ) {
185 throw new Exception('Unauthorized');
186 }
187
188 return $bookmark;
189 }
190
191 /**
192 * @inheritDoc
193 */
efb7d21b 194 public function set(Bookmark $bookmark, bool $save = true): Bookmark
336a28fa 195 {
d6e5f04d 196 if (true !== $this->isLoggedIn) {
336a28fa
A
197 throw new Exception(t('You\'re not authorized to alter the datastore'));
198 }
336a28fa
A
199 if (! isset($this->bookmarks[$bookmark->getId()])) {
200 throw new BookmarkNotFoundException();
201 }
202 $bookmark->validate();
203
efb7d21b 204 $bookmark->setUpdated(new DateTime());
336a28fa
A
205 $this->bookmarks[$bookmark->getId()] = $bookmark;
206 if ($save === true) {
207 $this->save();
208 $this->history->updateLink($bookmark);
209 }
210 return $this->bookmarks[$bookmark->getId()];
211 }
212
213 /**
214 * @inheritDoc
215 */
efb7d21b 216 public function add(Bookmark $bookmark, bool $save = true): Bookmark
336a28fa 217 {
d6e5f04d 218 if (true !== $this->isLoggedIn) {
336a28fa
A
219 throw new Exception(t('You\'re not authorized to alter the datastore'));
220 }
efb7d21b 221 if (!empty($bookmark->getId())) {
336a28fa
A
222 throw new Exception(t('This bookmarks already exists'));
223 }
224 $bookmark->setId($this->bookmarks->getNextId());
225 $bookmark->validate();
226
227 $this->bookmarks[$bookmark->getId()] = $bookmark;
228 if ($save === true) {
229 $this->save();
230 $this->history->addLink($bookmark);
231 }
232 return $this->bookmarks[$bookmark->getId()];
233 }
234
235 /**
236 * @inheritDoc
237 */
efb7d21b 238 public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark
336a28fa 239 {
d6e5f04d 240 if (true !== $this->isLoggedIn) {
336a28fa
A
241 throw new Exception(t('You\'re not authorized to alter the datastore'));
242 }
336a28fa
A
243 if ($bookmark->getId() === null) {
244 return $this->add($bookmark, $save);
245 }
246 return $this->set($bookmark, $save);
247 }
248
249 /**
250 * @inheritDoc
251 */
efb7d21b 252 public function remove(Bookmark $bookmark, bool $save = true): void
336a28fa 253 {
d6e5f04d 254 if (true !== $this->isLoggedIn) {
336a28fa
A
255 throw new Exception(t('You\'re not authorized to alter the datastore'));
256 }
336a28fa
A
257 if (! isset($this->bookmarks[$bookmark->getId()])) {
258 throw new BookmarkNotFoundException();
259 }
260
261 unset($this->bookmarks[$bookmark->getId()]);
262 if ($save === true) {
263 $this->save();
264 $this->history->deleteLink($bookmark);
265 }
266 }
267
268 /**
269 * @inheritDoc
270 */
efb7d21b 271 public function exists(int $id, string $visibility = null): bool
336a28fa
A
272 {
273 if (! isset($this->bookmarks[$id])) {
274 return false;
275 }
276
277 if ($visibility === null) {
278 $visibility = $this->isLoggedIn ? 'all' : 'public';
279 }
280
281 $bookmark = $this->bookmarks[$id];
53054b2b
A
282 if (
283 ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
336a28fa
A
284 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
285 ) {
286 return false;
287 }
288
289 return true;
290 }
291
292 /**
293 * @inheritDoc
294 */
efb7d21b 295 public function count(string $visibility = null): int
336a28fa 296 {
9b8c0a45 297 return $this->search([], $visibility)->getResultCount();
336a28fa
A
298 }
299
300 /**
301 * @inheritDoc
302 */
efb7d21b 303 public function save(): void
336a28fa 304 {
d6e5f04d 305 if (true !== $this->isLoggedIn) {
336a28fa
A
306 // TODO: raise an Exception instead
307 die('You are not authorized to change the database.');
308 }
c4ad3d4f 309
336a28fa
A
310 $this->bookmarks->reorder();
311 $this->bookmarksIO->write($this->bookmarks);
b0428aa9 312 $this->pageCacheManager->invalidateCaches();
336a28fa
A
313 }
314
315 /**
316 * @inheritDoc
317 */
efb7d21b 318 public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
336a28fa 319 {
9b8c0a45 320 $searchResult = $this->search(['searchtags' => $filteringTags], $visibility);
336a28fa
A
321 $tags = [];
322 $caseMapping = [];
9b8c0a45 323 foreach ($searchResult->getBookmarks() as $bookmark) {
336a28fa 324 foreach ($bookmark->getTags() as $tag) {
53054b2b
A
325 if (
326 empty($tag)
a39acb25
A
327 || (! $this->isLoggedIn && startsWith($tag, '.'))
328 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
c79473bd 329 || in_array($tag, $filteringTags, true)
a39acb25 330 ) {
336a28fa
A
331 continue;
332 }
a39acb25 333
336a28fa
A
334 // The first case found will be displayed.
335 if (!isset($caseMapping[strtolower($tag)])) {
336 $caseMapping[strtolower($tag)] = $tag;
337 $tags[$caseMapping[strtolower($tag)]] = 0;
338 }
339 $tags[$caseMapping[strtolower($tag)]]++;
340 }
341 }
342
343 /*
344 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
345 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
346 *
347 * So we now use array_multisort() to sort tags by DESC occurrences,
348 * then ASC alphabetically for equal values.
349 *
350 * @see https://github.com/shaarli/Shaarli/issues/1142
351 */
352 $keys = array_keys($tags);
353 $tmpTags = array_combine($keys, $keys);
354 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
efb7d21b 355
336a28fa
A
356 return $tags;
357 }
358
359 /**
360 * @inheritDoc
361 */
36e6d88d
A
362 public function findByDate(
363 \DateTimeInterface $from,
364 \DateTimeInterface $to,
365 ?\DateTimeInterface &$previous,
366 ?\DateTimeInterface &$next
367 ): array {
368 $out = [];
369 $previous = null;
370 $next = null;
371
9b8c0a45 372 foreach ($this->search([], null, false, false, true)->getBookmarks() as $bookmark) {
36e6d88d
A
373 if ($to < $bookmark->getCreated()) {
374 $next = $bookmark->getCreated();
53054b2b 375 } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
36e6d88d
A
376 $out[] = $bookmark;
377 } else {
378 if ($previous !== null) {
379 break;
380 }
381 $previous = $bookmark->getCreated();
382 }
336a28fa 383 }
336a28fa 384
36e6d88d 385 return $out;
336a28fa
A
386 }
387
388 /**
389 * @inheritDoc
390 */
36e6d88d 391 public function getLatest(): ?Bookmark
336a28fa 392 {
9b8c0a45 393 foreach ($this->search([], null, false, false, true)->getBookmarks() as $bookmark) {
36e6d88d
A
394 return $bookmark;
395 }
27ddfec3 396
36e6d88d 397 return null;
336a28fa
A
398 }
399
400 /**
401 * @inheritDoc
402 */
efb7d21b 403 public function initialize(): void
336a28fa
A
404 {
405 $initializer = new BookmarkInitializer($this);
406 $initializer->initialize();
336a28fa 407
d6e5f04d
A
408 if (true === $this->isLoggedIn) {
409 $this->save();
410 }
c4ad3d4f
A
411 }
412
336a28fa
A
413 /**
414 * Handles migration to the new database format (BookmarksArray).
415 */
efb7d21b 416 protected function migrate(): void
336a28fa
A
417 {
418 $bookmarkDb = new LegacyLinkDB(
419 $this->conf->get('resource.datastore'),
420 true,
421 false
422 );
423 $updater = new LegacyUpdater(
b99e00f7 424 UpdaterUtils::readUpdatesFile($this->conf->get('resource.updates')),
336a28fa
A
425 $bookmarkDb,
426 $this->conf,
427 true
428 );
429 $newUpdates = $updater->update();
430 if (! empty($newUpdates)) {
b99e00f7 431 UpdaterUtils::writeUpdatesFile(
336a28fa
A
432 $this->conf->get('resource.updates'),
433 $updater->getDoneUpdates()
434 );
435 }
436 }
437}