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