]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - application/bookmark/BookmarkFileService.php
66248cc26537ebed1dd9f0f9ecbb8faeac38703d
[github/shaarli/Shaarli.git] / application / bookmark / BookmarkFileService.php
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Shaarli\Bookmark;
6
7 use DateTime;
8 use Exception;
9 use malkusch\lock\mutex\Mutex;
10 use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
11 use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
12 use Shaarli\Bookmark\Exception\EmptyDataStoreException;
13 use Shaarli\Config\ConfigManager;
14 use Shaarli\Formatter\BookmarkMarkdownFormatter;
15 use Shaarli\History;
16 use Shaarli\Legacy\LegacyLinkDB;
17 use Shaarli\Legacy\LegacyUpdater;
18 use Shaarli\Render\PageCacheManager;
19 use 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 */
29 class 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
46 /** @var PageCacheManager instance */
47 protected $pageCacheManager;
48
49 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
50 protected $isLoggedIn;
51
52 /** @var Mutex */
53 protected $mutex;
54
55 /**
56 * @inheritDoc
57 */
58 public function __construct(ConfigManager $conf, History $history, Mutex $mutex, bool $isLoggedIn)
59 {
60 $this->conf = $conf;
61 $this->history = $history;
62 $this->mutex = $mutex;
63 $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
64 $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex);
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();
72 } catch (EmptyDataStoreException | DatastoreNotInitializedException $e) {
73 $this->bookmarks = new BookmarkArray();
74
75 if ($this->isLoggedIn) {
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 }
82 }
83 }
84
85 if (! $this->bookmarks instanceof BookmarkArray) {
86 $this->migrate();
87 exit(
88 'Your data store has been migrated, please reload the page.' . PHP_EOL .
89 'If this message keeps showing up, please delete data/updates.txt file.'
90 );
91 }
92 }
93
94 $this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf);
95 }
96
97 /**
98 * @inheritDoc
99 */
100 public function findByHash(string $hash, string $privateKey = null): Bookmark
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);
105 if (
106 !$this->isLoggedIn
107 && $first->isPrivate()
108 && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key'))
109 ) {
110 throw new BookmarkNotFoundException();
111 }
112
113 return $first;
114 }
115
116 /**
117 * @inheritDoc
118 */
119 public function findByUrl(string $url): ?Bookmark
120 {
121 return $this->bookmarks->getByUrl($url);
122 }
123
124 /**
125 * @inheritDoc
126 */
127 public function search(
128 array $request = [],
129 string $visibility = null,
130 bool $caseSensitive = false,
131 bool $untaggedOnly = false,
132 bool $ignoreSticky = false
133 ) {
134 if ($visibility === null) {
135 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
136 }
137
138 // Filter bookmark database according to parameters.
139 $searchTags = isset($request['searchtags']) ? $request['searchtags'] : '';
140 $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : '';
141
142 if ($ignoreSticky) {
143 $this->bookmarks->reorder('DESC', true);
144 }
145
146 return $this->bookmarkFilter->filter(
147 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
148 [$searchTags, $searchTerm],
149 $caseSensitive,
150 $visibility,
151 $untaggedOnly
152 );
153 }
154
155 /**
156 * @inheritDoc
157 */
158 public function get(int $id, string $visibility = null): Bookmark
159 {
160 if (! isset($this->bookmarks[$id])) {
161 throw new BookmarkNotFoundException();
162 }
163
164 if ($visibility === null) {
165 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
166 }
167
168 $bookmark = $this->bookmarks[$id];
169 if (
170 ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
171 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
172 ) {
173 throw new Exception('Unauthorized');
174 }
175
176 return $bookmark;
177 }
178
179 /**
180 * @inheritDoc
181 */
182 public function set(Bookmark $bookmark, bool $save = true): Bookmark
183 {
184 if (true !== $this->isLoggedIn) {
185 throw new Exception(t('You\'re not authorized to alter the datastore'));
186 }
187 if (! isset($this->bookmarks[$bookmark->getId()])) {
188 throw new BookmarkNotFoundException();
189 }
190 $bookmark->validate();
191
192 $bookmark->setUpdated(new DateTime());
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 */
204 public function add(Bookmark $bookmark, bool $save = true): Bookmark
205 {
206 if (true !== $this->isLoggedIn) {
207 throw new Exception(t('You\'re not authorized to alter the datastore'));
208 }
209 if (!empty($bookmark->getId())) {
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 */
226 public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark
227 {
228 if (true !== $this->isLoggedIn) {
229 throw new Exception(t('You\'re not authorized to alter the datastore'));
230 }
231 if ($bookmark->getId() === null) {
232 return $this->add($bookmark, $save);
233 }
234 return $this->set($bookmark, $save);
235 }
236
237 /**
238 * @inheritDoc
239 */
240 public function remove(Bookmark $bookmark, bool $save = true): void
241 {
242 if (true !== $this->isLoggedIn) {
243 throw new Exception(t('You\'re not authorized to alter the datastore'));
244 }
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 */
259 public function exists(int $id, string $visibility = null): bool
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];
270 if (
271 ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
272 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
273 ) {
274 return false;
275 }
276
277 return true;
278 }
279
280 /**
281 * @inheritDoc
282 */
283 public function count(string $visibility = null): int
284 {
285 return count($this->search([], $visibility));
286 }
287
288 /**
289 * @inheritDoc
290 */
291 public function save(): void
292 {
293 if (true !== $this->isLoggedIn) {
294 // TODO: raise an Exception instead
295 die('You are not authorized to change the database.');
296 }
297
298 $this->bookmarks->reorder();
299 $this->bookmarksIO->write($this->bookmarks);
300 $this->pageCacheManager->invalidateCaches();
301 }
302
303 /**
304 * @inheritDoc
305 */
306 public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
307 {
308 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
309 $tags = [];
310 $caseMapping = [];
311 foreach ($bookmarks as $bookmark) {
312 foreach ($bookmark->getTags() as $tag) {
313 if (
314 empty($tag)
315 || (! $this->isLoggedIn && startsWith($tag, '.'))
316 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
317 || in_array($tag, $filteringTags, true)
318 ) {
319 continue;
320 }
321
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);
343
344 return $tags;
345 }
346
347 /**
348 * @inheritDoc
349 */
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();
363 } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
364 $out[] = $bookmark;
365 } else {
366 if ($previous !== null) {
367 break;
368 }
369 $previous = $bookmark->getCreated();
370 }
371 }
372
373 return $out;
374 }
375
376 /**
377 * @inheritDoc
378 */
379 public function getLatest(): ?Bookmark
380 {
381 foreach ($this->search([], null, false, false, true) as $bookmark) {
382 return $bookmark;
383 }
384
385 return null;
386 }
387
388 /**
389 * @inheritDoc
390 */
391 public function initialize(): void
392 {
393 $initializer = new BookmarkInitializer($this);
394 $initializer->initialize();
395
396 if (true === $this->isLoggedIn) {
397 $this->save();
398 }
399 }
400
401 /**
402 * Handles migration to the new database format (BookmarksArray).
403 */
404 protected function migrate(): void
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 }