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\Render\PageCacheManager
;
19 use Shaarli\Updater\UpdaterUtils
;
22 * Class BookmarksService
24 * This is the entry point to manipulate the bookmark DB.
25 * It manipulates loads links from a file data store containing all bookmarks.
27 * It also triggers the legacy format (bookmarks as arrays) migration.
29 class BookmarkFileService
implements BookmarkServiceInterface
31 /** @var Bookmark[] instance */
34 /** @var BookmarkIO instance */
35 protected $bookmarksIO;
37 /** @var BookmarkFilter */
38 protected $bookmarkFilter;
40 /** @var ConfigManager instance */
43 /** @var History instance */
46 /** @var PageCacheManager instance */
47 protected $pageCacheManager;
49 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
50 protected $isLoggedIn;
58 public function __construct(ConfigManager
$conf, History
$history, Mutex
$mutex, bool $isLoggedIn)
61 $this->history
= $history;
62 $this->mutex
= $mutex;
63 $this->pageCacheManager
= new PageCacheManager($this->conf
->get('resource.page_cache'), $isLoggedIn);
64 $this->bookmarksIO
= new BookmarkIO($this->conf
, $this->mutex
);
65 $this->isLoggedIn
= $isLoggedIn;
67 if (!$this->isLoggedIn
&& $this->conf
->get('privacy.hide_public_links', false)) {
68 $this->bookmarks
= [];
71 $this->bookmarks
= $this->bookmarksIO
->read();
72 } catch (EmptyDataStoreException
|DatastoreNotInitializedException
$e) {
73 $this->bookmarks
= new BookmarkArray();
75 if ($this->isLoggedIn
) {
76 // Datastore file does not exists, we initialize it with default bookmarks.
77 if ($e instanceof DatastoreNotInitializedException
) {
85 if (! $this->bookmarks
instanceof BookmarkArray
) {
88 'Your data store has been migrated, please reload the page.'. PHP_EOL
.
89 'If this message keeps showing up, please delete data/updates.txt file.'
94 $this->bookmarkFilter
= new BookmarkFilter($this->bookmarks
);
100 public function findByHash(string $hash): Bookmark
102 $bookmark = $this->bookmarkFilter
->filter(BookmarkFilter
::$FILTER_HASH, $hash);
103 // PHP 7.3 introduced array_key_first() to avoid this hack
104 $first = reset($bookmark);
105 if (! $this->isLoggedIn
&& $first->isPrivate()) {
106 throw new Exception('Not authorized');
115 public function findByUrl(string $url): ?Bookmark
117 return $this->bookmarks
->getByUrl($url);
123 public function search(
125 string $visibility = null,
126 bool $caseSensitive = false,
127 bool $untaggedOnly = false,
128 bool $ignoreSticky = false
130 if ($visibility === null) {
131 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
134 // Filter bookmark database according to parameters.
135 $searchTags = isset($request['searchtags']) ? $request['searchtags'] : '';
136 $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : '';
139 $this->bookmarks
->reorder('DESC', true);
142 return $this->bookmarkFilter
->filter(
143 BookmarkFilter
::$FILTER_TAG | BookmarkFilter
::$FILTER_TEXT,
144 [$searchTags, $searchTerm],
154 public function get(int $id, string $visibility = null): Bookmark
156 if (! isset($this->bookmarks
[$id])) {
157 throw new BookmarkNotFoundException();
160 if ($visibility === null) {
161 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
164 $bookmark = $this->bookmarks
[$id];
165 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
166 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
168 throw new Exception('Unauthorized');
177 public function set(Bookmark
$bookmark, bool $save = true): Bookmark
179 if (true !== $this->isLoggedIn
) {
180 throw new Exception(t('You\'re not authorized to alter the datastore'));
182 if (! isset($this->bookmarks
[$bookmark->getId()])) {
183 throw new BookmarkNotFoundException();
185 $bookmark->validate();
187 $bookmark->setUpdated(new DateTime());
188 $this->bookmarks
[$bookmark->getId()] = $bookmark;
189 if ($save === true) {
191 $this->history
->updateLink($bookmark);
193 return $this->bookmarks
[$bookmark->getId()];
199 public function add(Bookmark
$bookmark, bool $save = true): Bookmark
201 if (true !== $this->isLoggedIn
) {
202 throw new Exception(t('You\'re not authorized to alter the datastore'));
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
$bookmark, bool $save = true): Bookmark
223 if (true !== $this->isLoggedIn
) {
224 throw new Exception(t('You\'re not authorized to alter the datastore'));
226 if ($bookmark->getId() === null) {
227 return $this->add($bookmark, $save);
229 return $this->set($bookmark, $save);
235 public function remove(Bookmark
$bookmark, bool $save = true): void
237 if (true !== $this->isLoggedIn
) {
238 throw new Exception(t('You\'re not authorized to alter the datastore'));
240 if (! isset($this->bookmarks
[$bookmark->getId()])) {
241 throw new BookmarkNotFoundException();
244 unset($this->bookmarks
[$bookmark->getId()]);
245 if ($save === true) {
247 $this->history
->deleteLink($bookmark);
254 public function exists(int $id, string $visibility = null): bool
256 if (! isset($this->bookmarks
[$id])) {
260 if ($visibility === null) {
261 $visibility = $this->isLoggedIn
? 'all' : 'public';
264 $bookmark = $this->bookmarks
[$id];
265 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
266 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
277 public function count(string $visibility = null): int
279 return count($this->search([], $visibility));
285 public function save(): void
287 if (true !== $this->isLoggedIn
) {
288 // TODO: raise an Exception instead
289 die('You are not authorized to change the database.');
292 $this->bookmarks
->reorder();
293 $this->bookmarksIO
->write($this->bookmarks
);
294 $this->pageCacheManager
->invalidateCaches();
300 public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
302 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
305 foreach ($bookmarks as $bookmark) {
306 foreach ($bookmark->getTags() as $tag) {
308 || (! $this->isLoggedIn
&& startsWith($tag, '.'))
309 || $tag === BookmarkMarkdownFormatter
::NO_MD_TAG
310 || in_array($tag, $filteringTags, true)
315 // The first case found will be displayed.
316 if (!isset($caseMapping[strtolower($tag)])) {
317 $caseMapping[strtolower($tag)] = $tag;
318 $tags[$caseMapping[strtolower($tag)]] = 0;
320 $tags[$caseMapping[strtolower($tag)]]++
;
325 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
326 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
328 * So we now use array_multisort() to sort tags by DESC occurrences,
329 * then ASC alphabetically for equal values.
331 * @see https://github.com/shaarli/Shaarli/issues/1142
333 $keys = array_keys($tags);
334 $tmpTags = array_combine($keys, $keys);
335 array_multisort($tags, SORT_DESC
, $tmpTags, SORT_ASC
, $tags);
343 public function days(): array
346 foreach ($this->search() as $bookmark) {
347 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
349 $bookmarkDays = array_keys($bookmarkDays);
352 return array_map('strval', $bookmarkDays);
358 public function filterDay(string $request)
360 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
362 return $this->bookmarkFilter
->filter(BookmarkFilter
::$FILTER_DAY, $request, false, $visibility);
368 public function initialize(): void
370 $initializer = new BookmarkInitializer($this);
371 $initializer->initialize();
373 if (true === $this->isLoggedIn
) {
379 * Handles migration to the new database format (BookmarksArray).
381 protected function migrate(): void
383 $bookmarkDb = new LegacyLinkDB(
384 $this->conf
->get('resource.datastore'),
388 $updater = new LegacyUpdater(
389 UpdaterUtils
::read_updates_file($this->conf
->get('resource.updates')),
394 $newUpdates = $updater->update();
395 if (! empty($newUpdates)) {
396 UpdaterUtils
::write_updates_file(
397 $this->conf
->get('resource.updates'),
398 $updater->getDoneUpdates()