4 namespace Shaarli\Bookmark
;
8 use Shaarli\Bookmark\Exception\BookmarkNotFoundException
;
9 use Shaarli\Bookmark\Exception\DatastoreNotInitializedException
;
10 use Shaarli\Bookmark\Exception\EmptyDataStoreException
;
11 use Shaarli\Config\ConfigManager
;
12 use Shaarli\Formatter\BookmarkMarkdownFormatter
;
14 use Shaarli\Legacy\LegacyLinkDB
;
15 use Shaarli\Legacy\LegacyUpdater
;
16 use Shaarli\Render\PageCacheManager
;
17 use Shaarli\Updater\UpdaterUtils
;
20 * Class BookmarksService
22 * This is the entry point to manipulate the bookmark DB.
23 * It manipulates loads links from a file data store containing all bookmarks.
25 * It also triggers the legacy format (bookmarks as arrays) migration.
27 class BookmarkFileService
implements BookmarkServiceInterface
29 /** @var Bookmark[] instance */
32 /** @var BookmarkIO instance */
33 protected $bookmarksIO;
35 /** @var BookmarkFilter */
36 protected $bookmarkFilter;
38 /** @var ConfigManager instance */
41 /** @var History instance */
44 /** @var PageCacheManager instance */
45 protected $pageCacheManager;
47 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
48 protected $isLoggedIn;
53 public function __construct(ConfigManager
$conf, History
$history, $isLoggedIn)
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;
61 if (!$this->isLoggedIn
&& $this->conf
->get('privacy.hide_public_links', false)) {
62 $this->bookmarks
= [];
65 $this->bookmarks
= $this->bookmarksIO
->read();
66 } catch (EmptyDataStoreException
|DatastoreNotInitializedException
$e) {
67 $this->bookmarks
= new BookmarkArray();
69 if ($this->isLoggedIn
) {
70 // Datastore file does not exists, we initialize it with default bookmarks.
71 if ($e instanceof DatastoreNotInitializedException
) {
79 if (! $this->bookmarks
instanceof BookmarkArray
) {
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.'
88 $this->bookmarkFilter
= new BookmarkFilter($this->bookmarks
);
94 public function findByHash($hash)
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');
109 public function findByUrl($url)
111 return $this->bookmarks
->getByUrl($url);
117 public function search(
120 $caseSensitive = false,
121 $untaggedOnly = false,
122 bool $ignoreSticky = false
124 if ($visibility === null) {
125 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
128 // Filter bookmark database according to parameters.
129 $searchtags = isset($request['searchtags']) ? $request['searchtags'] : '';
130 $searchterm = isset($request['searchterm']) ? $request['searchterm'] : '';
133 $this->bookmarks
->reorder('DESC', true);
136 return $this->bookmarkFilter
->filter(
137 BookmarkFilter
::$FILTER_TAG | BookmarkFilter
::$FILTER_TEXT,
138 [$searchtags, $searchterm],
148 public function get($id, $visibility = null)
150 if (! isset($this->bookmarks
[$id])) {
151 throw new BookmarkNotFoundException();
154 if ($visibility === null) {
155 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
158 $bookmark = $this->bookmarks
[$id];
159 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
160 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
162 throw new Exception('Unauthorized');
171 public function set($bookmark, $save = true)
173 if (true !== $this->isLoggedIn
) {
174 throw new Exception(t('You\'re not authorized to alter the datastore'));
176 if (! $bookmark instanceof Bookmark
) {
177 throw new Exception(t('Provided data is invalid'));
179 if (! isset($this->bookmarks
[$bookmark->getId()])) {
180 throw new BookmarkNotFoundException();
182 $bookmark->validate();
184 $bookmark->setUpdated(new \
DateTime());
185 $this->bookmarks
[$bookmark->getId()] = $bookmark;
186 if ($save === true) {
188 $this->history
->updateLink($bookmark);
190 return $this->bookmarks
[$bookmark->getId()];
196 public function add($bookmark, $save = true)
198 if (true !== $this->isLoggedIn
) {
199 throw new Exception(t('You\'re not authorized to alter the datastore'));
201 if (! $bookmark instanceof Bookmark
) {
202 throw new Exception(t('Provided data is invalid'));
204 if (! empty($bookmark->getId())) {
205 throw new Exception(t('This bookmarks already exists'));
207 $bookmark->setId($this->bookmarks
->getNextId());
208 $bookmark->validate();
210 $this->bookmarks
[$bookmark->getId()] = $bookmark;
211 if ($save === true) {
213 $this->history
->addLink($bookmark);
215 return $this->bookmarks
[$bookmark->getId()];
221 public function addOrSet($bookmark, $save = true)
223 if (true !== $this->isLoggedIn
) {
224 throw new Exception(t('You\'re not authorized to alter the datastore'));
226 if (! $bookmark instanceof Bookmark
) {
227 throw new Exception('Provided data is invalid');
229 if ($bookmark->getId() === null) {
230 return $this->add($bookmark, $save);
232 return $this->set($bookmark, $save);
238 public function remove($bookmark, $save = true)
240 if (true !== $this->isLoggedIn
) {
241 throw new Exception(t('You\'re not authorized to alter the datastore'));
243 if (! $bookmark instanceof Bookmark
) {
244 throw new Exception(t('Provided data is invalid'));
246 if (! isset($this->bookmarks
[$bookmark->getId()])) {
247 throw new BookmarkNotFoundException();
250 unset($this->bookmarks
[$bookmark->getId()]);
251 if ($save === true) {
253 $this->history
->deleteLink($bookmark);
260 public function exists($id, $visibility = null)
262 if (! isset($this->bookmarks
[$id])) {
266 if ($visibility === null) {
267 $visibility = $this->isLoggedIn
? 'all' : 'public';
270 $bookmark = $this->bookmarks
[$id];
271 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
272 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
283 public function count($visibility = null)
285 return count($this->search([], $visibility));
291 public function save()
293 if (true !== $this->isLoggedIn
) {
294 // TODO: raise an Exception instead
295 die('You are not authorized to change the database.');
298 $this->bookmarks
->reorder();
299 $this->bookmarksIO
->write($this->bookmarks
);
300 $this->pageCacheManager
->invalidateCaches();
306 public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
308 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
311 foreach ($bookmarks as $bookmark) {
312 foreach ($bookmark->getTags() as $tag) {
314 || (! $this->isLoggedIn
&& startsWith($tag, '.'))
315 || $tag === BookmarkMarkdownFormatter
::NO_MD_TAG
316 || in_array($tag, $filteringTags, true)
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;
326 $tags[$caseMapping[strtolower($tag)]]++
;
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.
334 * So we now use array_multisort() to sort tags by DESC occurrences,
335 * then ASC alphabetically for equal values.
337 * @see https://github.com/shaarli/Shaarli/issues/1142
339 $keys = array_keys($tags);
340 $tmpTags = array_combine($keys, $keys);
341 array_multisort($tags, SORT_DESC
, $tmpTags, SORT_ASC
, $tags);
348 public function days()
351 foreach ($this->search() as $bookmark) {
352 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
354 $bookmarkDays = array_keys($bookmarkDays);
357 return $bookmarkDays;
363 public function filterDay($request)
365 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
367 return $this->bookmarkFilter
->filter(BookmarkFilter
::$FILTER_DAY, $request, false, $visibility);
373 public function initialize()
375 $initializer = new BookmarkInitializer($this);
376 $initializer->initialize();
378 if (true === $this->isLoggedIn
) {
384 * Handles migration to the new database format (BookmarksArray).
386 protected function migrate()
388 $bookmarkDb = new LegacyLinkDB(
389 $this->conf
->get('resource.datastore'),
393 $updater = new LegacyUpdater(
394 UpdaterUtils
::read_updates_file($this->conf
->get('resource.updates')),
399 $newUpdates = $updater->update();
400 if (! empty($newUpdates)) {
401 UpdaterUtils
::write_updates_file(
402 $this->conf
->get('resource.updates'),
403 $updater->getDoneUpdates()