]> git.immae.eu Git - github/shaarli/Shaarli.git/blame - application/bookmark/BookmarkFileService.php
Merge pull request #1570 from ArthurHoaro/feature/datastore-mutex
[github/shaarli/Shaarli.git] / application / bookmark / BookmarkFileService.php
CommitLineData
336a28fa
A
1<?php
2
3
4namespace Shaarli\Bookmark;
5
6
7use Exception;
fd1ddad9 8use malkusch\lock\mutex\Mutex;
336a28fa 9use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
d6e5f04d 10use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
336a28fa
A
11use Shaarli\Bookmark\Exception\EmptyDataStoreException;
12use Shaarli\Config\ConfigManager;
a39acb25 13use Shaarli\Formatter\BookmarkMarkdownFormatter;
336a28fa
A
14use Shaarli\History;
15use Shaarli\Legacy\LegacyLinkDB;
16use Shaarli\Legacy\LegacyUpdater;
b0428aa9 17use Shaarli\Render\PageCacheManager;
336a28fa
A
18use Shaarli\Updater\UpdaterUtils;
19
20/**
21 * Class BookmarksService
22 *
23 * This is the entry point to manipulate the bookmark DB.
24 * It manipulates loads links from a file data store containing all bookmarks.
25 *
26 * It also triggers the legacy format (bookmarks as arrays) migration.
27 */
28class BookmarkFileService implements BookmarkServiceInterface
29{
30 /** @var Bookmark[] instance */
31 protected $bookmarks;
32
33 /** @var BookmarkIO instance */
34 protected $bookmarksIO;
35
36 /** @var BookmarkFilter */
37 protected $bookmarkFilter;
38
39 /** @var ConfigManager instance */
40 protected $conf;
41
42 /** @var History instance */
43 protected $history;
44
b0428aa9
A
45 /** @var PageCacheManager instance */
46 protected $pageCacheManager;
47
336a28fa
A
48 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
49 protected $isLoggedIn;
50
fd1ddad9
A
51 /** @var Mutex */
52 protected $mutex;
53
336a28fa
A
54 /**
55 * @inheritDoc
56 */
fd1ddad9 57 public function __construct(ConfigManager $conf, History $history, Mutex $mutex, $isLoggedIn)
336a28fa
A
58 {
59 $this->conf = $conf;
60 $this->history = $history;
fd1ddad9 61 $this->mutex = $mutex;
c4d5be53 62 $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
fd1ddad9 63 $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex);
336a28fa
A
64 $this->isLoggedIn = $isLoggedIn;
65
66 if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
67 $this->bookmarks = [];
68 } else {
69 try {
70 $this->bookmarks = $this->bookmarksIO->read();
d6e5f04d 71 } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) {
336a28fa 72 $this->bookmarks = new BookmarkArray();
d6e5f04d 73
c4ad3d4f 74 if ($this->isLoggedIn) {
d6e5f04d
A
75 // Datastore file does not exists, we initialize it with default bookmarks.
76 if ($e instanceof DatastoreNotInitializedException) {
77 $this->initialize();
78 } else {
79 $this->save();
80 }
336a28fa
A
81 }
82 }
83
84 if (! $this->bookmarks instanceof BookmarkArray) {
85 $this->migrate();
86 exit(
87 'Your data store has been migrated, please reload the page.'. PHP_EOL .
88 'If this message keeps showing up, please delete data/updates.txt file.'
89 );
90 }
91 }
92
93 $this->bookmarkFilter = new BookmarkFilter($this->bookmarks);
94 }
95
96 /**
97 * @inheritDoc
98 */
99 public function findByHash($hash)
100 {
101 $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
102 // PHP 7.3 introduced array_key_first() to avoid this hack
103 $first = reset($bookmark);
104 if (! $this->isLoggedIn && $first->isPrivate()) {
105 throw new Exception('Not authorized');
106 }
107
1a8ac737 108 return $first;
336a28fa
A
109 }
110
111 /**
112 * @inheritDoc
113 */
114 public function findByUrl($url)
115 {
116 return $this->bookmarks->getByUrl($url);
117 }
118
119 /**
120 * @inheritDoc
121 */
a8e210fa
A
122 public function search(
123 $request = [],
124 $visibility = null,
125 $caseSensitive = false,
126 $untaggedOnly = false,
127 bool $ignoreSticky = false
128 ) {
336a28fa
A
129 if ($visibility === null) {
130 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
131 }
132
133 // Filter bookmark database according to parameters.
134 $searchtags = isset($request['searchtags']) ? $request['searchtags'] : '';
135 $searchterm = isset($request['searchterm']) ? $request['searchterm'] : '';
136
a8e210fa
A
137 if ($ignoreSticky) {
138 $this->bookmarks->reorder('DESC', true);
139 }
140
336a28fa
A
141 return $this->bookmarkFilter->filter(
142 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
143 [$searchtags, $searchterm],
144 $caseSensitive,
145 $visibility,
146 $untaggedOnly
147 );
148 }
149
150 /**
151 * @inheritDoc
152 */
153 public function get($id, $visibility = null)
154 {
155 if (! isset($this->bookmarks[$id])) {
156 throw new BookmarkNotFoundException();
157 }
158
159 if ($visibility === null) {
a39acb25 160 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
336a28fa
A
161 }
162
163 $bookmark = $this->bookmarks[$id];
164 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
165 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
166 ) {
167 throw new Exception('Unauthorized');
168 }
169
170 return $bookmark;
171 }
172
173 /**
174 * @inheritDoc
175 */
176 public function set($bookmark, $save = true)
177 {
d6e5f04d 178 if (true !== $this->isLoggedIn) {
336a28fa
A
179 throw new Exception(t('You\'re not authorized to alter the datastore'));
180 }
181 if (! $bookmark instanceof Bookmark) {
182 throw new Exception(t('Provided data is invalid'));
183 }
184 if (! isset($this->bookmarks[$bookmark->getId()])) {
185 throw new BookmarkNotFoundException();
186 }
187 $bookmark->validate();
188
189 $bookmark->setUpdated(new \DateTime());
190 $this->bookmarks[$bookmark->getId()] = $bookmark;
191 if ($save === true) {
192 $this->save();
193 $this->history->updateLink($bookmark);
194 }
195 return $this->bookmarks[$bookmark->getId()];
196 }
197
198 /**
199 * @inheritDoc
200 */
201 public function add($bookmark, $save = true)
202 {
d6e5f04d 203 if (true !== $this->isLoggedIn) {
336a28fa
A
204 throw new Exception(t('You\'re not authorized to alter the datastore'));
205 }
206 if (! $bookmark instanceof Bookmark) {
207 throw new Exception(t('Provided data is invalid'));
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, $save = true)
227 {
d6e5f04d 228 if (true !== $this->isLoggedIn) {
336a28fa
A
229 throw new Exception(t('You\'re not authorized to alter the datastore'));
230 }
231 if (! $bookmark instanceof Bookmark) {
232 throw new Exception('Provided data is invalid');
233 }
234 if ($bookmark->getId() === null) {
235 return $this->add($bookmark, $save);
236 }
237 return $this->set($bookmark, $save);
238 }
239
240 /**
241 * @inheritDoc
242 */
243 public function remove($bookmark, $save = true)
244 {
d6e5f04d 245 if (true !== $this->isLoggedIn) {
336a28fa
A
246 throw new Exception(t('You\'re not authorized to alter the datastore'));
247 }
248 if (! $bookmark instanceof Bookmark) {
249 throw new Exception(t('Provided data is invalid'));
250 }
251 if (! isset($this->bookmarks[$bookmark->getId()])) {
252 throw new BookmarkNotFoundException();
253 }
254
255 unset($this->bookmarks[$bookmark->getId()]);
256 if ($save === true) {
257 $this->save();
258 $this->history->deleteLink($bookmark);
259 }
260 }
261
262 /**
263 * @inheritDoc
264 */
265 public function exists($id, $visibility = null)
266 {
267 if (! isset($this->bookmarks[$id])) {
268 return false;
269 }
270
271 if ($visibility === null) {
272 $visibility = $this->isLoggedIn ? 'all' : 'public';
273 }
274
275 $bookmark = $this->bookmarks[$id];
276 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
277 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
278 ) {
279 return false;
280 }
281
282 return true;
283 }
284
285 /**
286 * @inheritDoc
287 */
288 public function count($visibility = null)
289 {
290 return count($this->search([], $visibility));
291 }
292
293 /**
294 * @inheritDoc
295 */
296 public function save()
297 {
d6e5f04d 298 if (true !== $this->isLoggedIn) {
336a28fa
A
299 // TODO: raise an Exception instead
300 die('You are not authorized to change the database.');
301 }
c4ad3d4f 302
336a28fa
A
303 $this->bookmarks->reorder();
304 $this->bookmarksIO->write($this->bookmarks);
b0428aa9 305 $this->pageCacheManager->invalidateCaches();
336a28fa
A
306 }
307
308 /**
309 * @inheritDoc
310 */
311 public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
312 {
313 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
314 $tags = [];
315 $caseMapping = [];
316 foreach ($bookmarks as $bookmark) {
317 foreach ($bookmark->getTags() as $tag) {
a39acb25
A
318 if (empty($tag)
319 || (! $this->isLoggedIn && startsWith($tag, '.'))
320 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
c79473bd 321 || in_array($tag, $filteringTags, true)
a39acb25 322 ) {
336a28fa
A
323 continue;
324 }
a39acb25 325
336a28fa
A
326 // The first case found will be displayed.
327 if (!isset($caseMapping[strtolower($tag)])) {
328 $caseMapping[strtolower($tag)] = $tag;
329 $tags[$caseMapping[strtolower($tag)]] = 0;
330 }
331 $tags[$caseMapping[strtolower($tag)]]++;
332 }
333 }
334
335 /*
336 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
337 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
338 *
339 * So we now use array_multisort() to sort tags by DESC occurrences,
340 * then ASC alphabetically for equal values.
341 *
342 * @see https://github.com/shaarli/Shaarli/issues/1142
343 */
344 $keys = array_keys($tags);
345 $tmpTags = array_combine($keys, $keys);
346 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
347 return $tags;
348 }
349
350 /**
351 * @inheritDoc
352 */
353 public function days()
354 {
355 $bookmarkDays = [];
356 foreach ($this->search() as $bookmark) {
357 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
358 }
359 $bookmarkDays = array_keys($bookmarkDays);
360 sort($bookmarkDays);
361
362 return $bookmarkDays;
363 }
364
365 /**
366 * @inheritDoc
367 */
368 public function filterDay($request)
369 {
27ddfec3
A
370 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
371
372 return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility);
336a28fa
A
373 }
374
375 /**
376 * @inheritDoc
377 */
378 public function initialize()
379 {
380 $initializer = new BookmarkInitializer($this);
381 $initializer->initialize();
336a28fa 382
d6e5f04d
A
383 if (true === $this->isLoggedIn) {
384 $this->save();
385 }
c4ad3d4f
A
386 }
387
336a28fa
A
388 /**
389 * Handles migration to the new database format (BookmarksArray).
390 */
391 protected function migrate()
392 {
393 $bookmarkDb = new LegacyLinkDB(
394 $this->conf->get('resource.datastore'),
395 true,
396 false
397 );
398 $updater = new LegacyUpdater(
399 UpdaterUtils::read_updates_file($this->conf->get('resource.updates')),
400 $bookmarkDb,
401 $this->conf,
402 true
403 );
404 $newUpdates = $updater->update();
405 if (! empty($newUpdates)) {
406 UpdaterUtils::write_updates_file(
407 $this->conf->get('resource.updates'),
408 $updater->getDoneUpdates()
409 );
410 }
411 }
412}