4 namespace Shaarli\Bookmark
;
8 use Shaarli\Bookmark\Exception\BookmarkNotFoundException
;
9 use Shaarli\Bookmark\Exception\EmptyDataStoreException
;
10 use Shaarli\Config\ConfigManager
;
11 use Shaarli\Formatter\BookmarkMarkdownFormatter
;
13 use Shaarli\Legacy\LegacyLinkDB
;
14 use Shaarli\Legacy\LegacyUpdater
;
15 use Shaarli\Render\PageCacheManager
;
16 use Shaarli\Updater\UpdaterUtils
;
19 * Class BookmarksService
21 * This is the entry point to manipulate the bookmark DB.
22 * It manipulates loads links from a file data store containing all bookmarks.
24 * It also triggers the legacy format (bookmarks as arrays) migration.
26 class BookmarkFileService
implements BookmarkServiceInterface
28 /** @var Bookmark[] instance */
31 /** @var BookmarkIO instance */
32 protected $bookmarksIO;
34 /** @var BookmarkFilter */
35 protected $bookmarkFilter;
37 /** @var ConfigManager instance */
40 /** @var History instance */
43 /** @var PageCacheManager instance */
44 protected $pageCacheManager;
46 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
47 protected $isLoggedIn;
52 public function __construct(ConfigManager
$conf, History
$history, $isLoggedIn)
55 $this->history
= $history;
56 $this->pageCacheManager
= new PageCacheManager($this->conf
->get('resource.page_cache'), $isLoggedIn);
57 $this->bookmarksIO
= new BookmarkIO($this->conf
);
58 $this->isLoggedIn
= $isLoggedIn;
60 if (!$this->isLoggedIn
&& $this->conf
->get('privacy.hide_public_links', false)) {
61 $this->bookmarks
= [];
64 $this->bookmarks
= $this->bookmarksIO
->read();
65 } catch (EmptyDataStoreException
$e) {
66 $this->bookmarks
= new BookmarkArray();
72 if (! $this->bookmarks
instanceof BookmarkArray
) {
75 'Your data store has been migrated, please reload the page.'. PHP_EOL
.
76 'If this message keeps showing up, please delete data/updates.txt file.'
81 $this->bookmarkFilter
= new BookmarkFilter($this->bookmarks
);
87 public function findByHash($hash)
89 $bookmark = $this->bookmarkFilter
->filter(BookmarkFilter
::$FILTER_HASH, $hash);
90 // PHP 7.3 introduced array_key_first() to avoid this hack
91 $first = reset($bookmark);
92 if (! $this->isLoggedIn
&& $first->isPrivate()) {
93 throw new Exception('Not authorized');
102 public function findByUrl($url)
104 return $this->bookmarks
->getByUrl($url);
110 public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false)
112 if ($visibility === null) {
113 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
116 // Filter bookmark database according to parameters.
117 $searchtags = isset($request['searchtags']) ? $request['searchtags'] : '';
118 $searchterm = isset($request['searchterm']) ? $request['searchterm'] : '';
120 return $this->bookmarkFilter
->filter(
121 BookmarkFilter
::$FILTER_TAG | BookmarkFilter
::$FILTER_TEXT,
122 [$searchtags, $searchterm],
132 public function get($id, $visibility = null)
134 if (! isset($this->bookmarks
[$id])) {
135 throw new BookmarkNotFoundException();
138 if ($visibility === null) {
139 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
142 $bookmark = $this->bookmarks
[$id];
143 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
144 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
146 throw new Exception('Unauthorized');
155 public function set($bookmark, $save = true)
157 if ($this->isLoggedIn
!== true) {
158 throw new Exception(t('You\'re not authorized to alter the datastore'));
160 if (! $bookmark instanceof Bookmark
) {
161 throw new Exception(t('Provided data is invalid'));
163 if (! isset($this->bookmarks
[$bookmark->getId()])) {
164 throw new BookmarkNotFoundException();
166 $bookmark->validate();
168 $bookmark->setUpdated(new \
DateTime());
169 $this->bookmarks
[$bookmark->getId()] = $bookmark;
170 if ($save === true) {
172 $this->history
->updateLink($bookmark);
174 return $this->bookmarks
[$bookmark->getId()];
180 public function add($bookmark, $save = true)
182 if ($this->isLoggedIn
!== true) {
183 throw new Exception(t('You\'re not authorized to alter the datastore'));
185 if (! $bookmark instanceof Bookmark
) {
186 throw new Exception(t('Provided data is invalid'));
188 if (! empty($bookmark->getId())) {
189 throw new Exception(t('This bookmarks already exists'));
191 $bookmark->setId($this->bookmarks
->getNextId());
192 $bookmark->validate();
194 $this->bookmarks
[$bookmark->getId()] = $bookmark;
195 if ($save === true) {
197 $this->history
->addLink($bookmark);
199 return $this->bookmarks
[$bookmark->getId()];
205 public function addOrSet($bookmark, $save = true)
207 if ($this->isLoggedIn
!== true) {
208 throw new Exception(t('You\'re not authorized to alter the datastore'));
210 if (! $bookmark instanceof Bookmark
) {
211 throw new Exception('Provided data is invalid');
213 if ($bookmark->getId() === null) {
214 return $this->add($bookmark, $save);
216 return $this->set($bookmark, $save);
222 public function remove($bookmark, $save = true)
224 if ($this->isLoggedIn
!== true) {
225 throw new Exception(t('You\'re not authorized to alter the datastore'));
227 if (! $bookmark instanceof Bookmark
) {
228 throw new Exception(t('Provided data is invalid'));
230 if (! isset($this->bookmarks
[$bookmark->getId()])) {
231 throw new BookmarkNotFoundException();
234 unset($this->bookmarks
[$bookmark->getId()]);
235 if ($save === true) {
237 $this->history
->deleteLink($bookmark);
244 public function exists($id, $visibility = null)
246 if (! isset($this->bookmarks
[$id])) {
250 if ($visibility === null) {
251 $visibility = $this->isLoggedIn
? 'all' : 'public';
254 $bookmark = $this->bookmarks
[$id];
255 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
256 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
267 public function count($visibility = null)
269 return count($this->search([], $visibility));
275 public function save()
277 if (!$this->isLoggedIn
) {
278 // TODO: raise an Exception instead
279 die('You are not authorized to change the database.');
281 $this->bookmarks
->reorder();
282 $this->bookmarksIO
->write($this->bookmarks
);
283 $this->pageCacheManager
->invalidateCaches();
289 public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
291 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
294 foreach ($bookmarks as $bookmark) {
295 foreach ($bookmark->getTags() as $tag) {
297 || (! $this->isLoggedIn
&& startsWith($tag, '.'))
298 || $tag === BookmarkMarkdownFormatter
::NO_MD_TAG
299 || in_array($tag, $filteringTags, true)
304 // The first case found will be displayed.
305 if (!isset($caseMapping[strtolower($tag)])) {
306 $caseMapping[strtolower($tag)] = $tag;
307 $tags[$caseMapping[strtolower($tag)]] = 0;
309 $tags[$caseMapping[strtolower($tag)]]++
;
314 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
315 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
317 * So we now use array_multisort() to sort tags by DESC occurrences,
318 * then ASC alphabetically for equal values.
320 * @see https://github.com/shaarli/Shaarli/issues/1142
322 $keys = array_keys($tags);
323 $tmpTags = array_combine($keys, $keys);
324 array_multisort($tags, SORT_DESC
, $tmpTags, SORT_ASC
, $tags);
331 public function days()
334 foreach ($this->search() as $bookmark) {
335 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
337 $bookmarkDays = array_keys($bookmarkDays);
340 return $bookmarkDays;
346 public function filterDay($request)
348 return $this->bookmarkFilter
->filter(BookmarkFilter
::$FILTER_DAY, $request);
354 public function initialize()
356 $initializer = new BookmarkInitializer($this);
357 $initializer->initialize();
361 * Handles migration to the new database format (BookmarksArray).
363 protected function migrate()
365 $bookmarkDb = new LegacyLinkDB(
366 $this->conf
->get('resource.datastore'),
370 $updater = new LegacyUpdater(
371 UpdaterUtils
::read_updates_file($this->conf
->get('resource.updates')),
376 $newUpdates = $updater->update();
377 if (! empty($newUpdates)) {
378 UpdaterUtils
::write_updates_file(
379 $this->conf
->get('resource.updates'),
380 $updater->getDoneUpdates()