]> git.immae.eu Git - github/shaarli/Shaarli.git/blob - application/bookmark/BookmarkFileService.php
eb7899bf7edc24b85ed4462fbb0f24dd50dedd6c
[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);
95 }
96
97 /**
98 * @inheritDoc
99 */
100 public function findByHash(string $hash): 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 && $first->isPrivate()) {
106 throw new Exception('Not authorized');
107 }
108
109 return $first;
110 }
111
112 /**
113 * @inheritDoc
114 */
115 public function findByUrl(string $url): ?Bookmark
116 {
117 return $this->bookmarks->getByUrl($url);
118 }
119
120 /**
121 * @inheritDoc
122 */
123 public function search(
124 array $request = [],
125 string $visibility = null,
126 bool $caseSensitive = false,
127 bool $untaggedOnly = false,
128 bool $ignoreSticky = false
129 ) {
130 if ($visibility === null) {
131 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
132 }
133
134 // Filter bookmark database according to parameters.
135 $searchTags = isset($request['searchtags']) ? $request['searchtags'] : '';
136 $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : '';
137
138 if ($ignoreSticky) {
139 $this->bookmarks->reorder('DESC', true);
140 }
141
142 return $this->bookmarkFilter->filter(
143 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
144 [$searchTags, $searchTerm],
145 $caseSensitive,
146 $visibility,
147 $untaggedOnly
148 );
149 }
150
151 /**
152 * @inheritDoc
153 */
154 public function get(int $id, string $visibility = null): Bookmark
155 {
156 if (! isset($this->bookmarks[$id])) {
157 throw new BookmarkNotFoundException();
158 }
159
160 if ($visibility === null) {
161 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
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 */
177 public function set(Bookmark $bookmark, bool $save = true): Bookmark
178 {
179 if (true !== $this->isLoggedIn) {
180 throw new Exception(t('You\'re not authorized to alter the datastore'));
181 }
182 if (! isset($this->bookmarks[$bookmark->getId()])) {
183 throw new BookmarkNotFoundException();
184 }
185 $bookmark->validate();
186
187 $bookmark->setUpdated(new DateTime());
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 */
199 public function add(Bookmark $bookmark, bool $save = true): Bookmark
200 {
201 if (true !== $this->isLoggedIn) {
202 throw new Exception(t('You\'re not authorized to alter the datastore'));
203 }
204 if (!empty($bookmark->getId())) {
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 */
221 public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark
222 {
223 if (true !== $this->isLoggedIn) {
224 throw new Exception(t('You\'re not authorized to alter the datastore'));
225 }
226 if ($bookmark->getId() === null) {
227 return $this->add($bookmark, $save);
228 }
229 return $this->set($bookmark, $save);
230 }
231
232 /**
233 * @inheritDoc
234 */
235 public function remove(Bookmark $bookmark, bool $save = true): void
236 {
237 if (true !== $this->isLoggedIn) {
238 throw new Exception(t('You\'re not authorized to alter the datastore'));
239 }
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 */
254 public function exists(int $id, string $visibility = null): bool
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 */
277 public function count(string $visibility = null): int
278 {
279 return count($this->search([], $visibility));
280 }
281
282 /**
283 * @inheritDoc
284 */
285 public function save(): void
286 {
287 if (true !== $this->isLoggedIn) {
288 // TODO: raise an Exception instead
289 die('You are not authorized to change the database.');
290 }
291
292 $this->bookmarks->reorder();
293 $this->bookmarksIO->write($this->bookmarks);
294 $this->pageCacheManager->invalidateCaches();
295 }
296
297 /**
298 * @inheritDoc
299 */
300 public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
301 {
302 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
303 $tags = [];
304 $caseMapping = [];
305 foreach ($bookmarks as $bookmark) {
306 foreach ($bookmark->getTags() as $tag) {
307 if (empty($tag)
308 || (! $this->isLoggedIn && startsWith($tag, '.'))
309 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
310 || in_array($tag, $filteringTags, true)
311 ) {
312 continue;
313 }
314
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);
336
337 return $tags;
338 }
339
340 /**
341 * @inheritDoc
342 */
343 public function days(): array
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 array_map('strval', $bookmarkDays);
353 }
354
355 /**
356 * @inheritDoc
357 */
358 public function filterDay(string $request)
359 {
360 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
361
362 return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility);
363 }
364
365 /**
366 * @inheritDoc
367 */
368 public function initialize(): void
369 {
370 $initializer = new BookmarkInitializer($this);
371 $initializer->initialize();
372
373 if (true === $this->isLoggedIn) {
374 $this->save();
375 }
376 }
377
378 /**
379 * Handles migration to the new database format (BookmarksArray).
380 */
381 protected function migrate(): void
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 }