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