]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - application/bookmark/BookmarkFileService.php
85efeea60097860bf96066e4c712a35c3d1d20d6
[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 (!$this->isLoggedIn
106 && $first->isPrivate()
107 && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key'))
108 ) {
109 throw new BookmarkNotFoundException();
110 }
111
112 return $first;
113 }
114
115 /**
116 * @inheritDoc
117 */
118 public function findByUrl(string $url): ?Bookmark
119 {
120 return $this->bookmarks->getByUrl($url);
121 }
122
123 /**
124 * @inheritDoc
125 */
126 public function search(
127 array $request = [],
128 string $visibility = null,
129 bool $caseSensitive = false,
130 bool $untaggedOnly = false,
131 bool $ignoreSticky = false
132 ) {
133 if ($visibility === null) {
134 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
135 }
136
137 // Filter bookmark database according to parameters.
138 $searchTags = isset($request['searchtags']) ? $request['searchtags'] : '';
139 $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : '';
140
141 if ($ignoreSticky) {
142 $this->bookmarks->reorder('DESC', true);
143 }
144
145 return $this->bookmarkFilter->filter(
146 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
147 [$searchTags, $searchTerm],
148 $caseSensitive,
149 $visibility,
150 $untaggedOnly
151 );
152 }
153
154 /**
155 * @inheritDoc
156 */
157 public function get(int $id, string $visibility = null): Bookmark
158 {
159 if (! isset($this->bookmarks[$id])) {
160 throw new BookmarkNotFoundException();
161 }
162
163 if ($visibility === null) {
164 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
165 }
166
167 $bookmark = $this->bookmarks[$id];
168 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
169 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
170 ) {
171 throw new Exception('Unauthorized');
172 }
173
174 return $bookmark;
175 }
176
177 /**
178 * @inheritDoc
179 */
180 public function set(Bookmark $bookmark, bool $save = true): Bookmark
181 {
182 if (true !== $this->isLoggedIn) {
183 throw new Exception(t('You\'re not authorized to alter the datastore'));
184 }
185 if (! isset($this->bookmarks[$bookmark->getId()])) {
186 throw new BookmarkNotFoundException();
187 }
188 $bookmark->validate();
189
190 $bookmark->setUpdated(new DateTime());
191 $this->bookmarks[$bookmark->getId()] = $bookmark;
192 if ($save === true) {
193 $this->save();
194 $this->history->updateLink($bookmark);
195 }
196 return $this->bookmarks[$bookmark->getId()];
197 }
198
199 /**
200 * @inheritDoc
201 */
202 public function add(Bookmark $bookmark, bool $save = true): Bookmark
203 {
204 if (true !== $this->isLoggedIn) {
205 throw new Exception(t('You\'re not authorized to alter the datastore'));
206 }
207 if (!empty($bookmark->getId())) {
208 throw new Exception(t('This bookmarks already exists'));
209 }
210 $bookmark->setId($this->bookmarks->getNextId());
211 $bookmark->validate();
212
213 $this->bookmarks[$bookmark->getId()] = $bookmark;
214 if ($save === true) {
215 $this->save();
216 $this->history->addLink($bookmark);
217 }
218 return $this->bookmarks[$bookmark->getId()];
219 }
220
221 /**
222 * @inheritDoc
223 */
224 public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark
225 {
226 if (true !== $this->isLoggedIn) {
227 throw new Exception(t('You\'re not authorized to alter the datastore'));
228 }
229 if ($bookmark->getId() === null) {
230 return $this->add($bookmark, $save);
231 }
232 return $this->set($bookmark, $save);
233 }
234
235 /**
236 * @inheritDoc
237 */
238 public function remove(Bookmark $bookmark, bool $save = true): void
239 {
240 if (true !== $this->isLoggedIn) {
241 throw new Exception(t('You\'re not authorized to alter the datastore'));
242 }
243 if (! isset($this->bookmarks[$bookmark->getId()])) {
244 throw new BookmarkNotFoundException();
245 }
246
247 unset($this->bookmarks[$bookmark->getId()]);
248 if ($save === true) {
249 $this->save();
250 $this->history->deleteLink($bookmark);
251 }
252 }
253
254 /**
255 * @inheritDoc
256 */
257 public function exists(int $id, string $visibility = null): bool
258 {
259 if (! isset($this->bookmarks[$id])) {
260 return false;
261 }
262
263 if ($visibility === null) {
264 $visibility = $this->isLoggedIn ? 'all' : 'public';
265 }
266
267 $bookmark = $this->bookmarks[$id];
268 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
269 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
270 ) {
271 return false;
272 }
273
274 return true;
275 }
276
277 /**
278 * @inheritDoc
279 */
280 public function count(string $visibility = null): int
281 {
282 return count($this->search([], $visibility));
283 }
284
285 /**
286 * @inheritDoc
287 */
288 public function save(): void
289 {
290 if (true !== $this->isLoggedIn) {
291 // TODO: raise an Exception instead
292 die('You are not authorized to change the database.');
293 }
294
295 $this->bookmarks->reorder();
296 $this->bookmarksIO->write($this->bookmarks);
297 $this->pageCacheManager->invalidateCaches();
298 }
299
300 /**
301 * @inheritDoc
302 */
303 public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
304 {
305 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
306 $tags = [];
307 $caseMapping = [];
308 foreach ($bookmarks as $bookmark) {
309 foreach ($bookmark->getTags() as $tag) {
310 if (empty($tag)
311 || (! $this->isLoggedIn && startsWith($tag, '.'))
312 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
313 || in_array($tag, $filteringTags, true)
314 ) {
315 continue;
316 }
317
318 // The first case found will be displayed.
319 if (!isset($caseMapping[strtolower($tag)])) {
320 $caseMapping[strtolower($tag)] = $tag;
321 $tags[$caseMapping[strtolower($tag)]] = 0;
322 }
323 $tags[$caseMapping[strtolower($tag)]]++;
324 }
325 }
326
327 /*
328 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
329 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
330 *
331 * So we now use array_multisort() to sort tags by DESC occurrences,
332 * then ASC alphabetically for equal values.
333 *
334 * @see https://github.com/shaarli/Shaarli/issues/1142
335 */
336 $keys = array_keys($tags);
337 $tmpTags = array_combine($keys, $keys);
338 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
339
340 return $tags;
341 }
342
343 /**
344 * @inheritDoc
345 */
346 public function findByDate(
347 \DateTimeInterface $from,
348 \DateTimeInterface $to,
349 ?\DateTimeInterface &$previous,
350 ?\DateTimeInterface &$next
351 ): array {
352 $out = [];
353 $previous = null;
354 $next = null;
355
356 foreach ($this->search([], null, false, false, true) as $bookmark) {
357 if ($to < $bookmark->getCreated()) {
358 $next = $bookmark->getCreated();
359 } else if ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
360 $out[] = $bookmark;
361 } else {
362 if ($previous !== null) {
363 break;
364 }
365 $previous = $bookmark->getCreated();
366 }
367 }
368
369 return $out;
370 }
371
372 /**
373 * @inheritDoc
374 */
375 public function getLatest(): ?Bookmark
376 {
377 foreach ($this->search([], null, false, false, true) as $bookmark) {
378 return $bookmark;
379 }
380
381 return null;
382 }
383
384 /**
385 * @inheritDoc
386 */
387 public function initialize(): void
388 {
389 $initializer = new BookmarkInitializer($this);
390 $initializer->initialize();
391
392 if (true === $this->isLoggedIn) {
393 $this->save();
394 }
395 }
396
397 /**
398 * Handles migration to the new database format (BookmarksArray).
399 */
400 protected function migrate(): void
401 {
402 $bookmarkDb = new LegacyLinkDB(
403 $this->conf->get('resource.datastore'),
404 true,
405 false
406 );
407 $updater = new LegacyUpdater(
408 UpdaterUtils::read_updates_file($this->conf->get('resource.updates')),
409 $bookmarkDb,
410 $this->conf,
411 true
412 );
413 $newUpdates = $updater->update();
414 if (! empty($newUpdates)) {
415 UpdaterUtils::write_updates_file(
416 $this->conf->get('resource.updates'),
417 $updater->getDoneUpdates()
418 );
419 }
420 }
421 }