aboutsummaryrefslogtreecommitdiffhomepage
path: root/application/bookmark/BookmarkFileService.php
diff options
context:
space:
mode:
authorArthurHoaro <arthur@hoa.ro>2020-10-13 12:05:08 +0200
committerArthurHoaro <arthur@hoa.ro>2020-10-13 12:05:08 +0200
commitb6f678a5a1d15acf284ebcec16c905e976671ce1 (patch)
tree33c7da831482ed79c44896ef19c73c72ada84f2e /application/bookmark/BookmarkFileService.php
parentb14687036b9b800681197f51fdc47e62f0c88e2e (diff)
parent1c1520b6b98ab20201bfe15577782a52320339df (diff)
downloadShaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.tar.gz
Shaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.tar.zst
Shaarli-b6f678a5a1d15acf284ebcec16c905e976671ce1.zip
Merge branch 'v0.12' into latest
Diffstat (limited to 'application/bookmark/BookmarkFileService.php')
-rw-r--r--application/bookmark/BookmarkFileService.php407
1 files changed, 407 insertions, 0 deletions
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php
new file mode 100644
index 00000000..c9ec2609
--- /dev/null
+++ b/application/bookmark/BookmarkFileService.php
@@ -0,0 +1,407 @@
1<?php
2
3
4namespace Shaarli\Bookmark;
5
6
7use Exception;
8use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
9use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
10use Shaarli\Bookmark\Exception\EmptyDataStoreException;
11use Shaarli\Config\ConfigManager;
12use Shaarli\Formatter\BookmarkMarkdownFormatter;
13use Shaarli\History;
14use Shaarli\Legacy\LegacyLinkDB;
15use Shaarli\Legacy\LegacyUpdater;
16use Shaarli\Render\PageCacheManager;
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
44 /** @var PageCacheManager instance */
45 protected $pageCacheManager;
46
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;
57 $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
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();
66 } catch (EmptyDataStoreException|DatastoreNotInitializedException $e) {
67 $this->bookmarks = new BookmarkArray();
68
69 if ($this->isLoggedIn) {
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 }
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
103 return $first;
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(
118 $request = [],
119 $visibility = null,
120 $caseSensitive = false,
121 $untaggedOnly = false,
122 bool $ignoreSticky = false
123 ) {
124 if ($visibility === null) {
125 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
126 }
127
128 // Filter bookmark database according to parameters.
129 $searchtags = isset($request['searchtags']) ? $request['searchtags'] : '';
130 $searchterm = isset($request['searchterm']) ? $request['searchterm'] : '';
131
132 if ($ignoreSticky) {
133 $this->bookmarks->reorder('DESC', true);
134 }
135
136 return $this->bookmarkFilter->filter(
137 BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
138 [$searchtags, $searchterm],
139 $caseSensitive,
140 $visibility,
141 $untaggedOnly
142 );
143 }
144
145 /**
146 * @inheritDoc
147 */
148 public function get($id, $visibility = null)
149 {
150 if (! isset($this->bookmarks[$id])) {
151 throw new BookmarkNotFoundException();
152 }
153
154 if ($visibility === null) {
155 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
156 }
157
158 $bookmark = $this->bookmarks[$id];
159 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
160 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
161 ) {
162 throw new Exception('Unauthorized');
163 }
164
165 return $bookmark;
166 }
167
168 /**
169 * @inheritDoc
170 */
171 public function set($bookmark, $save = true)
172 {
173 if (true !== $this->isLoggedIn) {
174 throw new Exception(t('You\'re not authorized to alter the datastore'));
175 }
176 if (! $bookmark instanceof Bookmark) {
177 throw new Exception(t('Provided data is invalid'));
178 }
179 if (! isset($this->bookmarks[$bookmark->getId()])) {
180 throw new BookmarkNotFoundException();
181 }
182 $bookmark->validate();
183
184 $bookmark->setUpdated(new \DateTime());
185 $this->bookmarks[$bookmark->getId()] = $bookmark;
186 if ($save === true) {
187 $this->save();
188 $this->history->updateLink($bookmark);
189 }
190 return $this->bookmarks[$bookmark->getId()];
191 }
192
193 /**
194 * @inheritDoc
195 */
196 public function add($bookmark, $save = true)
197 {
198 if (true !== $this->isLoggedIn) {
199 throw new Exception(t('You\'re not authorized to alter the datastore'));
200 }
201 if (! $bookmark instanceof Bookmark) {
202 throw new Exception(t('Provided data is invalid'));
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, $save = true)
222 {
223 if (true !== $this->isLoggedIn) {
224 throw new Exception(t('You\'re not authorized to alter the datastore'));
225 }
226 if (! $bookmark instanceof Bookmark) {
227 throw new Exception('Provided data is invalid');
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, $save = true)
239 {
240 if (true !== $this->isLoggedIn) {
241 throw new Exception(t('You\'re not authorized to alter the datastore'));
242 }
243 if (! $bookmark instanceof Bookmark) {
244 throw new Exception(t('Provided data is invalid'));
245 }
246 if (! isset($this->bookmarks[$bookmark->getId()])) {
247 throw new BookmarkNotFoundException();
248 }
249
250 unset($this->bookmarks[$bookmark->getId()]);
251 if ($save === true) {
252 $this->save();
253 $this->history->deleteLink($bookmark);
254 }
255 }
256
257 /**
258 * @inheritDoc
259 */
260 public function exists($id, $visibility = null)
261 {
262 if (! isset($this->bookmarks[$id])) {
263 return false;
264 }
265
266 if ($visibility === null) {
267 $visibility = $this->isLoggedIn ? 'all' : 'public';
268 }
269
270 $bookmark = $this->bookmarks[$id];
271 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
272 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
273 ) {
274 return false;
275 }
276
277 return true;
278 }
279
280 /**
281 * @inheritDoc
282 */
283 public function count($visibility = null)
284 {
285 return count($this->search([], $visibility));
286 }
287
288 /**
289 * @inheritDoc
290 */
291 public function save()
292 {
293 if (true !== $this->isLoggedIn) {
294 // TODO: raise an Exception instead
295 die('You are not authorized to change the database.');
296 }
297
298 $this->bookmarks->reorder();
299 $this->bookmarksIO->write($this->bookmarks);
300 $this->pageCacheManager->invalidateCaches();
301 }
302
303 /**
304 * @inheritDoc
305 */
306 public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
307 {
308 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
309 $tags = [];
310 $caseMapping = [];
311 foreach ($bookmarks as $bookmark) {
312 foreach ($bookmark->getTags() as $tag) {
313 if (empty($tag)
314 || (! $this->isLoggedIn && startsWith($tag, '.'))
315 || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
316 || in_array($tag, $filteringTags, true)
317 ) {
318 continue;
319 }
320
321 // The first case found will be displayed.
322 if (!isset($caseMapping[strtolower($tag)])) {
323 $caseMapping[strtolower($tag)] = $tag;
324 $tags[$caseMapping[strtolower($tag)]] = 0;
325 }
326 $tags[$caseMapping[strtolower($tag)]]++;
327 }
328 }
329
330 /*
331 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
332 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
333 *
334 * So we now use array_multisort() to sort tags by DESC occurrences,
335 * then ASC alphabetically for equal values.
336 *
337 * @see https://github.com/shaarli/Shaarli/issues/1142
338 */
339 $keys = array_keys($tags);
340 $tmpTags = array_combine($keys, $keys);
341 array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
342 return $tags;
343 }
344
345 /**
346 * @inheritDoc
347 */
348 public function days()
349 {
350 $bookmarkDays = [];
351 foreach ($this->search() as $bookmark) {
352 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
353 }
354 $bookmarkDays = array_keys($bookmarkDays);
355 sort($bookmarkDays);
356
357 return $bookmarkDays;
358 }
359
360 /**
361 * @inheritDoc
362 */
363 public function filterDay($request)
364 {
365 $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
366
367 return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility);
368 }
369
370 /**
371 * @inheritDoc
372 */
373 public function initialize()
374 {
375 $initializer = new BookmarkInitializer($this);
376 $initializer->initialize();
377
378 if (true === $this->isLoggedIn) {
379 $this->save();
380 }
381 }
382
383 /**
384 * Handles migration to the new database format (BookmarksArray).
385 */
386 protected function migrate()
387 {
388 $bookmarkDb = new LegacyLinkDB(
389 $this->conf->get('resource.datastore'),
390 true,
391 false
392 );
393 $updater = new LegacyUpdater(
394 UpdaterUtils::read_updates_file($this->conf->get('resource.updates')),
395 $bookmarkDb,
396 $this->conf,
397 true
398 );
399 $newUpdates = $updater->update();
400 if (! empty($newUpdates)) {
401 UpdaterUtils::write_updates_file(
402 $this->conf->get('resource.updates'),
403 $updater->getDoneUpdates()
404 );
405 }
406 }
407}