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