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