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