3 declare(strict_types
=1);
5 namespace Shaarli\Bookmark
;
9 use malkusch\lock\mutex\Mutex
;
10 use Shaarli\Bookmark\Exception\BookmarkNotFoundException
;
11 use Shaarli\Bookmark\Exception\DatastoreNotInitializedException
;
12 use Shaarli\Bookmark\Exception\EmptyDataStoreException
;
13 use Shaarli\Config\ConfigManager
;
14 use Shaarli\Formatter\BookmarkMarkdownFormatter
;
16 use Shaarli\Legacy\LegacyLinkDB
;
17 use Shaarli\Legacy\LegacyUpdater
;
18 use Shaarli\Plugin\PluginManager
;
19 use Shaarli\Render\PageCacheManager
;
20 use Shaarli\Updater\UpdaterUtils
;
23 * Class BookmarksService
25 * This is the entry point to manipulate the bookmark DB.
26 * It manipulates loads links from a file data store containing all bookmarks.
28 * It also triggers the legacy format (bookmarks as arrays) migration.
30 class BookmarkFileService
implements BookmarkServiceInterface
32 /** @var Bookmark[] instance */
35 /** @var BookmarkIO instance */
36 protected $bookmarksIO;
38 /** @var BookmarkFilter */
39 protected $bookmarkFilter;
41 /** @var ConfigManager instance */
44 /** @var PluginManager */
45 protected $pluginManager;
47 /** @var History instance */
50 /** @var PageCacheManager instance */
51 protected $pageCacheManager;
53 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
54 protected $isLoggedIn;
62 public function __construct(
64 PluginManager
$pluginManager,
70 $this->history
= $history;
71 $this->mutex
= $mutex;
72 $this->pageCacheManager
= new PageCacheManager($this->conf
->get('resource.page_cache'), $isLoggedIn);
73 $this->bookmarksIO
= new BookmarkIO($this->conf
, $this->mutex
);
74 $this->isLoggedIn
= $isLoggedIn;
76 if (!$this->isLoggedIn
&& $this->conf
->get('privacy.hide_public_links', false)) {
77 $this->bookmarks
= [];
80 $this->bookmarks
= $this->bookmarksIO
->read();
81 } catch (EmptyDataStoreException
| DatastoreNotInitializedException
$e) {
82 $this->bookmarks
= new BookmarkArray();
84 if ($this->isLoggedIn
) {
85 // Datastore file does not exists, we initialize it with default bookmarks.
86 if ($e instanceof DatastoreNotInitializedException
) {
94 if (! $this->bookmarks
instanceof BookmarkArray
) {
97 'Your data store has been migrated, please reload the page.' . PHP_EOL
.
98 'If this message keeps showing up, please delete data/updates.txt file.'
103 $this->pluginManager
= $pluginManager;
104 $this->bookmarkFilter
= new BookmarkFilter($this->bookmarks
, $this->conf
, $this->pluginManager
);
110 public function findByHash(string $hash, string $privateKey = null): Bookmark
112 $bookmark = $this->bookmarkFilter
->filter(BookmarkFilter
::$FILTER_HASH, $hash);
113 // PHP 7.3 introduced array_key_first() to avoid this hack
114 $first = reset($bookmark);
117 && $first->isPrivate()
118 && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key'))
120 throw new BookmarkNotFoundException();
129 public function findByUrl(string $url): ?Bookmark
131 return $this->bookmarks
->getByUrl($url);
137 public function search(
139 string $visibility = null,
140 bool $caseSensitive = false,
141 bool $untaggedOnly = false,
142 bool $ignoreSticky = false,
143 array $pagination = []
145 if ($visibility === null) {
146 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
149 // Filter bookmark database according to parameters.
150 $searchTags = isset($request['searchtags']) ? $request['searchtags'] : '';
151 $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : '';
154 $this->bookmarks
->reorder('DESC', true);
157 $bookmarks = $this->bookmarkFilter
->filter(
158 BookmarkFilter
::$FILTER_TAG | BookmarkFilter
::$FILTER_TEXT,
159 [$searchTags, $searchTerm],
165 return SearchResult
::getSearchResult(
167 $pagination['offset'] ?? 0,
168 $pagination['limit'] ?? null,
169 $pagination['allowOutOfBounds'] ?? false
176 public function get(int $id, string $visibility = null): Bookmark
178 if (! isset($this->bookmarks
[$id])) {
179 throw new BookmarkNotFoundException();
182 if ($visibility === null) {
183 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
186 $bookmark = $this->bookmarks
[$id];
188 ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
189 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
191 throw new Exception('Unauthorized');
200 public function set(Bookmark
$bookmark, bool $save = true): Bookmark
202 if (true !== $this->isLoggedIn
) {
203 throw new Exception(t('You\'re not authorized to alter the datastore'));
205 if (! isset($this->bookmarks
[$bookmark->getId()])) {
206 throw new BookmarkNotFoundException();
208 $bookmark->validate();
210 $bookmark->setUpdated(new DateTime());
211 $this->bookmarks
[$bookmark->getId()] = $bookmark;
212 if ($save === true) {
214 $this->history
->updateLink($bookmark);
216 return $this->bookmarks
[$bookmark->getId()];
222 public function add(Bookmark
$bookmark, bool $save = true): Bookmark
224 if (true !== $this->isLoggedIn
) {
225 throw new Exception(t('You\'re not authorized to alter the datastore'));
227 if (!empty($bookmark->getId())) {
228 throw new Exception(t('This bookmarks already exists'));
230 $bookmark->setId($this->bookmarks
->getNextId());
231 $bookmark->validate();
233 $this->bookmarks
[$bookmark->getId()] = $bookmark;
234 if ($save === true) {
236 $this->history
->addLink($bookmark);
238 return $this->bookmarks
[$bookmark->getId()];
244 public function addOrSet(Bookmark
$bookmark, bool $save = true): Bookmark
246 if (true !== $this->isLoggedIn
) {
247 throw new Exception(t('You\'re not authorized to alter the datastore'));
249 if ($bookmark->getId() === null) {
250 return $this->add($bookmark, $save);
252 return $this->set($bookmark, $save);
258 public function remove(Bookmark
$bookmark, bool $save = true): void
260 if (true !== $this->isLoggedIn
) {
261 throw new Exception(t('You\'re not authorized to alter the datastore'));
263 if (! isset($this->bookmarks
[$bookmark->getId()])) {
264 throw new BookmarkNotFoundException();
267 unset($this->bookmarks
[$bookmark->getId()]);
268 if ($save === true) {
270 $this->history
->deleteLink($bookmark);
277 public function exists(int $id, string $visibility = null): bool
279 if (! isset($this->bookmarks
[$id])) {
283 if ($visibility === null) {
284 $visibility = $this->isLoggedIn
? 'all' : 'public';
287 $bookmark = $this->bookmarks
[$id];
289 ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
290 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
301 public function count(string $visibility = null): int
303 return $this->search([], $visibility)->getResultCount();
309 public function save(): void
311 if (true !== $this->isLoggedIn
) {
312 // TODO: raise an Exception instead
313 die('You are not authorized to change the database.');
316 $this->bookmarks
->reorder();
317 $this->bookmarksIO
->write($this->bookmarks
);
318 $this->pageCacheManager
->invalidateCaches();
324 public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
326 $searchResult = $this->search(['searchtags' => $filteringTags], $visibility);
329 foreach ($searchResult->getBookmarks() as $bookmark) {
330 foreach ($bookmark->getTags() as $tag) {
333 || (! $this->isLoggedIn
&& startsWith($tag, '.'))
334 || $tag === BookmarkMarkdownFormatter
::NO_MD_TAG
335 || in_array($tag, $filteringTags, true)
340 // The first case found will be displayed.
341 if (!isset($caseMapping[strtolower($tag)])) {
342 $caseMapping[strtolower($tag)] = $tag;
343 $tags[$caseMapping[strtolower($tag)]] = 0;
345 $tags[$caseMapping[strtolower($tag)]]++
;
350 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
351 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
353 * So we now use array_multisort() to sort tags by DESC occurrences,
354 * then ASC alphabetically for equal values.
356 * @see https://github.com/shaarli/Shaarli/issues/1142
358 $keys = array_keys($tags);
359 $tmpTags = array_combine($keys, $keys);
360 array_multisort($tags, SORT_DESC
, $tmpTags, SORT_ASC
, $tags);
368 public function findByDate(
369 \DateTimeInterface
$from,
370 \DateTimeInterface
$to,
371 ?\DateTimeInterface
&$previous,
372 ?\DateTimeInterface
&$next
378 foreach ($this->search([], null, false, false, true)->getBookmarks() as $bookmark) {
379 if ($to < $bookmark->getCreated()) {
380 $next = $bookmark->getCreated();
381 } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
384 if ($previous !== null) {
387 $previous = $bookmark->getCreated();
397 public function getLatest(): ?Bookmark
399 foreach ($this->search([], null, false, false, true)->getBookmarks() as $bookmark) {
409 public function initialize(): void
411 $initializer = new BookmarkInitializer($this);
412 $initializer->initialize();
414 if (true === $this->isLoggedIn
) {
420 * Handles migration to the new database format (BookmarksArray).
422 protected function migrate(): void
424 $bookmarkDb = new LegacyLinkDB(
425 $this->conf
->get('resource.datastore'),
429 $updater = new LegacyUpdater(
430 UpdaterUtils
::readUpdatesFile($this->conf
->get('resource.updates')),
435 $newUpdates = $updater->update();
436 if (! empty($newUpdates)) {
437 UpdaterUtils
::writeUpdatesFile(
438 $this->conf
->get('resource.updates'),
439 $updater->getDoneUpdates()