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\Updater\UpdaterUtils
;
18 * Class BookmarksService
20 * This is the entry point to manipulate the bookmark DB.
21 * It manipulates loads links from a file data store containing all bookmarks.
23 * It also triggers the legacy format (bookmarks as arrays) migration.
25 class BookmarkFileService
implements BookmarkServiceInterface
27 /** @var Bookmark[] instance */
30 /** @var BookmarkIO instance */
31 protected $bookmarksIO;
33 /** @var BookmarkFilter */
34 protected $bookmarkFilter;
36 /** @var ConfigManager instance */
39 /** @var History instance */
42 /** @var bool true for logged in users. Default value to retrieve private bookmarks. */
43 protected $isLoggedIn;
48 public function __construct(ConfigManager
$conf, History
$history, $isLoggedIn)
51 $this->history
= $history;
52 $this->bookmarksIO
= new BookmarkIO($this->conf
);
53 $this->isLoggedIn
= $isLoggedIn;
55 if (!$this->isLoggedIn
&& $this->conf
->get('privacy.hide_public_links', false)) {
56 $this->bookmarks
= [];
59 $this->bookmarks
= $this->bookmarksIO
->read();
60 } catch (EmptyDataStoreException
$e) {
61 $this->bookmarks
= new BookmarkArray();
67 if (! $this->bookmarks
instanceof BookmarkArray
) {
70 'Your data store has been migrated, please reload the page.'. PHP_EOL
.
71 'If this message keeps showing up, please delete data/updates.txt file.'
76 $this->bookmarkFilter
= new BookmarkFilter($this->bookmarks
);
82 public function findByHash($hash)
84 $bookmark = $this->bookmarkFilter
->filter(BookmarkFilter
::$FILTER_HASH, $hash);
85 // PHP 7.3 introduced array_key_first() to avoid this hack
86 $first = reset($bookmark);
87 if (! $this->isLoggedIn
&& $first->isPrivate()) {
88 throw new Exception('Not authorized');
97 public function findByUrl($url)
99 return $this->bookmarks
->getByUrl($url);
105 public function search($request = [], $visibility = null, $caseSensitive = false, $untaggedOnly = false)
107 if ($visibility === null) {
108 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
111 // Filter bookmark database according to parameters.
112 $searchtags = isset($request['searchtags']) ? $request['searchtags'] : '';
113 $searchterm = isset($request['searchterm']) ? $request['searchterm'] : '';
115 return $this->bookmarkFilter
->filter(
116 BookmarkFilter
::$FILTER_TAG | BookmarkFilter
::$FILTER_TEXT,
117 [$searchtags, $searchterm],
127 public function get($id, $visibility = null)
129 if (! isset($this->bookmarks
[$id])) {
130 throw new BookmarkNotFoundException();
133 if ($visibility === null) {
134 $visibility = $this->isLoggedIn
? BookmarkFilter
::$ALL : BookmarkFilter
::$PUBLIC;
137 $bookmark = $this->bookmarks
[$id];
138 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
139 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
141 throw new Exception('Unauthorized');
150 public function set($bookmark, $save = true)
152 if ($this->isLoggedIn
!== true) {
153 throw new Exception(t('You\'re not authorized to alter the datastore'));
155 if (! $bookmark instanceof Bookmark
) {
156 throw new Exception(t('Provided data is invalid'));
158 if (! isset($this->bookmarks
[$bookmark->getId()])) {
159 throw new BookmarkNotFoundException();
161 $bookmark->validate();
163 $bookmark->setUpdated(new \
DateTime());
164 $this->bookmarks
[$bookmark->getId()] = $bookmark;
165 if ($save === true) {
167 $this->history
->updateLink($bookmark);
169 return $this->bookmarks
[$bookmark->getId()];
175 public function add($bookmark, $save = true)
177 if ($this->isLoggedIn
!== true) {
178 throw new Exception(t('You\'re not authorized to alter the datastore'));
180 if (! $bookmark instanceof Bookmark
) {
181 throw new Exception(t('Provided data is invalid'));
183 if (! empty($bookmark->getId())) {
184 throw new Exception(t('This bookmarks already exists'));
186 $bookmark->setId($this->bookmarks
->getNextId());
187 $bookmark->validate();
189 $this->bookmarks
[$bookmark->getId()] = $bookmark;
190 if ($save === true) {
192 $this->history
->addLink($bookmark);
194 return $this->bookmarks
[$bookmark->getId()];
200 public function addOrSet($bookmark, $save = true)
202 if ($this->isLoggedIn
!== true) {
203 throw new Exception(t('You\'re not authorized to alter the datastore'));
205 if (! $bookmark instanceof Bookmark
) {
206 throw new Exception('Provided data is invalid');
208 if ($bookmark->getId() === null) {
209 return $this->add($bookmark, $save);
211 return $this->set($bookmark, $save);
217 public function remove($bookmark, $save = true)
219 if ($this->isLoggedIn
!== true) {
220 throw new Exception(t('You\'re not authorized to alter the datastore'));
222 if (! $bookmark instanceof Bookmark
) {
223 throw new Exception(t('Provided data is invalid'));
225 if (! isset($this->bookmarks
[$bookmark->getId()])) {
226 throw new BookmarkNotFoundException();
229 unset($this->bookmarks
[$bookmark->getId()]);
230 if ($save === true) {
232 $this->history
->deleteLink($bookmark);
239 public function exists($id, $visibility = null)
241 if (! isset($this->bookmarks
[$id])) {
245 if ($visibility === null) {
246 $visibility = $this->isLoggedIn
? 'all' : 'public';
249 $bookmark = $this->bookmarks
[$id];
250 if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
251 || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
262 public function count($visibility = null)
264 return count($this->search([], $visibility));
270 public function save()
272 if (!$this->isLoggedIn
) {
273 // TODO: raise an Exception instead
274 die('You are not authorized to change the database.');
276 $this->bookmarks
->reorder();
277 $this->bookmarksIO
->write($this->bookmarks
);
278 invalidateCaches($this->conf
->get('resource.page_cache'));
284 public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
286 $bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
289 foreach ($bookmarks as $bookmark) {
290 foreach ($bookmark->getTags() as $tag) {
292 || (! $this->isLoggedIn
&& startsWith($tag, '.'))
293 || $tag === BookmarkMarkdownFormatter
::NO_MD_TAG
298 // The first case found will be displayed.
299 if (!isset($caseMapping[strtolower($tag)])) {
300 $caseMapping[strtolower($tag)] = $tag;
301 $tags[$caseMapping[strtolower($tag)]] = 0;
303 $tags[$caseMapping[strtolower($tag)]]++
;
308 * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
309 * Also, this function doesn't produce the same result between PHP 5.6 and 7.
311 * So we now use array_multisort() to sort tags by DESC occurrences,
312 * then ASC alphabetically for equal values.
314 * @see https://github.com/shaarli/Shaarli/issues/1142
316 $keys = array_keys($tags);
317 $tmpTags = array_combine($keys, $keys);
318 array_multisort($tags, SORT_DESC
, $tmpTags, SORT_ASC
, $tags);
325 public function days()
328 foreach ($this->search() as $bookmark) {
329 $bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
331 $bookmarkDays = array_keys($bookmarkDays);
334 return $bookmarkDays;
340 public function filterDay($request)
342 return $this->bookmarkFilter
->filter(BookmarkFilter
::$FILTER_DAY, $request);
348 public function initialize()
350 $initializer = new BookmarkInitializer($this);
351 $initializer->initialize();
355 * Handles migration to the new database format (BookmarksArray).
357 protected function migrate()
359 $bookmarkDb = new LegacyLinkDB(
360 $this->conf
->get('resource.datastore'),
364 $updater = new LegacyUpdater(
365 UpdaterUtils
::read_updates_file($this->conf
->get('resource.updates')),
370 $newUpdates = $updater->update();
371 if (! empty($newUpdates)) {
372 UpdaterUtils
::write_updates_file(
373 $this->conf
->get('resource.updates'),
374 $updater->getDoneUpdates()