4 namespace Shaarli\Bookmark
;
8 use malkusch\lock\mutex\Mutex
;
9 use Shaarli\Bookmark\Exception\BookmarkNotFoundException
;
10 use Shaarli\Bookmark\Exception\DatastoreNotInitializedException
;
11 use Shaarli\Bookmark\Exception\EmptyDataStoreException
;
12 use Shaarli\Config\ConfigManager
;
13 use Shaarli\Formatter\BookmarkMarkdownFormatter
;
15 use Shaarli\Legacy\LegacyLinkDB
;
16 use Shaarli\Legacy\LegacyUpdater
;
17 use Shaarli\Render\PageCacheManager
;
18 use Shaarli\Updater\UpdaterUtils
;
21 * Class BookmarksService
23 * This is the entry point to manipulate the bookmark DB.
24 * It manipulates loads links from a file data store containing all bookmarks.
26 * It also triggers the legacy format (bookmarks as arrays) migration.
28 class BookmarkFileService
implements BookmarkServiceInterface
30 /** @var Bookmark[] instance */
33 /** @var BookmarkIO instance */
34 protected $bookmarksIO;
36 /** @var BookmarkFilter */
37 protected $bookmarkFilter;
39 /** @var ConfigManager instance */
42 /** @var History instance */
45 /** @var PageCacheManager instance */
46 protected $pageCacheManager;
48 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
49 protected $isLoggedIn;
57 public function __construct(ConfigManager
$conf, History
$history, Mutex
$mutex, $isLoggedIn)
60 $this->history
= $history;
61 $this->mutex
= $mutex;
62 $this->pageCacheManager
= new PageCacheManager($this->conf
->get('resource.page_cache'), $isLoggedIn);
63 $this->bookmarksIO
= new BookmarkIO($this->conf
, $this->mutex
);
64 $this->isLoggedIn
= $isLoggedIn;
66 if (!$this->isLoggedIn
&& $this->conf
->get('privacy.hide_public_links', false)) {
67 $this->bookmarks
= [];
70 $this->bookmarks
= $this->bookmarksIO
->read();
71 } catch (EmptyDataStoreException
|DatastoreNotInitializedException
$e) {
72 $this->bookmarks
= new BookmarkArray();
74 if ($this->isLoggedIn
) {
75 // Datastore file does not exists, we initialize it with default bookmarks.
76 if ($e instanceof DatastoreNotInitializedException
) {
84 if (! $this->bookmarks
instanceof BookmarkArray
) {
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.'
93 $this->bookmarkFilter
= new BookmarkFilter($this->bookmarks
);
99 public function findByHash($hash)
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');
114 public function findByUrl($url)
116 return $this->bookmarks
->getByUrl($url);
122 public function search(
125 $caseSensitive = false,
126 $untaggedOnly = false,
127 bool $ignoreSticky = false
129 if ($visibility === null) {
130 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
133 // Filter bookmark database according to parameters.
134 $searchtags = isset($request['searchtags']) ? $request['searchtags'] : '';
135 $searchterm = isset($request['searchterm']) ? $request['searchterm'] : '';
138 $this->bookmarks
->reorder('DESC', true);
141 return $this->bookmarkFilter
->filter(
142 BookmarkFilter
::$FILTER_TAG | BookmarkFilter
::$FILTER_TEXT,
143 [$searchtags, $searchterm],
153 public function get($id, $visibility = null)
155 if (! isset($this->bookmarks
[$id])) {
156 throw new BookmarkNotFoundException();
159 if ($visibility === null) {
160 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
163 $bookmark = $this->bookmarks
[$id];
164 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
165 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
167 throw new Exception('Unauthorized');
176 public function set($bookmark, $save = true)
178 if (true !== $this->isLoggedIn
) {
179 throw new Exception(t('You\'re not authorized to alter the datastore'));
181 if (! $bookmark instanceof Bookmark
) {
182 throw new Exception(t('Provided data is invalid'));
184 if (! isset($this->bookmarks
[$bookmark->getId()])) {
185 throw new BookmarkNotFoundException();
187 $bookmark->validate();
189 $bookmark->setUpdated(new \
DateTime());
190 $this->bookmarks
[$bookmark->getId()] = $bookmark;
191 if ($save === true) {
193 $this->history
->updateLink($bookmark);
195 return $this->bookmarks
[$bookmark->getId()];
201 public function add($bookmark, $save = true)
203 if (true !== $this->isLoggedIn
) {
204 throw new Exception(t('You\'re not authorized to alter the datastore'));
206 if (! $bookmark instanceof Bookmark
) {
207 throw new Exception(t('Provided data is invalid'));
209 if (! empty($bookmark->getId())) {
210 throw new Exception(t('This bookmarks already exists'));
212 $bookmark->setId($this->bookmarks
->getNextId());
213 $bookmark->validate();
215 $this->bookmarks
[$bookmark->getId()] = $bookmark;
216 if ($save === true) {
218 $this->history
->addLink($bookmark);
220 return $this->bookmarks
[$bookmark->getId()];
226 public function addOrSet($bookmark, $save = true)
228 if (true !== $this->isLoggedIn
) {
229 throw new Exception(t('You\'re not authorized to alter the datastore'));
231 if (! $bookmark instanceof Bookmark
) {
232 throw new Exception('Provided data is invalid');
234 if ($bookmark->getId() === null) {
235 return $this->add($bookmark, $save);
237 return $this->set($bookmark, $save);
243 public function remove($bookmark, $save = true)
245 if (true !== $this->isLoggedIn
) {
246 throw new Exception(t('You\'re not authorized to alter the datastore'));
248 if (! $bookmark instanceof Bookmark
) {
249 throw new Exception(t('Provided data is invalid'));
251 if (! isset($this->bookmarks
[$bookmark->getId()])) {
252 throw new BookmarkNotFoundException();
255 unset($this->bookmarks
[$bookmark->getId()]);
256 if ($save === true) {
258 $this->history
->deleteLink($bookmark);
265 public function exists($id, $visibility = null)
267 if (! isset($this->bookmarks
[$id])) {
271 if ($visibility === null) {
272 $visibility = $this->isLoggedIn
? 'all' : 'public';
275 $bookmark = $this->bookmarks
[$id];
276 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
277 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
288 public function count($visibility = null)
290 return count($this->search([], $visibility));
296 public function save()
298 if (true !== $this->isLoggedIn
) {
299 // TODO: raise an Exception instead
300 die('You are not authorized to change the database.');
303 $this->bookmarks
->reorder();
304 $this->bookmarksIO
->write($this->bookmarks
);
305 $this->pageCacheManager
->invalidateCaches();
311 public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
313 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
316 foreach ($bookmarks as $bookmark) {
317 foreach ($bookmark->getTags() as $tag) {
319 || (! $this->isLoggedIn
&& startsWith($tag, '.'))
320 || $tag === BookmarkMarkdownFormatter
::NO_MD_TAG
321 || in_array($tag, $filteringTags, true)
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;
331 $tags[$caseMapping[strtolower($tag)]]++
;
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.
339 * So we now use array_multisort() to sort tags by DESC occurrences,
340 * then ASC alphabetically for equal values.
342 * @see https://github.com/shaarli/Shaarli/issues/1142
344 $keys = array_keys($tags);
345 $tmpTags = array_combine($keys, $keys);
346 array_multisort($tags, SORT_DESC
, $tmpTags, SORT_ASC
, $tags);
353 public function days()
356 foreach ($this->search() as $bookmark) {
357 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
359 $bookmarkDays = array_keys($bookmarkDays);
362 return $bookmarkDays;
368 public function filterDay($request)
370 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
372 return $this->bookmarkFilter
->filter(BookmarkFilter
::$FILTER_DAY, $request, false, $visibility);
378 public function initialize()
380 $initializer = new BookmarkInitializer($this);
381 $initializer->initialize();
383 if (true === $this->isLoggedIn
) {
389 * Handles migration to the new database format (BookmarksArray).
391 protected function migrate()
393 $bookmarkDb = new LegacyLinkDB(
394 $this->conf
->get('resource.datastore'),
398 $updater = new LegacyUpdater(
399 UpdaterUtils
::read_updates_file($this->conf
->get('resource.updates')),
404 $newUpdates = $updater->update();
405 if (! empty($newUpdates)) {
406 UpdaterUtils
::write_updates_file(
407 $this->conf
->get('resource.updates'),
408 $updater->getDoneUpdates()