2019-05-25 15:46:47 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace Shaarli\Bookmark;
|
|
|
|
|
|
|
|
|
|
|
|
use Exception;
|
2020-09-26 14:18:01 +02:00
|
|
|
use malkusch\lock\mutex\Mutex;
|
2019-05-25 15:46:47 +02:00
|
|
|
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
|
2020-08-01 11:10:57 +02:00
|
|
|
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
|
2019-05-25 15:46:47 +02:00
|
|
|
use Shaarli\Bookmark\Exception\EmptyDataStoreException;
|
|
|
|
use Shaarli\Config\ConfigManager;
|
2020-01-18 11:33:23 +01:00
|
|
|
use Shaarli\Formatter\BookmarkMarkdownFormatter;
|
2019-05-25 15:46:47 +02:00
|
|
|
use Shaarli\History;
|
|
|
|
use Shaarli\Legacy\LegacyLinkDB;
|
|
|
|
use Shaarli\Legacy\LegacyUpdater;
|
2020-01-23 21:13:41 +01:00
|
|
|
use Shaarli\Render\PageCacheManager;
|
2019-05-25 15:46:47 +02:00
|
|
|
use Shaarli\Updater\UpdaterUtils;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Class BookmarksService
|
|
|
|
*
|
|
|
|
* This is the entry point to manipulate the bookmark DB.
|
|
|
|
* It manipulates loads links from a file data store containing all bookmarks.
|
|
|
|
*
|
|
|
|
* It also triggers the legacy format (bookmarks as arrays) migration.
|
|
|
|
*/
|
|
|
|
class BookmarkFileService implements BookmarkServiceInterface
|
|
|
|
{
|
|
|
|
/** @var Bookmark[] instance */
|
|
|
|
protected $bookmarks;
|
|
|
|
|
|
|
|
/** @var BookmarkIO instance */
|
|
|
|
protected $bookmarksIO;
|
|
|
|
|
|
|
|
/** @var BookmarkFilter */
|
|
|
|
protected $bookmarkFilter;
|
|
|
|
|
|
|
|
/** @var ConfigManager instance */
|
|
|
|
protected $conf;
|
|
|
|
|
|
|
|
/** @var History instance */
|
|
|
|
protected $history;
|
|
|
|
|
2020-01-23 21:13:41 +01:00
|
|
|
/** @var PageCacheManager instance */
|
|
|
|
protected $pageCacheManager;
|
|
|
|
|
2019-05-25 15:46:47 +02:00
|
|
|
/** @var bool true for logged in users. Default value to retrieve private bookmarks. */
|
|
|
|
protected $isLoggedIn;
|
|
|
|
|
2020-09-26 14:18:01 +02:00
|
|
|
/** @var Mutex */
|
|
|
|
protected $mutex;
|
|
|
|
|
2019-05-25 15:46:47 +02:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
2020-09-26 14:18:01 +02:00
|
|
|
public function __construct(ConfigManager $conf, History $history, Mutex $mutex, $isLoggedIn)
|
2019-05-25 15:46:47 +02:00
|
|
|
{
|
|
|
|
$this->conf = $conf;
|
|
|
|
$this->history = $history;
|
2020-09-26 14:18:01 +02:00
|
|
|
$this->mutex = $mutex;
|
2020-05-17 14:16:32 +02:00
|
|
|
$this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
|
2020-09-26 14:18:01 +02:00
|
|
|
$this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex);
|
2019-05-25 15:46:47 +02:00
|
|
|
$this->isLoggedIn = $isLoggedIn;
|
|
|
|
|
|
|
|
if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
|
|
|
|
$this->bookmarks = [];
|
|
|
|
} else {
|
|
|
|
try {
|
|
|
|
$this->bookmarks = $this->bookmarksIO->read();
|
2020-08-01 11:10:57 +02:00
|
|
|
} catch (EmptyDataStoreException|DatastoreNotInitializedException $e) {
|
2019-05-25 15:46:47 +02:00
|
|
|
$this->bookmarks = new BookmarkArray();
|
2020-08-01 11:10:57 +02:00
|
|
|
|
2020-07-07 10:15:56 +02:00
|
|
|
if ($this->isLoggedIn) {
|
2020-08-01 11:10:57 +02:00
|
|
|
// Datastore file does not exists, we initialize it with default bookmarks.
|
|
|
|
if ($e instanceof DatastoreNotInitializedException) {
|
|
|
|
$this->initialize();
|
|
|
|
} else {
|
|
|
|
$this->save();
|
|
|
|
}
|
2019-05-25 15:46:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (! $this->bookmarks instanceof BookmarkArray) {
|
|
|
|
$this->migrate();
|
|
|
|
exit(
|
|
|
|
'Your data store has been migrated, please reload the page.'. PHP_EOL .
|
|
|
|
'If this message keeps showing up, please delete data/updates.txt file.'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->bookmarkFilter = new BookmarkFilter($this->bookmarks);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function findByHash($hash)
|
|
|
|
{
|
|
|
|
$bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
|
|
|
|
// PHP 7.3 introduced array_key_first() to avoid this hack
|
|
|
|
$first = reset($bookmark);
|
|
|
|
if (! $this->isLoggedIn && $first->isPrivate()) {
|
|
|
|
throw new Exception('Not authorized');
|
|
|
|
}
|
|
|
|
|
2020-07-06 08:04:35 +02:00
|
|
|
return $first;
|
2019-05-25 15:46:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function findByUrl($url)
|
|
|
|
{
|
|
|
|
return $this->bookmarks->getByUrl($url);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
2020-08-29 10:06:40 +02:00
|
|
|
public function search(
|
|
|
|
$request = [],
|
|
|
|
$visibility = null,
|
|
|
|
$caseSensitive = false,
|
|
|
|
$untaggedOnly = false,
|
|
|
|
bool $ignoreSticky = false
|
|
|
|
) {
|
2019-05-25 15:46:47 +02:00
|
|
|
if ($visibility === null) {
|
|
|
|
$visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Filter bookmark database according to parameters.
|
|
|
|
$searchtags = isset($request['searchtags']) ? $request['searchtags'] : '';
|
|
|
|
$searchterm = isset($request['searchterm']) ? $request['searchterm'] : '';
|
|
|
|
|
2020-08-29 10:06:40 +02:00
|
|
|
if ($ignoreSticky) {
|
|
|
|
$this->bookmarks->reorder('DESC', true);
|
|
|
|
}
|
|
|
|
|
2019-05-25 15:46:47 +02:00
|
|
|
return $this->bookmarkFilter->filter(
|
|
|
|
BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
|
|
|
|
[$searchtags, $searchterm],
|
|
|
|
$caseSensitive,
|
|
|
|
$visibility,
|
|
|
|
$untaggedOnly
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function get($id, $visibility = null)
|
|
|
|
{
|
|
|
|
if (! isset($this->bookmarks[$id])) {
|
|
|
|
throw new BookmarkNotFoundException();
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($visibility === null) {
|
2020-01-18 11:33:23 +01:00
|
|
|
$visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
|
2019-05-25 15:46:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
$bookmark = $this->bookmarks[$id];
|
|
|
|
if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
|
|
|
|
|| (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
|
|
|
|
) {
|
|
|
|
throw new Exception('Unauthorized');
|
|
|
|
}
|
|
|
|
|
|
|
|
return $bookmark;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function set($bookmark, $save = true)
|
|
|
|
{
|
2020-08-01 11:10:57 +02:00
|
|
|
if (true !== $this->isLoggedIn) {
|
2019-05-25 15:46:47 +02:00
|
|
|
throw new Exception(t('You\'re not authorized to alter the datastore'));
|
|
|
|
}
|
|
|
|
if (! $bookmark instanceof Bookmark) {
|
|
|
|
throw new Exception(t('Provided data is invalid'));
|
|
|
|
}
|
|
|
|
if (! isset($this->bookmarks[$bookmark->getId()])) {
|
|
|
|
throw new BookmarkNotFoundException();
|
|
|
|
}
|
|
|
|
$bookmark->validate();
|
|
|
|
|
|
|
|
$bookmark->setUpdated(new \DateTime());
|
|
|
|
$this->bookmarks[$bookmark->getId()] = $bookmark;
|
|
|
|
if ($save === true) {
|
|
|
|
$this->save();
|
|
|
|
$this->history->updateLink($bookmark);
|
|
|
|
}
|
|
|
|
return $this->bookmarks[$bookmark->getId()];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function add($bookmark, $save = true)
|
|
|
|
{
|
2020-08-01 11:10:57 +02:00
|
|
|
if (true !== $this->isLoggedIn) {
|
2019-05-25 15:46:47 +02:00
|
|
|
throw new Exception(t('You\'re not authorized to alter the datastore'));
|
|
|
|
}
|
|
|
|
if (! $bookmark instanceof Bookmark) {
|
|
|
|
throw new Exception(t('Provided data is invalid'));
|
|
|
|
}
|
|
|
|
if (! empty($bookmark->getId())) {
|
|
|
|
throw new Exception(t('This bookmarks already exists'));
|
|
|
|
}
|
|
|
|
$bookmark->setId($this->bookmarks->getNextId());
|
|
|
|
$bookmark->validate();
|
|
|
|
|
|
|
|
$this->bookmarks[$bookmark->getId()] = $bookmark;
|
|
|
|
if ($save === true) {
|
|
|
|
$this->save();
|
|
|
|
$this->history->addLink($bookmark);
|
|
|
|
}
|
|
|
|
return $this->bookmarks[$bookmark->getId()];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function addOrSet($bookmark, $save = true)
|
|
|
|
{
|
2020-08-01 11:10:57 +02:00
|
|
|
if (true !== $this->isLoggedIn) {
|
2019-05-25 15:46:47 +02:00
|
|
|
throw new Exception(t('You\'re not authorized to alter the datastore'));
|
|
|
|
}
|
|
|
|
if (! $bookmark instanceof Bookmark) {
|
|
|
|
throw new Exception('Provided data is invalid');
|
|
|
|
}
|
|
|
|
if ($bookmark->getId() === null) {
|
|
|
|
return $this->add($bookmark, $save);
|
|
|
|
}
|
|
|
|
return $this->set($bookmark, $save);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function remove($bookmark, $save = true)
|
|
|
|
{
|
2020-08-01 11:10:57 +02:00
|
|
|
if (true !== $this->isLoggedIn) {
|
2019-05-25 15:46:47 +02:00
|
|
|
throw new Exception(t('You\'re not authorized to alter the datastore'));
|
|
|
|
}
|
|
|
|
if (! $bookmark instanceof Bookmark) {
|
|
|
|
throw new Exception(t('Provided data is invalid'));
|
|
|
|
}
|
|
|
|
if (! isset($this->bookmarks[$bookmark->getId()])) {
|
|
|
|
throw new BookmarkNotFoundException();
|
|
|
|
}
|
|
|
|
|
|
|
|
unset($this->bookmarks[$bookmark->getId()]);
|
|
|
|
if ($save === true) {
|
|
|
|
$this->save();
|
|
|
|
$this->history->deleteLink($bookmark);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function exists($id, $visibility = null)
|
|
|
|
{
|
|
|
|
if (! isset($this->bookmarks[$id])) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($visibility === null) {
|
|
|
|
$visibility = $this->isLoggedIn ? 'all' : 'public';
|
|
|
|
}
|
|
|
|
|
|
|
|
$bookmark = $this->bookmarks[$id];
|
|
|
|
if (($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
|
|
|
|
|| (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
|
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function count($visibility = null)
|
|
|
|
{
|
|
|
|
return count($this->search([], $visibility));
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function save()
|
|
|
|
{
|
2020-08-01 11:10:57 +02:00
|
|
|
if (true !== $this->isLoggedIn) {
|
2019-05-25 15:46:47 +02:00
|
|
|
// TODO: raise an Exception instead
|
|
|
|
die('You are not authorized to change the database.');
|
|
|
|
}
|
2020-07-07 10:15:56 +02:00
|
|
|
|
2019-05-25 15:46:47 +02:00
|
|
|
$this->bookmarks->reorder();
|
|
|
|
$this->bookmarksIO->write($this->bookmarks);
|
2020-01-23 21:13:41 +01:00
|
|
|
$this->pageCacheManager->invalidateCaches();
|
2019-05-25 15:46:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function bookmarksCountPerTag($filteringTags = [], $visibility = null)
|
|
|
|
{
|
|
|
|
$bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
|
|
|
|
$tags = [];
|
|
|
|
$caseMapping = [];
|
|
|
|
foreach ($bookmarks as $bookmark) {
|
|
|
|
foreach ($bookmark->getTags() as $tag) {
|
2020-01-18 11:33:23 +01:00
|
|
|
if (empty($tag)
|
|
|
|
|| (! $this->isLoggedIn && startsWith($tag, '.'))
|
|
|
|
|| $tag === BookmarkMarkdownFormatter::NO_MD_TAG
|
2020-05-16 13:13:00 +02:00
|
|
|
|| in_array($tag, $filteringTags, true)
|
2020-01-18 11:33:23 +01:00
|
|
|
) {
|
2019-05-25 15:46:47 +02:00
|
|
|
continue;
|
|
|
|
}
|
2020-01-18 11:33:23 +01:00
|
|
|
|
2019-05-25 15:46:47 +02:00
|
|
|
// The first case found will be displayed.
|
|
|
|
if (!isset($caseMapping[strtolower($tag)])) {
|
|
|
|
$caseMapping[strtolower($tag)] = $tag;
|
|
|
|
$tags[$caseMapping[strtolower($tag)]] = 0;
|
|
|
|
}
|
|
|
|
$tags[$caseMapping[strtolower($tag)]]++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Formerly used arsort(), which doesn't define the sort behaviour for equal values.
|
|
|
|
* Also, this function doesn't produce the same result between PHP 5.6 and 7.
|
|
|
|
*
|
|
|
|
* So we now use array_multisort() to sort tags by DESC occurrences,
|
|
|
|
* then ASC alphabetically for equal values.
|
|
|
|
*
|
|
|
|
* @see https://github.com/shaarli/Shaarli/issues/1142
|
|
|
|
*/
|
|
|
|
$keys = array_keys($tags);
|
|
|
|
$tmpTags = array_combine($keys, $keys);
|
|
|
|
array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
|
|
|
|
return $tags;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function days()
|
|
|
|
{
|
|
|
|
$bookmarkDays = [];
|
|
|
|
foreach ($this->search() as $bookmark) {
|
|
|
|
$bookmarkDays[$bookmark->getCreated()->format('Ymd')] = 0;
|
|
|
|
}
|
|
|
|
$bookmarkDays = array_keys($bookmarkDays);
|
|
|
|
sort($bookmarkDays);
|
|
|
|
|
|
|
|
return $bookmarkDays;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function filterDay($request)
|
|
|
|
{
|
2020-09-06 14:11:02 +02:00
|
|
|
$visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
|
|
|
|
|
|
|
|
return $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_DAY, $request, false, $visibility);
|
2019-05-25 15:46:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function initialize()
|
|
|
|
{
|
|
|
|
$initializer = new BookmarkInitializer($this);
|
|
|
|
$initializer->initialize();
|
|
|
|
|
2020-08-01 11:10:57 +02:00
|
|
|
if (true === $this->isLoggedIn) {
|
|
|
|
$this->save();
|
|
|
|
}
|
2020-07-07 10:15:56 +02:00
|
|
|
}
|
|
|
|
|
2019-05-25 15:46:47 +02:00
|
|
|
/**
|
|
|
|
* Handles migration to the new database format (BookmarksArray).
|
|
|
|
*/
|
|
|
|
protected function migrate()
|
|
|
|
{
|
|
|
|
$bookmarkDb = new LegacyLinkDB(
|
|
|
|
$this->conf->get('resource.datastore'),
|
|
|
|
true,
|
|
|
|
false
|
|
|
|
);
|
|
|
|
$updater = new LegacyUpdater(
|
|
|
|
UpdaterUtils::read_updates_file($this->conf->get('resource.updates')),
|
|
|
|
$bookmarkDb,
|
|
|
|
$this->conf,
|
|
|
|
true
|
|
|
|
);
|
|
|
|
$newUpdates = $updater->update();
|
|
|
|
if (! empty($newUpdates)) {
|
|
|
|
UpdaterUtils::write_updates_file(
|
|
|
|
$this->conf->get('resource.updates'),
|
|
|
|
$updater->getDoneUpdates()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|