Merge branch 'master' of github.com:Shaarli/Shaarli into upstream

This commit is contained in:
Keith Carangelo 2021-03-29 11:27:32 -04:00
commit 755c094bdd
100 changed files with 3492 additions and 1230 deletions

View file

@ -26,7 +26,7 @@ RUN cd shaarli \
# Stage 4: # Stage 4:
# - Shaarli image # - Shaarli image
FROM alpine:3.8 FROM alpine:3.12
LABEL maintainer="Shaarli Community" LABEL maintainer="Shaarli Community"
RUN apk --update --no-cache add \ RUN apk --update --no-cache add \

View file

@ -1,7 +1,7 @@
# Stage 1: # Stage 1:
# - Copy Shaarli sources # - Copy Shaarli sources
# - Build documentation # - Build documentation
FROM arm32v6/alpine:3.8 as docs FROM arm32v6/alpine:3.10 as docs
ADD . /usr/src/app/shaarli ADD . /usr/src/app/shaarli
RUN apk --update --no-cache add py2-pip \ RUN apk --update --no-cache add py2-pip \
&& cd /usr/src/app/shaarli \ && cd /usr/src/app/shaarli \
@ -10,7 +10,7 @@ RUN apk --update --no-cache add py2-pip \
# Stage 2: # Stage 2:
# - Resolve PHP dependencies with Composer # - Resolve PHP dependencies with Composer
FROM arm32v6/alpine:3.8 as composer FROM arm32v6/alpine:3.10 as composer
COPY --from=docs /usr/src/app/shaarli /app/shaarli COPY --from=docs /usr/src/app/shaarli /app/shaarli
RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \ RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer \
&& cd /app/shaarli \ && cd /app/shaarli \
@ -18,7 +18,7 @@ RUN apk --update --no-cache add php7-curl php7-mbstring php7-simplexml composer
# Stage 3: # Stage 3:
# - Frontend dependencies # - Frontend dependencies
FROM arm32v6/alpine:3.8 as node FROM arm32v6/alpine:3.10 as node
COPY --from=composer /app/shaarli /shaarli COPY --from=composer /app/shaarli /shaarli
RUN apk --update --no-cache add yarn nodejs-current python2 build-base \ RUN apk --update --no-cache add yarn nodejs-current python2 build-base \
&& cd /shaarli \ && cd /shaarli \
@ -28,7 +28,7 @@ RUN apk --update --no-cache add yarn nodejs-current python2 build-base \
# Stage 4: # Stage 4:
# - Shaarli image # - Shaarli image
FROM arm32v6/alpine:3.8 FROM arm32v6/alpine:3.10
LABEL maintainer="Shaarli Community" LABEL maintainer="Shaarli Community"
RUN apk --update --no-cache add \ RUN apk --update --no-cache add \

View file

@ -323,6 +323,7 @@ function format_date($date, $time = true, $intl = true)
IntlDateFormatter::LONG, IntlDateFormatter::LONG,
$time ? IntlDateFormatter::LONG : IntlDateFormatter::NONE $time ? IntlDateFormatter::LONG : IntlDateFormatter::NONE
); );
$formatter->setTimeZone($date->getTimezone());
return $formatter->format($date); return $formatter->format($date);
} }

View file

@ -145,6 +145,7 @@ protected function setLinkDb($conf)
{ {
$linkDb = new BookmarkFileService( $linkDb = new BookmarkFileService(
$conf, $conf,
$this->container->get('pluginManager'),
$this->container->get('history'), $this->container->get('history'),
new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2), new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
true true

View file

@ -93,11 +93,15 @@ public static function formatLink($bookmark, $indexUrl)
* *
* @param array|null $input Request Link. * @param array|null $input Request Link.
* @param bool $defaultPrivate Setting defined if a bookmark is private by default. * @param bool $defaultPrivate Setting defined if a bookmark is private by default.
* @param string $tagsSeparator Tags separator loaded from the config file.
* *
* @return Bookmark instance. * @return Bookmark instance.
*/ */
public static function buildBookmarkFromRequest(?array $input, bool $defaultPrivate): Bookmark public static function buildBookmarkFromRequest(
{ ?array $input,
bool $defaultPrivate,
string $tagsSeparator
): Bookmark {
$bookmark = new Bookmark(); $bookmark = new Bookmark();
$url = ! empty($input['url']) ? cleanup_url($input['url']) : ''; $url = ! empty($input['url']) ? cleanup_url($input['url']) : '';
if (isset($input['private'])) { if (isset($input['private'])) {
@ -109,6 +113,15 @@ public static function buildBookmarkFromRequest(?array $input, bool $defaultPriv
$bookmark->setTitle(! empty($input['title']) ? $input['title'] : ''); $bookmark->setTitle(! empty($input['title']) ? $input['title'] : '');
$bookmark->setUrl($url); $bookmark->setUrl($url);
$bookmark->setDescription(! empty($input['description']) ? $input['description'] : ''); $bookmark->setDescription(! empty($input['description']) ? $input['description'] : '');
// Be permissive with provided tags format
if (is_string($input['tags'] ?? null)) {
$input['tags'] = tags_str2array($input['tags'], $tagsSeparator);
}
if (is_array($input['tags'] ?? null) && count($input['tags']) === 1 && is_string($input['tags'][0])) {
$input['tags'] = tags_str2array($input['tags'][0], $tagsSeparator);
}
$bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []); $bookmark->setTags(! empty($input['tags']) ? $input['tags'] : []);
$bookmark->setPrivate($private); $bookmark->setPrivate($private);

View file

@ -36,13 +36,6 @@ class Links extends ApiController
public function getLinks($request, $response) public function getLinks($request, $response)
{ {
$private = $request->getParam('visibility'); $private = $request->getParam('visibility');
$bookmarks = $this->bookmarkService->search(
[
'searchtags' => $request->getParam('searchtags', ''),
'searchterm' => $request->getParam('searchterm', ''),
],
$private
);
// Return bookmarks from the {offset}th link, starting from 0. // Return bookmarks from the {offset}th link, starting from 0.
$offset = $request->getParam('offset'); $offset = $request->getParam('offset');
@ -50,9 +43,6 @@ public function getLinks($request, $response)
throw new ApiBadParametersException('Invalid offset'); throw new ApiBadParametersException('Invalid offset');
} }
$offset = ! empty($offset) ? intval($offset) : 0; $offset = ! empty($offset) ? intval($offset) : 0;
if ($offset > count($bookmarks)) {
return $response->withJson([], 200, $this->jsonStyle);
}
// limit parameter is either a number of bookmarks or 'all' for everything. // limit parameter is either a number of bookmarks or 'all' for everything.
$limit = $request->getParam('limit'); $limit = $request->getParam('limit');
@ -61,24 +51,34 @@ public function getLinks($request, $response)
} elseif (ctype_digit($limit)) { } elseif (ctype_digit($limit)) {
$limit = intval($limit); $limit = intval($limit);
} elseif ($limit === 'all') { } elseif ($limit === 'all') {
$limit = count($bookmarks); $limit = null;
} else { } else {
throw new ApiBadParametersException('Invalid limit'); throw new ApiBadParametersException('Invalid limit');
} }
$searchResult = $this->bookmarkService->search(
[
'searchtags' => $request->getParam('searchtags', ''),
'searchterm' => $request->getParam('searchterm', ''),
],
$private,
false,
false,
false,
[
'limit' => $limit,
'offset' => $offset,
'allowOutOfBounds' => true,
]
);
// 'environment' is set by Slim and encapsulate $_SERVER. // 'environment' is set by Slim and encapsulate $_SERVER.
$indexUrl = index_url($this->ci['environment']); $indexUrl = index_url($this->ci['environment']);
$out = []; $out = [];
$index = 0; foreach ($searchResult->getBookmarks() as $bookmark) {
foreach ($bookmarks as $bookmark) {
if (count($out) >= $limit) {
break;
}
if ($index++ >= $offset) {
$out[] = ApiUtils::formatLink($bookmark, $indexUrl); $out[] = ApiUtils::formatLink($bookmark, $indexUrl);
} }
}
return $response->withJson($out, 200, $this->jsonStyle); return $response->withJson($out, 200, $this->jsonStyle);
} }
@ -117,7 +117,11 @@ public function getLink($request, $response, $args)
public function postLink($request, $response) public function postLink($request, $response)
{ {
$data = (array) ($request->getParsedBody() ?? []); $data = (array) ($request->getParsedBody() ?? []);
$bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); $bookmark = ApiUtils::buildBookmarkFromRequest(
$data,
$this->conf->get('privacy.default_private_links'),
$this->conf->get('general.tags_separator', ' ')
);
// duplicate by URL, return 409 Conflict // duplicate by URL, return 409 Conflict
if ( if (
! empty($bookmark->getUrl()) ! empty($bookmark->getUrl())
@ -158,7 +162,11 @@ public function putLink($request, $response, $args)
$index = index_url($this->ci['environment']); $index = index_url($this->ci['environment']);
$data = $request->getParsedBody(); $data = $request->getParsedBody();
$requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links')); $requestBookmark = ApiUtils::buildBookmarkFromRequest(
$data,
$this->conf->get('privacy.default_private_links'),
$this->conf->get('general.tags_separator', ' ')
);
// duplicate URL on a different link, return 409 Conflict // duplicate URL on a different link, return 409 Conflict
if ( if (
! empty($requestBookmark->getUrl()) ! empty($requestBookmark->getUrl())

View file

@ -122,12 +122,12 @@ public function putTag($request, $response, $args)
throw new ApiBadParametersException('New tag name is required in the request body'); throw new ApiBadParametersException('New tag name is required in the request body');
} }
$bookmarks = $this->bookmarkService->search( $searchResult = $this->bookmarkService->search(
['searchtags' => $args['tagName']], ['searchtags' => $args['tagName']],
BookmarkFilter::$ALL, BookmarkFilter::$ALL,
true true
); );
foreach ($bookmarks as $bookmark) { foreach ($searchResult->getBookmarks() as $bookmark) {
$bookmark->renameTag($args['tagName'], $data['name']); $bookmark->renameTag($args['tagName'], $data['name']);
$this->bookmarkService->set($bookmark, false); $this->bookmarkService->set($bookmark, false);
$this->history->updateLink($bookmark); $this->history->updateLink($bookmark);
@ -157,12 +157,12 @@ public function deleteTag($request, $response, $args)
throw new ApiTagNotFoundException(); throw new ApiTagNotFoundException();
} }
$bookmarks = $this->bookmarkService->search( $searchResult = $this->bookmarkService->search(
['searchtags' => $args['tagName']], ['searchtags' => $args['tagName']],
BookmarkFilter::$ALL, BookmarkFilter::$ALL,
true true
); );
foreach ($bookmarks as $bookmark) { foreach ($searchResult->getBookmarks() as $bookmark) {
$bookmark->deleteTag($args['tagName']); $bookmark->deleteTag($args['tagName']);
$this->bookmarkService->set($bookmark, false); $this->bookmarkService->set($bookmark, false);
$this->history->updateLink($bookmark); $this->history->updateLink($bookmark);

View file

@ -15,6 +15,7 @@
use Shaarli\History; use Shaarli\History;
use Shaarli\Legacy\LegacyLinkDB; use Shaarli\Legacy\LegacyLinkDB;
use Shaarli\Legacy\LegacyUpdater; use Shaarli\Legacy\LegacyUpdater;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageCacheManager; use Shaarli\Render\PageCacheManager;
use Shaarli\Updater\UpdaterUtils; use Shaarli\Updater\UpdaterUtils;
@ -40,6 +41,9 @@ class BookmarkFileService implements BookmarkServiceInterface
/** @var ConfigManager instance */ /** @var ConfigManager instance */
protected $conf; protected $conf;
/** @var PluginManager */
protected $pluginManager;
/** @var History instance */ /** @var History instance */
protected $history; protected $history;
@ -55,8 +59,13 @@ class BookmarkFileService implements BookmarkServiceInterface
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function __construct(ConfigManager $conf, History $history, Mutex $mutex, bool $isLoggedIn) public function __construct(
{ ConfigManager $conf,
PluginManager $pluginManager,
History $history,
Mutex $mutex,
bool $isLoggedIn
) {
$this->conf = $conf; $this->conf = $conf;
$this->history = $history; $this->history = $history;
$this->mutex = $mutex; $this->mutex = $mutex;
@ -91,7 +100,8 @@ public function __construct(ConfigManager $conf, History $history, Mutex $mutex,
} }
} }
$this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf); $this->pluginManager = $pluginManager;
$this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf, $this->pluginManager);
} }
/** /**
@ -129,8 +139,9 @@ public function search(
string $visibility = null, string $visibility = null,
bool $caseSensitive = false, bool $caseSensitive = false,
bool $untaggedOnly = false, bool $untaggedOnly = false,
bool $ignoreSticky = false bool $ignoreSticky = false,
) { array $pagination = []
): SearchResult {
if ($visibility === null) { if ($visibility === null) {
$visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC; $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
} }
@ -143,13 +154,20 @@ public function search(
$this->bookmarks->reorder('DESC', true); $this->bookmarks->reorder('DESC', true);
} }
return $this->bookmarkFilter->filter( $bookmarks = $this->bookmarkFilter->filter(
BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT, BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
[$searchTags, $searchTerm], [$searchTags, $searchTerm],
$caseSensitive, $caseSensitive,
$visibility, $visibility,
$untaggedOnly $untaggedOnly
); );
return SearchResult::getSearchResult(
$bookmarks,
$pagination['offset'] ?? 0,
$pagination['limit'] ?? null,
$pagination['allowOutOfBounds'] ?? false
);
} }
/** /**
@ -282,7 +300,7 @@ public function exists(int $id, string $visibility = null): bool
*/ */
public function count(string $visibility = null): int public function count(string $visibility = null): int
{ {
return count($this->search([], $visibility)); return $this->search([], $visibility)->getResultCount();
} }
/** /**
@ -305,10 +323,10 @@ public function save(): void
*/ */
public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
{ {
$bookmarks = $this->search(['searchtags' => $filteringTags], $visibility); $searchResult = $this->search(['searchtags' => $filteringTags], $visibility);
$tags = []; $tags = [];
$caseMapping = []; $caseMapping = [];
foreach ($bookmarks as $bookmark) { foreach ($searchResult->getBookmarks() as $bookmark) {
foreach ($bookmark->getTags() as $tag) { foreach ($bookmark->getTags() as $tag) {
if ( if (
empty($tag) empty($tag)
@ -357,7 +375,7 @@ public function findByDate(
$previous = null; $previous = null;
$next = null; $next = null;
foreach ($this->search([], null, false, false, true) as $bookmark) { foreach ($this->search([], null, false, false, true)->getBookmarks() as $bookmark) {
if ($to < $bookmark->getCreated()) { if ($to < $bookmark->getCreated()) {
$next = $bookmark->getCreated(); $next = $bookmark->getCreated();
} elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) { } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
@ -378,7 +396,7 @@ public function findByDate(
*/ */
public function getLatest(): ?Bookmark public function getLatest(): ?Bookmark
{ {
foreach ($this->search([], null, false, false, true) as $bookmark) { foreach ($this->search([], null, false, false, true)->getBookmarks() as $bookmark) {
return $bookmark; return $bookmark;
} }

View file

@ -4,9 +4,9 @@
namespace Shaarli\Bookmark; namespace Shaarli\Bookmark;
use Exception;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager;
/** /**
* Class LinkFilter. * Class LinkFilter.
@ -30,11 +30,6 @@ class BookmarkFilter
*/ */
public static $FILTER_TAG = 'tags'; public static $FILTER_TAG = 'tags';
/**
* @var string filter by day.
*/
public static $FILTER_DAY = 'FILTER_DAY';
/** /**
* @var string filter by day. * @var string filter by day.
*/ */
@ -62,13 +57,17 @@ class BookmarkFilter
/** @var ConfigManager */ /** @var ConfigManager */
protected $conf; protected $conf;
/** @var PluginManager */
protected $pluginManager;
/** /**
* @param Bookmark[] $bookmarks initialization. * @param Bookmark[] $bookmarks initialization.
*/ */
public function __construct($bookmarks, ConfigManager $conf) public function __construct($bookmarks, ConfigManager $conf, PluginManager $pluginManager)
{ {
$this->bookmarks = $bookmarks; $this->bookmarks = $bookmarks;
$this->conf = $conf; $this->conf = $conf;
$this->pluginManager = $pluginManager;
} }
/** /**
@ -112,12 +111,12 @@ public function filter(
$filtered = $this->bookmarks; $filtered = $this->bookmarks;
} }
if (!empty($request[0])) { if (!empty($request[0])) {
$filtered = (new BookmarkFilter($filtered, $this->conf)) $filtered = (new BookmarkFilter($filtered, $this->conf, $this->pluginManager))
->filterTags($request[0], $casesensitive, $visibility) ->filterTags($request[0], $casesensitive, $visibility)
; ;
} }
if (!empty($request[1])) { if (!empty($request[1])) {
$filtered = (new BookmarkFilter($filtered, $this->conf)) $filtered = (new BookmarkFilter($filtered, $this->conf, $this->pluginManager))
->filterFulltext($request[1], $visibility) ->filterFulltext($request[1], $visibility)
; ;
} }
@ -130,8 +129,6 @@ public function filter(
} else { } else {
return $this->filterTags($request, $casesensitive, $visibility); return $this->filterTags($request, $casesensitive, $visibility);
} }
case self::$FILTER_DAY:
return $this->filterDay($request, $visibility);
default: default:
return $this->noFilter($visibility); return $this->noFilter($visibility);
} }
@ -146,13 +143,20 @@ public function filter(
*/ */
private function noFilter(string $visibility = 'all') private function noFilter(string $visibility = 'all')
{ {
if ($visibility === 'all') {
return $this->bookmarks;
}
$out = []; $out = [];
foreach ($this->bookmarks as $key => $value) { foreach ($this->bookmarks as $key => $value) {
if ($value->isPrivate() && $visibility === 'private') { if (
!$this->pluginManager->filterSearchEntry(
$value,
['source' => 'no_filter', 'visibility' => $visibility]
)
) {
continue;
}
if ($visibility === 'all') {
$out[$key] = $value;
} elseif ($value->isPrivate() && $visibility === 'private') {
$out[$key] = $value; $out[$key] = $value;
} elseif (!$value->isPrivate() && $visibility === 'public') { } elseif (!$value->isPrivate() && $visibility === 'public') {
$out[$key] = $value; $out[$key] = $value;
@ -233,18 +237,34 @@ private function filterFulltext(string $searchterms, string $visibility = 'all')
} }
// Iterate over every stored link. // Iterate over every stored link.
foreach ($this->bookmarks as $id => $link) { foreach ($this->bookmarks as $id => $bookmark) {
if (
!$this->pluginManager->filterSearchEntry(
$bookmark,
[
'source' => 'fulltext',
'searchterms' => $searchterms,
'andSearch' => $andSearch,
'exactSearch' => $exactSearch,
'excludeSearch' => $excludeSearch,
'visibility' => $visibility
]
)
) {
continue;
}
// ignore non private bookmarks when 'privatonly' is on. // ignore non private bookmarks when 'privatonly' is on.
if ($visibility !== 'all') { if ($visibility !== 'all') {
if (!$link->isPrivate() && $visibility === 'private') { if (!$bookmark->isPrivate() && $visibility === 'private') {
continue; continue;
} elseif ($link->isPrivate() && $visibility === 'public') { } elseif ($bookmark->isPrivate() && $visibility === 'public') {
continue; continue;
} }
} }
$lengths = []; $lengths = [];
$content = $this->buildFullTextSearchableLink($link, $lengths); $content = $this->buildFullTextSearchableLink($bookmark, $lengths);
// Be optimistic // Be optimistic
$found = true; $found = true;
@ -270,68 +290,18 @@ private function filterFulltext(string $searchterms, string $visibility = 'all')
} }
if ($found !== false) { if ($found !== false) {
$link->addAdditionalContentEntry( $bookmark->addAdditionalContentEntry(
'search_highlight', 'search_highlight',
$this->postProcessFoundPositions($lengths, $foundPositions) $this->postProcessFoundPositions($lengths, $foundPositions)
); );
$filtered[$id] = $link; $filtered[$id] = $bookmark;
} }
} }
return $filtered; return $filtered;
} }
/**
* generate a regex fragment out of a tag
*
* @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
*
* @return string generated regex fragment
*/
protected function tag2regex(string $tag): string
{
$tagsSeparator = $this->conf->get('general.tags_separator', ' ');
$len = strlen($tag);
if (!$len || $tag === "-" || $tag === "*") {
// nothing to search, return empty regex
return '';
}
if ($tag[0] === "-") {
// query is negated
$i = 1; // use offset to start after '-' character
$regex = '(?!'; // create negative lookahead
} else {
$i = 0; // start at first character
$regex = '(?='; // use positive lookahead
}
// before tag may only be the separator or the beginning
$regex .= '.*(?:^|' . $tagsSeparator . ')';
// iterate over string, separating it into placeholder and content
for (; $i < $len; $i++) {
if ($tag[$i] === '*') {
// placeholder found
$regex .= '[^' . $tagsSeparator . ']*?';
} else {
// regular characters
$offset = strpos($tag, '*', $i);
if ($offset === false) {
// no placeholder found, set offset to end of string
$offset = $len;
}
// subtract one, as we want to get before the placeholder or end of string
$offset -= 1;
// we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
$regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
// move $i on
$i = $offset;
}
}
// after the tag may only be the separator or the end
$regex .= '(?:$|' . $tagsSeparator . '))';
return $regex;
}
/** /**
* Returns the list of bookmarks associated with a given list of tags * Returns the list of bookmarks associated with a given list of tags
* *
@ -381,25 +351,39 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
$filtered = []; $filtered = [];
// iterate over each link // iterate over each link
foreach ($this->bookmarks as $key => $link) { foreach ($this->bookmarks as $key => $bookmark) {
if (
!$this->pluginManager->filterSearchEntry(
$bookmark,
[
'source' => 'tags',
'tags' => $tags,
'casesensitive' => $casesensitive,
'visibility' => $visibility
]
)
) {
continue;
}
// check level of visibility // check level of visibility
// ignore non private bookmarks when 'privateonly' is on. // ignore non private bookmarks when 'privateonly' is on.
if ($visibility !== 'all') { if ($visibility !== 'all') {
if (!$link->isPrivate() && $visibility === 'private') { if (!$bookmark->isPrivate() && $visibility === 'private') {
continue; continue;
} elseif ($link->isPrivate() && $visibility === 'public') { } elseif ($bookmark->isPrivate() && $visibility === 'public') {
continue; continue;
} }
} }
// build search string, start with tags of current link // build search string, start with tags of current link
$search = $link->getTagsString($tagsSeparator); $search = $bookmark->getTagsString($tagsSeparator);
if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) { if (strlen(trim($bookmark->getDescription())) && strpos($bookmark->getDescription(), '#') !== false) {
// description given and at least one possible tag found // description given and at least one possible tag found
$descTags = []; $descTags = [];
// find all tags in the form of #tag in the description // find all tags in the form of #tag in the description
preg_match_all( preg_match_all(
'/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm', '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
$link->getDescription(), $bookmark->getDescription(),
$descTags $descTags
); );
if (count($descTags[1])) { if (count($descTags[1])) {
@ -412,8 +396,9 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
// this entry does _not_ match our regex // this entry does _not_ match our regex
continue; continue;
} }
$filtered[$key] = $link; $filtered[$key] = $bookmark;
} }
return $filtered; return $filtered;
} }
@ -426,56 +411,31 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
*/ */
public function filterUntagged(string $visibility) public function filterUntagged(string $visibility)
{ {
$filtered = [];
foreach ($this->bookmarks as $key => $link) {
if ($visibility !== 'all') {
if (!$link->isPrivate() && $visibility === 'private') {
continue;
} elseif ($link->isPrivate() && $visibility === 'public') {
continue;
}
}
if (empty($link->getTags())) {
$filtered[$key] = $link;
}
}
return $filtered;
}
/**
* Returns the list of articles for a given day, chronologically sorted
*
* Day must be in the form 'YYYYMMDD' (e.g. '20120125'), e.g.
* print_r($mydb->filterDay('20120125'));
*
* @param string $day day to filter.
* @param string $visibility return only all/private/public bookmarks.
* @return Bookmark[] all link matching given day.
*
* @throws Exception if date format is invalid.
*/
public function filterDay(string $day, string $visibility)
{
if (!checkDateFormat('Ymd', $day)) {
throw new Exception('Invalid date format');
}
$filtered = []; $filtered = [];
foreach ($this->bookmarks as $key => $bookmark) { foreach ($this->bookmarks as $key => $bookmark) {
if ($visibility === static::$PUBLIC && $bookmark->isPrivate()) { if (
!$this->pluginManager->filterSearchEntry(
$bookmark,
['source' => 'untagged', 'visibility' => $visibility]
)
) {
continue; continue;
} }
if ($bookmark->getCreated()->format('Ymd') == $day) { if ($visibility !== 'all') {
if (!$bookmark->isPrivate() && $visibility === 'private') {
continue;
} elseif ($bookmark->isPrivate() && $visibility === 'public') {
continue;
}
}
if (empty($bookmark->getTags())) {
$filtered[$key] = $bookmark; $filtered[$key] = $bookmark;
} }
} }
// sort by date ASC return $filtered;
return array_reverse($filtered, true);
} }
/** /**
@ -497,6 +457,56 @@ public static function tagsStrToArray(string $tags, bool $casesensitive): array
return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY); return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
} }
/**
* generate a regex fragment out of a tag
*
* @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
*
* @return string generated regex fragment
*/
protected function tag2regex(string $tag): string
{
$tagsSeparator = $this->conf->get('general.tags_separator', ' ');
$len = strlen($tag);
if (!$len || $tag === "-" || $tag === "*") {
// nothing to search, return empty regex
return '';
}
if ($tag[0] === "-") {
// query is negated
$i = 1; // use offset to start after '-' character
$regex = '(?!'; // create negative lookahead
} else {
$i = 0; // start at first character
$regex = '(?='; // use positive lookahead
}
// before tag may only be the separator or the beginning
$regex .= '.*(?:^|' . $tagsSeparator . ')';
// iterate over string, separating it into placeholder and content
for (; $i < $len; $i++) {
if ($tag[$i] === '*') {
// placeholder found
$regex .= '[^' . $tagsSeparator . ']*?';
} else {
// regular characters
$offset = strpos($tag, '*', $i);
if ($offset === false) {
// no placeholder found, set offset to end of string
$offset = $len;
}
// subtract one, as we want to get before the placeholder or end of string
$offset -= 1;
// we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
$regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
// move $i on
$i = $offset;
}
}
// after the tag may only be the separator or the end
$regex .= '(?:$|' . $tagsSeparator . '))';
return $regex;
}
/** /**
* This method finalize the content of the foundPositions array, * This method finalize the content of the foundPositions array,
* by associated all search results to their associated bookmark field, * by associated all search results to their associated bookmark field,

View file

@ -4,6 +4,7 @@
namespace Shaarli\Bookmark; namespace Shaarli\Bookmark;
use malkusch\lock\exception\LockAcquireException;
use malkusch\lock\mutex\Mutex; use malkusch\lock\mutex\Mutex;
use malkusch\lock\mutex\NoMutex; use malkusch\lock\mutex\NoMutex;
use Shaarli\Bookmark\Exception\DatastoreNotInitializedException; use Shaarli\Bookmark\Exception\DatastoreNotInitializedException;
@ -80,7 +81,7 @@ public function read()
} }
$content = null; $content = null;
$this->mutex->synchronized(function () use (&$content) { $this->synchronized(function () use (&$content) {
$content = file_get_contents($this->datastore); $content = file_get_contents($this->datastore);
}); });
@ -119,11 +120,28 @@ public function write($links)
$data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix; $data = self::$phpPrefix . base64_encode(gzdeflate(serialize($links))) . self::$phpSuffix;
$this->mutex->synchronized(function () use ($data) { $this->synchronized(function () use ($data) {
file_put_contents( file_put_contents(
$this->datastore, $this->datastore,
$data $data
); );
}); });
} }
/**
* Wrapper applying mutex to provided function.
* If the lock can't be acquired (e.g. some shared hosting provider), we execute the function without mutex.
*
* @see https://github.com/shaarli/Shaarli/issues/1650
*
* @param callable $function
*/
protected function synchronized(callable $function): void
{
try {
$this->mutex->synchronized($function);
} catch (LockAcquireException $exception) {
$function();
}
}
} }

View file

@ -44,16 +44,18 @@ public function findByUrl(string $url): ?Bookmark;
* @param bool $caseSensitive * @param bool $caseSensitive
* @param bool $untaggedOnly * @param bool $untaggedOnly
* @param bool $ignoreSticky * @param bool $ignoreSticky
* @param array $pagination This array can contain the following keys for pagination: limit, offset.
* *
* @return Bookmark[] * @return SearchResult
*/ */
public function search( public function search(
array $request = [], array $request = [],
string $visibility = null, string $visibility = null,
bool $caseSensitive = false, bool $caseSensitive = false,
bool $untaggedOnly = false, bool $untaggedOnly = false,
bool $ignoreSticky = false bool $ignoreSticky = false,
); array $pagination = []
): SearchResult;
/** /**
* Get a single bookmark by its ID. * Get a single bookmark by its ID.

View file

@ -1,6 +1,7 @@
<?php <?php
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Formatter\BookmarkDefaultFormatter;
/** /**
* Extract title from an HTML document. * Extract title from an HTML document.
@ -68,11 +69,13 @@ function html_extract_tag($tag, $html)
$properties = implode('|', $propertiesKey); $properties = implode('|', $propertiesKey);
// We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"' // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"'
$orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]'; $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]';
// Support quotes in double quoted content, and the other way around
$content = 'content=(["\'])((?:(?!\1).)*)\1';
// Try to retrieve OpenGraph tag. // Try to retrieve OpenGraph tag.
$ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; $ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*' . $content . '.*?>#';
// If the attributes are not in the order property => content (e.g. Github) // If the attributes are not in the order property => content (e.g. Github)
// New regex to keep this readable... more or less. // New regex to keep this readable... more or less.
$ogRegexReverse = '#<meta[^>]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#'; $ogRegexReverse = '#<meta[^>]+' . $content . '[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#';
if ( if (
preg_match($ogRegex, $html, $matches) > 0 preg_match($ogRegex, $html, $matches) > 0
@ -96,7 +99,18 @@ function html_extract_tag($tag, $html)
function text2clickable($text) function text2clickable($text)
{ {
$regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si'; $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
return preg_replace($regex, '<a href="$1">$1</a>', $text); $format = function (array $match): string {
return '<a href="' .
str_replace(
BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN,
'',
str_replace(BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE, '', $match[1])
) .
'">' . $match[1] . '</a>'
;
};
return preg_replace_callback($regex, $format, $text);
} }
/** /**
@ -109,6 +123,9 @@ function text2clickable($text)
*/ */
function hashtag_autolink($description, $indexUrl = '') function hashtag_autolink($description, $indexUrl = '')
{ {
$tokens = '(?:' . BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN . ')' .
'(?:' . BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE . ')'
;
/* /*
* To support unicode: http://stackoverflow.com/a/35498078/1484919 * To support unicode: http://stackoverflow.com/a/35498078/1484919
* \p{Pc} - to match underscore * \p{Pc} - to match underscore
@ -116,9 +133,20 @@ function hashtag_autolink($description, $indexUrl = '')
* \p{L} - letter from any language * \p{L} - letter from any language
* \p{Mn} - any non marking space (accents, umlauts, etc) * \p{Mn} - any non marking space (accents, umlauts, etc)
*/ */
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}' . $tokens . ']+)/mui';
$replacement = '$1<a href="' . $indexUrl . './add-tag/$2" title="Hashtag $2">#$2</a>'; $format = function (array $match) use ($indexUrl): string {
return preg_replace($regex, $replacement, $description); $cleanMatch = str_replace(
BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN,
'',
str_replace(BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE, '', $match[2])
);
return $match[1] . '<a href="' . $indexUrl . './add-tag/' . $cleanMatch . '"' .
' title="Hashtag ' . $cleanMatch . '">' .
'#' . $match[2] .
'</a>';
};
return preg_replace_callback($regex, $format, $description);
} }
/** /**

View file

@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace Shaarli\Bookmark;
/**
* Read-only class used to represent search result, including pagination.
*/
class SearchResult
{
/** @var Bookmark[] List of result bookmarks with pagination applied */
protected $bookmarks;
/** @var int number of Bookmarks found, with pagination applied */
protected $resultCount;
/** @var int total number of result found */
protected $totalCount;
/** @var int pagination: limit number of result bookmarks */
protected $limit;
/** @var int pagination: offset to apply to complete result list */
protected $offset;
public function __construct(array $bookmarks, int $totalCount, int $offset, ?int $limit)
{
$this->bookmarks = $bookmarks;
$this->resultCount = count($bookmarks);
$this->totalCount = $totalCount;
$this->limit = $limit;
$this->offset = $offset;
}
/**
* Build a SearchResult from provided full result set and pagination settings.
*
* @param Bookmark[] $bookmarks Full set of result which will be filtered
* @param int $offset Start recording results from $offset
* @param int|null $limit End recording results after $limit bookmarks is reached
* @param bool $allowOutOfBounds Set to false to display the last page if the offset is out of bound,
* return empty result set otherwise (default: false)
*
* @return SearchResult
*/
public static function getSearchResult(
$bookmarks,
int $offset = 0,
?int $limit = null,
bool $allowOutOfBounds = false
): self {
$totalCount = count($bookmarks);
if (!$allowOutOfBounds && $offset > $totalCount) {
$offset = $limit === null ? 0 : $limit * -1;
}
if ($bookmarks instanceof BookmarkArray) {
$buffer = [];
foreach ($bookmarks as $key => $value) {
$buffer[$key] = $value;
}
$bookmarks = $buffer;
}
return new static(
array_slice($bookmarks, $offset, $limit, true),
$totalCount,
$offset,
$limit
);
}
/** @return Bookmark[] List of result bookmarks with pagination applied */
public function getBookmarks(): array
{
return $this->bookmarks;
}
/** @return int number of Bookmarks found, with pagination applied */
public function getResultCount(): int
{
return $this->resultCount;
}
/** @return int total number of result found */
public function getTotalCount(): int
{
return $this->totalCount;
}
/** @return int pagination: limit number of result bookmarks */
public function getLimit(): ?int
{
return $this->limit;
}
/** @return int pagination: offset to apply to complete result list */
public function getOffset(): int
{
return $this->offset;
}
/** @return int Current page of result set in complete results */
public function getPage(): int
{
if (empty($this->limit)) {
return $this->offset === 0 ? 1 : 2;
}
$base = $this->offset >= 0 ? $this->offset : $this->totalCount + $this->offset;
return (int) ceil($base / $this->limit) + 1;
}
/** @return int Get the # of the last page */
public function getLastPage(): int
{
if (empty($this->limit)) {
return $this->offset === 0 ? 1 : 2;
}
return (int) ceil($this->totalCount / $this->limit);
}
/** @return bool Either the current page is the last one or not */
public function isLastPage(): bool
{
return $this->getPage() === $this->getLastPage();
}
/** @return bool Either the current page is the first one or not */
public function isFirstPage(): bool
{
return $this->offset === 0;
}
}

View file

@ -50,6 +50,9 @@ class ContainerBuilder
/** @var LoginManager */ /** @var LoginManager */
protected $login; protected $login;
/** @var PluginManager */
protected $pluginManager;
/** @var LoggerInterface */ /** @var LoggerInterface */
protected $logger; protected $logger;
@ -61,12 +64,14 @@ public function __construct(
SessionManager $session, SessionManager $session,
CookieManager $cookieManager, CookieManager $cookieManager,
LoginManager $login, LoginManager $login,
PluginManager $pluginManager,
LoggerInterface $logger LoggerInterface $logger
) { ) {
$this->conf = $conf; $this->conf = $conf;
$this->session = $session; $this->session = $session;
$this->login = $login; $this->login = $login;
$this->cookieManager = $cookieManager; $this->cookieManager = $cookieManager;
$this->pluginManager = $pluginManager;
$this->logger = $logger; $this->logger = $logger;
} }
@ -78,12 +83,10 @@ public function build(): ShaarliContainer
$container['sessionManager'] = $this->session; $container['sessionManager'] = $this->session;
$container['cookieManager'] = $this->cookieManager; $container['cookieManager'] = $this->cookieManager;
$container['loginManager'] = $this->login; $container['loginManager'] = $this->login;
$container['pluginManager'] = $this->pluginManager;
$container['logger'] = $this->logger; $container['logger'] = $this->logger;
$container['basePath'] = $this->basePath; $container['basePath'] = $this->basePath;
$container['plugins'] = function (ShaarliContainer $container): PluginManager {
return new PluginManager($container->conf);
};
$container['history'] = function (ShaarliContainer $container): History { $container['history'] = function (ShaarliContainer $container): History {
return new History($container->conf->get('resource.history')); return new History($container->conf->get('resource.history'));
@ -92,6 +95,7 @@ public function build(): ShaarliContainer
$container['bookmarkService'] = function (ShaarliContainer $container): BookmarkServiceInterface { $container['bookmarkService'] = function (ShaarliContainer $container): BookmarkServiceInterface {
return new BookmarkFileService( return new BookmarkFileService(
$container->conf, $container->conf,
$container->pluginManager,
$container->history, $container->history,
new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2), new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
$container->loginManager->isLoggedIn() $container->loginManager->isLoggedIn()
@ -113,14 +117,6 @@ public function build(): ShaarliContainer
); );
}; };
$container['pluginManager'] = function (ShaarliContainer $container): PluginManager {
$pluginManager = new PluginManager($container->conf);
$pluginManager->load($container->conf->get('general.enabled_plugins'));
return $pluginManager;
};
$container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory { $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
return new FormatterFactory( return new FormatterFactory(
$container->conf, $container->conf,

View file

@ -1,20 +1,27 @@
<?php <?php
declare(strict_types=1);
namespace Shaarli\Feed; namespace Shaarli\Feed;
use DatePeriod;
/** /**
* Simple cache system, mainly for the RSS/ATOM feeds * Simple cache system, mainly for the RSS/ATOM feeds
*/ */
class CachedPage class CachedPage
{ {
// Directory containing page caches /** Directory containing page caches */
private $cacheDir; protected $cacheDir;
// Should this URL be cached (boolean)? /** Should this URL be cached (boolean)? */
private $shouldBeCached; protected $shouldBeCached;
// Name of the cache file for this URL /** Name of the cache file for this URL */
private $filename; protected $filename;
/** @var DatePeriod|null Optionally specify a period of time for cache validity */
protected $validityPeriod;
/** /**
* Creates a new CachedPage * Creates a new CachedPage
@ -22,13 +29,15 @@ class CachedPage
* @param string $cacheDir page cache directory * @param string $cacheDir page cache directory
* @param string $url page URL * @param string $url page URL
* @param bool $shouldBeCached whether this page needs to be cached * @param bool $shouldBeCached whether this page needs to be cached
* @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
*/ */
public function __construct($cacheDir, $url, $shouldBeCached) public function __construct($cacheDir, $url, $shouldBeCached, ?DatePeriod $validityPeriod)
{ {
// TODO: check write access to the cache directory // TODO: check write access to the cache directory
$this->cacheDir = $cacheDir; $this->cacheDir = $cacheDir;
$this->filename = $this->cacheDir . '/' . sha1($url) . '.cache'; $this->filename = $this->cacheDir . '/' . sha1($url) . '.cache';
$this->shouldBeCached = $shouldBeCached; $this->shouldBeCached = $shouldBeCached;
$this->validityPeriod = $validityPeriod;
} }
/** /**
@ -41,11 +50,21 @@ public function cachedVersion()
if (!$this->shouldBeCached) { if (!$this->shouldBeCached) {
return null; return null;
} }
if (is_file($this->filename)) { if (!is_file($this->filename)) {
return file_get_contents($this->filename);
}
return null; return null;
} }
if ($this->validityPeriod !== null) {
$cacheDate = \DateTime::createFromFormat('U', (string) filemtime($this->filename));
if (
$cacheDate < $this->validityPeriod->getStartDate()
|| $cacheDate > $this->validityPeriod->getEndDate()
) {
return null;
}
}
return file_get_contents($this->filename);
}
/** /**
* Puts a page in the cache * Puts a page in the cache

View file

@ -102,22 +102,16 @@ public function buildData(string $feedType, ?array $userInput)
$userInput['searchtags'] = false; $userInput['searchtags'] = false;
} }
$limit = $this->getLimit($userInput);
// Optionally filter the results: // Optionally filter the results:
$linksToDisplay = $this->linkDB->search($userInput ?? [], null, false, false, true); $searchResult = $this->linkDB->search($userInput ?? [], null, false, false, true, ['limit' => $limit]);
$nblinksToDisplay = $this->getNbLinks(count($linksToDisplay), $userInput);
// Can't use array_keys() because $link is a LinkDB instance and not a real array.
$keys = [];
foreach ($linksToDisplay as $key => $value) {
$keys[] = $key;
}
$pageaddr = escape(index_url($this->serverInfo)); $pageaddr = escape(index_url($this->serverInfo));
$this->formatter->addContextData('index_url', $pageaddr); $this->formatter->addContextData('index_url', $pageaddr);
$linkDisplayed = []; $links = [];
for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) { foreach ($searchResult->getBookmarks() as $key => $bookmark) {
$linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr); $links[$key] = $this->buildItem($feedType, $bookmark, $pageaddr);
} }
$data['language'] = $this->getTypeLanguage($feedType); $data['language'] = $this->getTypeLanguage($feedType);
@ -128,7 +122,7 @@ public function buildData(string $feedType, ?array $userInput)
$data['self_link'] = $pageaddr . $requestUri; $data['self_link'] = $pageaddr . $requestUri;
$data['index_url'] = $pageaddr; $data['index_url'] = $pageaddr;
$data['usepermalinks'] = $this->usePermalinks === true; $data['usepermalinks'] = $this->usePermalinks === true;
$data['links'] = $linkDisplayed; $data['links'] = $links;
return $data; return $data;
} }
@ -268,19 +262,18 @@ protected function getIsoDate(string $feedType, DateTime $date, $format = false)
* If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS. * If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
* If 'nb' is set to 'all', display all filtered bookmarks (max parameter). * If 'nb' is set to 'all', display all filtered bookmarks (max parameter).
* *
* @param int $max maximum number of bookmarks to display.
* @param array $userInput $_GET. * @param array $userInput $_GET.
* *
* @return int number of bookmarks to display. * @return int number of bookmarks to display.
*/ */
protected function getNbLinks($max, ?array $userInput) protected function getLimit(?array $userInput)
{ {
if (empty($userInput['nb'])) { if (empty($userInput['nb'])) {
return self::$DEFAULT_NB_LINKS; return self::$DEFAULT_NB_LINKS;
} }
if ($userInput['nb'] == 'all') { if ($userInput['nb'] == 'all') {
return $max; return null;
} }
$intNb = intval($userInput['nb']); $intNb = intval($userInput['nb']);

View file

@ -12,8 +12,8 @@
*/ */
class BookmarkDefaultFormatter extends BookmarkFormatter class BookmarkDefaultFormatter extends BookmarkFormatter
{ {
protected const SEARCH_HIGHLIGHT_OPEN = '|@@HIGHLIGHT'; public const SEARCH_HIGHLIGHT_OPEN = '||O_HIGHLIGHT';
protected const SEARCH_HIGHLIGHT_CLOSE = 'HIGHLIGHT@@|'; public const SEARCH_HIGHLIGHT_CLOSE = '||C_HIGHLIGHT';
/** /**
* @inheritdoc * @inheritdoc

View file

@ -3,6 +3,7 @@
namespace Shaarli\Formatter; namespace Shaarli\Formatter;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\Parsedown\ShaarliParsedownExtra;
/** /**
* Class BookmarkMarkdownExtraFormatter * Class BookmarkMarkdownExtraFormatter
@ -18,7 +19,6 @@ class BookmarkMarkdownExtraFormatter extends BookmarkMarkdownFormatter
public function __construct(ConfigManager $conf, bool $isLoggedIn) public function __construct(ConfigManager $conf, bool $isLoggedIn)
{ {
parent::__construct($conf, $isLoggedIn); parent::__construct($conf, $isLoggedIn);
$this->parsedown = new ShaarliParsedownExtra();
$this->parsedown = new \ParsedownExtra();
} }
} }

View file

@ -3,6 +3,7 @@
namespace Shaarli\Formatter; namespace Shaarli\Formatter;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\Parsedown\ShaarliParsedown;
/** /**
* Class BookmarkMarkdownFormatter * Class BookmarkMarkdownFormatter
@ -42,7 +43,7 @@ public function __construct(ConfigManager $conf, bool $isLoggedIn)
{ {
parent::__construct($conf, $isLoggedIn); parent::__construct($conf, $isLoggedIn);
$this->parsedown = new \Parsedown(); $this->parsedown = new ShaarliParsedown();
$this->escape = $conf->get('security.markdown_escape', true); $this->escape = $conf->get('security.markdown_escape', true);
$this->allowedProtocols = $conf->get('security.allowed_protocols', []); $this->allowedProtocols = $conf->get('security.allowed_protocols', []);
} }
@ -128,6 +129,9 @@ function ($match) use ($allowedProtocols, $indexUrl) {
protected function formatHashTags($description) protected function formatHashTags($description)
{ {
$indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : ''; $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
$tokens = '(?:' . BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN . ')' .
'(?:' . BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE . ')'
;
/* /*
* To support unicode: http://stackoverflow.com/a/35498078/1484919 * To support unicode: http://stackoverflow.com/a/35498078/1484919
@ -136,8 +140,15 @@ protected function formatHashTags($description)
* \p{L} - letter from any language * \p{L} - letter from any language
* \p{Mn} - any non marking space (accents, umlauts, etc) * \p{Mn} - any non marking space (accents, umlauts, etc)
*/ */
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui'; $regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}' . $tokens . ']+)/mui';
$replacement = '$1[#$2](' . $indexUrl . './add-tag/$2)'; $replacement = function (array $match) use ($indexUrl): string {
$cleanMatch = str_replace(
BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN,
'',
str_replace(BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE, '', $match[2])
);
return $match[1] . '[#' . $match[2] . '](' . $indexUrl . './add-tag/' . $cleanMatch . ')';
};
$descriptionLines = explode(PHP_EOL, $description); $descriptionLines = explode(PHP_EOL, $description);
$descriptionOut = ''; $descriptionOut = '';
@ -156,7 +167,7 @@ protected function formatHashTags($description)
} }
if (!$codeBlockOn && !$codeLineOn) { if (!$codeBlockOn && !$codeLineOn) {
$descriptionLine = preg_replace($regex, $replacement, $descriptionLine); $descriptionLine = preg_replace_callback($regex, $replacement, $descriptionLine);
} }
$descriptionOut .= $descriptionLine; $descriptionOut .= $descriptionLine;

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shaarli\Formatter\Parsedown;
/**
* Parsedown extension for Shaarli.
*
* Extension for both Parsedown and ParsedownExtra centralized in ShaarliParsedownTrait.
*/
class ShaarliParsedown extends \Parsedown
{
use ShaarliParsedownTrait;
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shaarli\Formatter\Parsedown;
/**
* ParsedownExtra extension for Shaarli.
*
* Extension for both Parsedown and ParsedownExtra centralized in ShaarliParsedownTrait.
*/
class ShaarliParsedownExtra extends \ParsedownExtra
{
use ShaarliParsedownTrait;
}

View file

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Shaarli\Formatter\Parsedown;
use Shaarli\Formatter\BookmarkDefaultFormatter as Formatter;
/**
* Trait used for Parsedown and ParsedownExtra extension.
*
* Extended:
* - Format links properly in search context
*/
trait ShaarliParsedownTrait
{
/**
* @inheritDoc
*/
protected function inlineLink($excerpt)
{
return $this->shaarliFormatLink(parent::inlineLink($excerpt), true);
}
/**
* @inheritDoc
*/
protected function inlineUrl($excerpt)
{
return $this->shaarliFormatLink(parent::inlineUrl($excerpt), false);
}
/**
* Properly format markdown link:
* - remove highlight tags from HREF attribute
* - (optional) add highlight tags to link caption
*
* @param array|null $link Parsedown formatted link array.
* It can be empty.
* @param bool $fullWrap Add highlight tags the whole link caption
*
* @return array|null
*/
protected function shaarliFormatLink(?array $link, bool $fullWrap): ?array
{
// If open and clean search tokens are found in the link, process.
if (
is_array($link)
&& strpos($link['element']['attributes']['href'] ?? '', Formatter::SEARCH_HIGHLIGHT_OPEN) !== false
&& strpos($link['element']['attributes']['href'] ?? '', Formatter::SEARCH_HIGHLIGHT_CLOSE) !== false
) {
$link['element']['attributes']['href'] = $this->shaarliRemoveSearchTokens(
$link['element']['attributes']['href']
);
if ($fullWrap) {
$link['element']['text'] = Formatter::SEARCH_HIGHLIGHT_OPEN .
$link['element']['text'] .
Formatter::SEARCH_HIGHLIGHT_CLOSE
;
}
}
return $link;
}
/**
* Remove open and close tags from provided string.
*
* @param string $entry input
*
* @return string Striped input
*/
protected function shaarliRemoveSearchTokens(string $entry): string
{
$entry = str_replace(Formatter::SEARCH_HIGHLIGHT_OPEN, '', $entry);
$entry = str_replace(Formatter::SEARCH_HIGHLIGHT_CLOSE, '', $entry);
return $entry;
}
}

View file

@ -57,9 +57,12 @@ public function save(Request $request, Response $response): Response
} }
// TODO: move this to bookmark service // TODO: move this to bookmark service
$count = 0; $searchResult = $this->container->bookmarkService->search(
$bookmarks = $this->container->bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true); ['searchtags' => $fromTag],
foreach ($bookmarks as $bookmark) { BookmarkFilter::$ALL,
true
);
foreach ($searchResult->getBookmarks() as $bookmark) {
if (false === $isDelete) { if (false === $isDelete) {
$bookmark->renameTag($fromTag, $toTag); $bookmark->renameTag($fromTag, $toTag);
} else { } else {
@ -68,11 +71,11 @@ public function save(Request $request, Response $response): Response
$this->container->bookmarkService->set($bookmark, false); $this->container->bookmarkService->set($bookmark, false);
$this->container->history->updateLink($bookmark); $this->container->history->updateLink($bookmark);
$count++;
} }
$this->container->bookmarkService->save(); $this->container->bookmarkService->save();
$count = $searchResult->getResultCount();
if (true === $isDelete) { if (true === $isDelete) {
$alert = sprintf( $alert = sprintf(
t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count), t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count),

View file

@ -39,11 +39,16 @@ public function index(Request $request, Response $response): Response
$currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion; $currentVersion = $currentVersion === 'dev' ? $currentVersion : 'v' . $currentVersion;
$phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
$permissions = array_merge(
ApplicationUtils::checkResourcePermissions($this->container->conf),
ApplicationUtils::checkDatastoreMutex()
);
$this->assignView('php_version', PHP_VERSION); $this->assignView('php_version', PHP_VERSION);
$this->assignView('php_eol', format_date($phpEol, false)); $this->assignView('php_eol', format_date($phpEol, false));
$this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
$this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
$this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); $this->assignView('permissions', $permissions);
$this->assignView('release_url', $releaseUrl); $this->assignView('release_url', $releaseUrl);
$this->assignView('latest_version', $latestVersion); $this->assignView('latest_version', $latestVersion);
$this->assignView('current_version', $currentVersion); $this->assignView('current_version', $currentVersion);

View file

@ -66,6 +66,10 @@ public function deleteBookmark(Request $request, Response $response): Response
return $response->write('<script>self.close();</script>'); return $response->write('<script>self.close();</script>');
} }
if ($request->getParam('source') === 'batch') {
return $response->withStatus(204);
}
// Don't redirect to permalink after deletion. // Don't redirect to permalink after deletion.
return $this->redirectFromReferer($request, $response, ['shaare/']); return $this->redirectFromReferer($request, $response, ['shaare/']);
} }

View file

@ -227,7 +227,7 @@ protected function buildLinkDataFromUrl(Request $request, string $url): array
protected function buildFormData(array $link, bool $isNew, Request $request): array protected function buildFormData(array $link, bool $isNew, Request $request): array
{ {
$link['tags'] = strlen($link['tags']) > 0 $link['tags'] = $link['tags'] !== null && strlen($link['tags']) > 0
? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ') ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ')
: $link['tags'] : $link['tags']
; ;

View file

@ -22,7 +22,7 @@ class ThumbnailsController extends ShaarliAdminController
public function index(Request $request, Response $response): Response public function index(Request $request, Response $response): Response
{ {
$ids = []; $ids = [];
foreach ($this->container->bookmarkService->search() as $bookmark) { foreach ($this->container->bookmarkService->search()->getBookmarks() as $bookmark) {
// A note or not HTTP(S) // A note or not HTTP(S)
if ($bookmark->isNote() || !startsWith(strtolower($bookmark->getUrl()), 'http')) { if ($bookmark->isNote() || !startsWith(strtolower($bookmark->getUrl()), 'http')) {
continue; continue;

View file

@ -33,10 +33,10 @@ public function index(Request $request, Response $response): Response
$formatter = $this->container->formatterFactory->getFormatter(); $formatter = $this->container->formatterFactory->getFormatter();
$formatter->addContextData('base_path', $this->container->basePath); $formatter->addContextData('base_path', $this->container->basePath);
$formatter->addContextData('index_url', index_url($this->container->environment));
$searchTags = normalize_spaces($request->getParam('searchtags') ?? ''); $searchTags = normalize_spaces($request->getParam('searchtags') ?? '');
$searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? '')); $searchTerm = escape(normalize_spaces($request->getParam('searchterm') ?? ''));
;
// Filter bookmarks according search parameters. // Filter bookmarks according search parameters.
$visibility = $this->container->sessionManager->getSessionParameter('visibility'); $visibility = $this->container->sessionManager->getSessionParameter('visibility');
@ -44,39 +44,26 @@ public function index(Request $request, Response $response): Response
'searchtags' => $searchTags, 'searchtags' => $searchTags,
'searchterm' => $searchTerm, 'searchterm' => $searchTerm,
]; ];
$linksToDisplay = $this->container->bookmarkService->search(
// Select articles according to paging.
$page = (int) ($request->getParam('page') ?? 1);
$page = $page < 1 ? 1 : $page;
$linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20;
$searchResult = $this->container->bookmarkService->search(
$search, $search,
$visibility, $visibility,
false, false,
!!$this->container->sessionManager->getSessionParameter('untaggedonly') !!$this->container->sessionManager->getSessionParameter('untaggedonly'),
false,
['offset' => $linksPerPage * ($page - 1), 'limit' => $linksPerPage]
) ?? []; ) ?? [];
// ---- Handle paging.
$keys = [];
foreach ($linksToDisplay as $key => $value) {
$keys[] = $key;
}
$linksPerPage = $this->container->sessionManager->getSessionParameter('LINKS_PER_PAGE', 20) ?: 20;
// Select articles according to paging.
$pageCount = (int) ceil(count($keys) / $linksPerPage) ?: 1;
$page = (int) $request->getParam('page') ?? 1;
$page = $page < 1 ? 1 : $page;
$page = $page > $pageCount ? $pageCount : $page;
// Start index.
$i = ($page - 1) * $linksPerPage;
$end = $i + $linksPerPage;
$linkDisp = [];
$save = false; $save = false;
while ($i < $end && $i < count($keys)) { $links = [];
$save = $this->updateThumbnail($linksToDisplay[$keys[$i]], false) || $save; foreach ($searchResult->getBookmarks() as $key => $bookmark) {
$link = $formatter->format($linksToDisplay[$keys[$i]]); $save = $this->updateThumbnail($bookmark, false) || $save;
$links[$key] = $formatter->format($bookmark);
$linkDisp[$keys[$i]] = $link;
$i++;
} }
if ($save) { if ($save) {
@ -86,15 +73,10 @@ public function index(Request $request, Response $response): Response
// Compute paging navigation // Compute paging navigation
$searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags); $searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags);
$searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm); $searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm);
$page = $searchResult->getPage();
$previous_page_url = ''; $previousPageUrl = !$searchResult->isLastPage() ? '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl : '';
if ($i !== count($keys)) { $nextPageUrl = !$searchResult->isFirstPage() ? '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl : '';
$previous_page_url = '?page=' . ($page + 1) . $searchtermUrl . $searchtagsUrl;
}
$next_page_url = '';
if ($page > 1) {
$next_page_url = '?page=' . ($page - 1) . $searchtermUrl . $searchtagsUrl;
}
$tagsSeparator = $this->container->conf->get('general.tags_separator', ' '); $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
$searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator)); $searchTagsUrlEncoded = array_map('urlencode', tags_str2array($searchTags, $tagsSeparator));
@ -104,16 +86,16 @@ public function index(Request $request, Response $response): Response
$data = array_merge( $data = array_merge(
$this->initializeTemplateVars(), $this->initializeTemplateVars(),
[ [
'previous_page_url' => $previous_page_url, 'previous_page_url' => $previousPageUrl,
'next_page_url' => $next_page_url, 'next_page_url' => $nextPageUrl,
'page_current' => $page, 'page_current' => $page,
'page_max' => $pageCount, 'page_max' => $searchResult->getLastPage(),
'result_count' => count($linksToDisplay), 'result_count' => $searchResult->getTotalCount(),
'search_term' => escape($searchTerm), 'search_term' => escape($searchTerm),
'search_tags' => escape($searchTags), 'search_tags' => escape($searchTags),
'search_tags_url' => $searchTagsUrlEncoded, 'search_tags_url' => $searchTagsUrlEncoded,
'visibility' => $visibility, 'visibility' => $visibility,
'links' => $linkDisp, 'links' => $links,
] ]
); );
@ -157,6 +139,7 @@ public function permalink(Request $request, Response $response, array $args): Re
$formatter = $this->container->formatterFactory->getFormatter(); $formatter = $this->container->formatterFactory->getFormatter();
$formatter->addContextData('base_path', $this->container->basePath); $formatter->addContextData('base_path', $this->container->basePath);
$formatter->addContextData('index_url', index_url($this->container->environment));
$data = array_merge( $data = array_merge(
$this->initializeTemplateVars(), $this->initializeTemplateVars(),

View file

@ -86,9 +86,11 @@ public function index(Request $request, Response $response): Response
public function rss(Request $request, Response $response): Response public function rss(Request $request, Response $response): Response
{ {
$response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8'); $response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
$type = DailyPageHelper::extractRequestedType($request);
$cacheDuration = DailyPageHelper::getCacheDatePeriodByType($type);
$pageUrl = page_url($this->container->environment); $pageUrl = page_url($this->container->environment);
$cache = $this->container->pageCacheManager->getCachePage($pageUrl); $cache = $this->container->pageCacheManager->getCachePage($pageUrl, $cacheDuration);
$cached = $cache->cachedVersion(); $cached = $cache->cachedVersion();
if (!empty($cached)) { if (!empty($cached)) {
@ -96,10 +98,9 @@ public function rss(Request $request, Response $response): Response
} }
$days = []; $days = [];
$type = DailyPageHelper::extractRequestedType($request);
$format = DailyPageHelper::getFormatByType($type); $format = DailyPageHelper::getFormatByType($type);
$length = DailyPageHelper::getRssLengthByType($type); $length = DailyPageHelper::getRssLengthByType($type);
foreach ($this->container->bookmarkService->search() as $bookmark) { foreach ($this->container->bookmarkService->search()->getBookmarks() as $bookmark) {
$day = $bookmark->getCreated()->format($format); $day = $bookmark->getCreated()->format($format);
// Stop iterating after DAILY_RSS_NB_DAYS entries // Stop iterating after DAILY_RSS_NB_DAYS entries
@ -131,7 +132,7 @@ public function rss(Request $request, Response $response): Response
$dataPerDay[$day] = [ $dataPerDay[$day] = [
'date' => $endDateTime, 'date' => $endDateTime,
'date_rss' => $endDateTime->format(DateTime::RSS), 'date_rss' => $endDateTime->format(DateTime::RSS),
'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime), 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime, false),
'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day, 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day,
'links' => [], 'links' => [],
]; ];

View file

@ -56,11 +56,16 @@ public function index(Request $request, Response $response): Response
$phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION)); $phpEol = new \DateTimeImmutable(ApplicationUtils::getPhpEol(PHP_VERSION));
$permissions = array_merge(
ApplicationUtils::checkResourcePermissions($this->container->conf),
ApplicationUtils::checkDatastoreMutex()
);
$this->assignView('php_version', PHP_VERSION); $this->assignView('php_version', PHP_VERSION);
$this->assignView('php_eol', format_date($phpEol, false)); $this->assignView('php_eol', format_date($phpEol, false));
$this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable()); $this->assignView('php_has_reached_eol', $phpEol < new \DateTimeImmutable());
$this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement()); $this->assignView('php_extensions', ApplicationUtils::getPhpExtensionsRequirement());
$this->assignView('permissions', ApplicationUtils::checkResourcePermissions($this->container->conf)); $this->assignView('permissions', $permissions);
$this->assignView('pagetitle', t('Install Shaarli')); $this->assignView('pagetitle', t('Install Shaarli'));

View file

@ -30,19 +30,19 @@ public function index(Request $request, Response $response): Response
); );
// Optionally filter the results: // Optionally filter the results:
$links = $this->container->bookmarkService->search($request->getQueryParams()); $bookmarks = $this->container->bookmarkService->search($request->getQueryParams())->getBookmarks();
$linksToDisplay = []; $links = [];
// Get only bookmarks which have a thumbnail. // Get only bookmarks which have a thumbnail.
// Note: we do not retrieve thumbnails here, the request is too heavy. // Note: we do not retrieve thumbnails here, the request is too heavy.
$formatter = $this->container->formatterFactory->getFormatter('raw'); $formatter = $this->container->formatterFactory->getFormatter('raw');
foreach ($links as $key => $link) { foreach ($bookmarks as $key => $bookmark) {
if (!empty($link->getThumbnail())) { if (!empty($bookmark->getThumbnail())) {
$linksToDisplay[] = $formatter->format($link); $links[] = $formatter->format($bookmark);
} }
} }
$data = ['linksToDisplay' => $linksToDisplay]; $data = ['linksToDisplay' => $links];
$this->executePageHooks('render_picwall', $data, TemplatePage::PICTURE_WALL); $this->executePageHooks('render_picwall', $data, TemplatePage::PICTURE_WALL);
foreach ($data as $key => $value) { foreach ($data as $key => $value) {

View file

@ -56,6 +56,10 @@ protected function assignAllView(array $data): self
protected function render(string $template): string protected function render(string $template): string
{ {
// Legacy key that used to be injected by PluginManager
$this->assignView('_PAGE_', $template);
$this->assignView('template', $template);
$this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL)); $this->assignView('linkcount', $this->container->bookmarkService->count(BookmarkFilter::$ALL));
$this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE)); $this->assignView('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE));

View file

@ -3,6 +3,8 @@
namespace Shaarli\Helper; namespace Shaarli\Helper;
use Exception; use Exception;
use malkusch\lock\exception\LockAcquireException;
use malkusch\lock\mutex\FlockMutex;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
/** /**
@ -35,7 +37,7 @@ public static function getLatestGitVersionCode($url, $timeout = 2)
{ {
list($headers, $data) = get_http_response($url, $timeout); list($headers, $data) = get_http_response($url, $timeout);
if (strpos($headers[0], '200 OK') === false) { if (preg_match('#HTTP/[\d\.]+ 200(?: OK)?#', $headers[0]) !== 1) {
error_log('Failed to retrieve ' . $url); error_log('Failed to retrieve ' . $url);
return false; return false;
} }
@ -252,6 +254,20 @@ public static function checkResourcePermissions(ConfigManager $conf, bool $minim
return $errors; return $errors;
} }
public static function checkDatastoreMutex(): array
{
$mutex = new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2);
try {
$mutex->synchronized(function () {
return true;
});
} catch (LockAcquireException $e) {
$errors[] = t('Lock can not be acquired on the datastore. You might encounter concurrent access issues.');
}
return $errors ?? [];
}
/** /**
* Returns a salted hash representing the current Shaarli version. * Returns a salted hash representing the current Shaarli version.
* *

View file

@ -4,6 +4,9 @@
namespace Shaarli\Helper; namespace Shaarli\Helper;
use DatePeriod;
use DateTimeImmutable;
use Exception;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Slim\Http\Request; use Slim\Http\Request;
@ -40,31 +43,31 @@ public static function extractRequestedType(Request $request): string
* @param string|null $requestedDate Input string extracted from the request * @param string|null $requestedDate Input string extracted from the request
* @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date) * @param Bookmark|null $latestBookmark Latest bookmark found in the datastore (by date)
* *
* @return \DateTimeImmutable from input or latest bookmark. * @return DateTimeImmutable from input or latest bookmark.
* *
* @throws \Exception Type not supported. * @throws Exception Type not supported.
*/ */
public static function extractRequestedDateTime( public static function extractRequestedDateTime(
string $type, string $type,
?string $requestedDate, ?string $requestedDate,
Bookmark $latestBookmark = null Bookmark $latestBookmark = null
): \DateTimeImmutable { ): DateTimeImmutable {
$format = static::getFormatByType($type); $format = static::getFormatByType($type);
if (empty($requestedDate)) { if (empty($requestedDate)) {
return $latestBookmark instanceof Bookmark return $latestBookmark instanceof Bookmark
? new \DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM)) ? new DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
: new \DateTimeImmutable() : new DateTimeImmutable()
; ;
} }
// W is not supported by createFromFormat... // W is not supported by createFromFormat...
if ($type === static::WEEK) { if ($type === static::WEEK) {
return (new \DateTimeImmutable()) return (new DateTimeImmutable())
->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2)) ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
; ;
} }
return \DateTimeImmutable::createFromFormat($format, $requestedDate); return DateTimeImmutable::createFromFormat($format, $requestedDate);
} }
/** /**
@ -80,7 +83,7 @@ public static function extractRequestedDateTime(
* *
* @see https://www.php.net/manual/en/datetime.format.php * @see https://www.php.net/manual/en/datetime.format.php
* *
* @throws \Exception Type not supported. * @throws Exception Type not supported.
*/ */
public static function getFormatByType(string $type): string public static function getFormatByType(string $type): string
{ {
@ -92,7 +95,7 @@ public static function getFormatByType(string $type): string
case static::DAY: case static::DAY:
return 'Ymd'; return 'Ymd';
default: default:
throw new \Exception('Unsupported daily format type'); throw new Exception('Unsupported daily format type');
} }
} }
@ -102,14 +105,14 @@ public static function getFormatByType(string $type): string
* and we don't want to alter original datetime. * and we don't want to alter original datetime.
* *
* @param string $type month/week/day * @param string $type month/week/day
* @param \DateTimeImmutable $requested DateTime extracted from request input * @param DateTimeImmutable $requested DateTime extracted from request input
* (should come from extractRequestedDateTime) * (should come from extractRequestedDateTime)
* *
* @return \DateTimeInterface First DateTime of the time period * @return \DateTimeInterface First DateTime of the time period
* *
* @throws \Exception Type not supported. * @throws Exception Type not supported.
*/ */
public static function getStartDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface public static function getStartDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
{ {
switch ($type) { switch ($type) {
case static::MONTH: case static::MONTH:
@ -119,7 +122,7 @@ public static function getStartDateTimeByType(string $type, \DateTimeImmutable $
case static::DAY: case static::DAY:
return $requested->modify('Today midnight'); return $requested->modify('Today midnight');
default: default:
throw new \Exception('Unsupported daily format type'); throw new Exception('Unsupported daily format type');
} }
} }
@ -129,14 +132,14 @@ public static function getStartDateTimeByType(string $type, \DateTimeImmutable $
* and we don't want to alter original datetime. * and we don't want to alter original datetime.
* *
* @param string $type month/week/day * @param string $type month/week/day
* @param \DateTimeImmutable $requested DateTime extracted from request input * @param DateTimeImmutable $requested DateTime extracted from request input
* (should come from extractRequestedDateTime) * (should come from extractRequestedDateTime)
* *
* @return \DateTimeInterface Last DateTime of the time period * @return \DateTimeInterface Last DateTime of the time period
* *
* @throws \Exception Type not supported. * @throws Exception Type not supported.
*/ */
public static function getEndDateTimeByType(string $type, \DateTimeImmutable $requested): \DateTimeInterface public static function getEndDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
{ {
switch ($type) { switch ($type) {
case static::MONTH: case static::MONTH:
@ -146,7 +149,7 @@ public static function getEndDateTimeByType(string $type, \DateTimeImmutable $re
case static::DAY: case static::DAY:
return $requested->modify('Today 23:59:59'); return $requested->modify('Today 23:59:59');
default: default:
throw new \Exception('Unsupported daily format type'); throw new Exception('Unsupported daily format type');
} }
} }
@ -157,13 +160,17 @@ public static function getEndDateTimeByType(string $type, \DateTimeImmutable $re
* @param string $type month/week/day * @param string $type month/week/day
* @param \DateTimeImmutable $requested DateTime extracted from request input * @param \DateTimeImmutable $requested DateTime extracted from request input
* (should come from extractRequestedDateTime) * (should come from extractRequestedDateTime)
* @param bool $includeRelative Include relative date description (today, yesterday, etc.)
* *
* @return string Localized time period description * @return string Localized time period description
* *
* @throws \Exception Type not supported. * @throws Exception Type not supported.
*/ */
public static function getDescriptionByType(string $type, \DateTimeImmutable $requested): string public static function getDescriptionByType(
{ string $type,
\DateTimeImmutable $requested,
bool $includeRelative = true
): string {
switch ($type) { switch ($type) {
case static::MONTH: case static::MONTH:
return $requested->format('F') . ', ' . $requested->format('Y'); return $requested->format('F') . ', ' . $requested->format('Y');
@ -172,14 +179,14 @@ public static function getDescriptionByType(string $type, \DateTimeImmutable $re
return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')'; return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
case static::DAY: case static::DAY:
$out = ''; $out = '';
if ($requested->format('Ymd') === date('Ymd')) { if ($includeRelative && $requested->format('Ymd') === date('Ymd')) {
$out = t('Today') . ' - '; $out = t('Today') . ' - ';
} elseif ($requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) { } elseif ($includeRelative && $requested->format('Ymd') === date('Ymd', strtotime('-1 days'))) {
$out = t('Yesterday') . ' - '; $out = t('Yesterday') . ' - ';
} }
return $out . format_date($requested, false); return $out . format_date($requested, false);
default: default:
throw new \Exception('Unsupported daily format type'); throw new Exception('Unsupported daily format type');
} }
} }
@ -190,7 +197,7 @@ public static function getDescriptionByType(string $type, \DateTimeImmutable $re
* *
* @return int number of elements * @return int number of elements
* *
* @throws \Exception Type not supported. * @throws Exception Type not supported.
*/ */
public static function getRssLengthByType(string $type): int public static function getRssLengthByType(string $type): int
{ {
@ -202,7 +209,28 @@ public static function getRssLengthByType(string $type): int
case static::DAY: case static::DAY:
return 30; // ~1 month return 30; // ~1 month
default: default:
throw new \Exception('Unsupported daily format type'); throw new Exception('Unsupported daily format type');
} }
} }
/**
* Get the number of items to display in the RSS feed depending on the given type.
*
* @param string $type month/week/day
* @param ?DateTimeImmutable $requested Currently only used for UT
*
* @return DatePeriod number of elements
*
* @throws Exception Type not supported.
*/
public static function getCacheDatePeriodByType(string $type, DateTimeImmutable $requested = null): DatePeriod
{
$requested = $requested ?? new DateTimeImmutable();
return new DatePeriod(
static::getStartDateTimeByType($type, $requested),
new \DateInterval('P1D'),
static::getEndDateTimeByType($type, $requested)
);
}
} }

View file

@ -60,10 +60,15 @@ public function retrieve(string $url): array
$title = mb_convert_encoding($title, 'utf-8', $charset); $title = mb_convert_encoding($title, 'utf-8', $charset);
} }
return [ return array_map([$this, 'cleanMetadata'], [
'title' => $title, 'title' => $title,
'description' => $description, 'description' => $description,
'tags' => $tags, 'tags' => $tags,
]; ]);
}
protected function cleanMetadata($data): ?string
{
return !is_string($data) || empty(trim($data)) ? null : trim($data);
} }
} }

View file

@ -64,7 +64,7 @@ public function filterAndFormat(
} }
$bookmarkLinks = []; $bookmarkLinks = [];
foreach ($this->bookmarkService->search([], $selection) as $bookmark) { foreach ($this->bookmarkService->search([], $selection)->getBookmarks() as $bookmark) {
$link = $formatter->format($bookmark); $link = $formatter->format($bookmark);
$link['taglist'] = implode(',', $bookmark->getTags()); $link['taglist'] = implode(',', $bookmark->getTags());
if ($bookmark->isNote() && $prependNoteUrl) { if ($bookmark->isNote() && $prependNoteUrl) {

View file

@ -2,8 +2,10 @@
namespace Shaarli\Plugin; namespace Shaarli\Plugin;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\Exception\PluginFileNotFoundException; use Shaarli\Plugin\Exception\PluginFileNotFoundException;
use Shaarli\Plugin\Exception\PluginInvalidRouteException;
/** /**
* Class PluginManager * Class PluginManager
@ -26,6 +28,14 @@ class PluginManager
*/ */
private $loadedPlugins = []; private $loadedPlugins = [];
/** @var array List of registered routes. Contains keys:
* - `method`: HTTP method, GET/POST/PUT/PATCH/DELETE
* - `route` (path): without prefix, e.g. `/up/{variable}`
* It will be later prefixed by `/plugin/<plugin name>/`.
* - `callable` string, function name or FQN class's method, e.g. `demo_plugin_custom_controller`.
*/
protected $registeredRoutes = [];
/** /**
* @var ConfigManager Configuration Manager instance. * @var ConfigManager Configuration Manager instance.
*/ */
@ -36,6 +46,9 @@ class PluginManager
*/ */
protected $errors; protected $errors;
/** @var callable[]|null Preloaded list of hook function for filterSearchEntry() */
protected $filterSearchEntryHooks = null;
/** /**
* Plugins subdirectory. * Plugins subdirectory.
* *
@ -86,6 +99,9 @@ public function load($authorizedPlugins)
$this->loadPlugin($dirs[$index], $plugin); $this->loadPlugin($dirs[$index], $plugin);
} catch (PluginFileNotFoundException $e) { } catch (PluginFileNotFoundException $e) {
error_log($e->getMessage()); error_log($e->getMessage());
} catch (\Throwable $e) {
$error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
$this->errors = array_unique(array_merge($this->errors, [$error]));
} }
} }
} }
@ -166,6 +182,22 @@ private function loadPlugin($dir, $pluginName)
} }
} }
$registerRouteFunction = $pluginName . '_register_routes';
$routes = null;
if (function_exists($registerRouteFunction)) {
$routes = call_user_func($registerRouteFunction);
}
if ($routes !== null) {
foreach ($routes as $route) {
if (static::validateRouteRegistration($route)) {
$this->registeredRoutes[$pluginName][] = $route;
} else {
throw new PluginInvalidRouteException($pluginName);
}
}
}
$this->loadedPlugins[] = $pluginName; $this->loadedPlugins[] = $pluginName;
} }
@ -237,6 +269,22 @@ public function getPluginsMeta()
return $metaData; return $metaData;
} }
/**
* @return array List of registered custom routes by plugins.
*/
public function getRegisteredRoutes(): array
{
return $this->registeredRoutes;
}
/**
* @return array List of registered filter_search_entry hooks
*/
public function getFilterSearchEntryHooks(): ?array
{
return $this->filterSearchEntryHooks;
}
/** /**
* Return the list of encountered errors. * Return the list of encountered errors.
* *
@ -246,4 +294,76 @@ public function getErrors()
{ {
return $this->errors; return $this->errors;
} }
/**
* Apply additional filter on every search result of BookmarkFilter calling plugins hooks.
*
* @param Bookmark $bookmark To check.
* @param array $context Additional info about search context, depends on the search source.
*
* @return bool True if the result must be kept in search results, false otherwise.
*/
public function filterSearchEntry(Bookmark $bookmark, array $context): bool
{
if ($this->filterSearchEntryHooks === null) {
$this->loadFilterSearchEntryHooks();
}
if ($this->filterSearchEntryHooks === []) {
return true;
}
foreach ($this->filterSearchEntryHooks as $filterSearchEntryHook) {
if ($filterSearchEntryHook($bookmark, $context) === false) {
return false;
}
}
return true;
}
/**
* filterSearchEntry() method will be called for every search result,
* so for performances we preload existing functions to invoke them directly.
*/
protected function loadFilterSearchEntryHooks(): void
{
$this->filterSearchEntryHooks = [];
foreach ($this->loadedPlugins as $plugin) {
$hookFunction = $this->buildHookName('filter_search_entry', $plugin);
if (function_exists($hookFunction)) {
$this->filterSearchEntryHooks[] = $hookFunction;
}
}
}
/**
* Checks whether provided input is valid to register a new route.
* It must contain keys `method`, `route`, `callable` (all strings).
*
* @param string[] $input
*
* @return bool
*/
protected static function validateRouteRegistration(array $input): bool
{
if (
!array_key_exists('method', $input)
|| !in_array(strtoupper($input['method']), ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
) {
return false;
}
if (!array_key_exists('route', $input) || !preg_match('#^[a-z\d/\.\-_]+$#', $input['route'])) {
return false;
}
if (!array_key_exists('callable', $input)) {
return false;
}
return true;
}
} }

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Shaarli\Plugin\Exception;
use Exception;
/**
* Class PluginFileNotFoundException
*
* Raise when plugin files can't be found.
*/
class PluginInvalidRouteException extends Exception
{
/**
* Construct exception with plugin name.
* Generate message.
*
* @param string $pluginName name of the plugin not found
*/
public function __construct()
{
$this->message = 'trying to register invalid route.';
}
}

View file

@ -2,6 +2,7 @@
namespace Shaarli\Render; namespace Shaarli\Render;
use DatePeriod;
use Shaarli\Feed\CachedPage; use Shaarli\Feed\CachedPage;
/** /**
@ -49,12 +50,21 @@ public function invalidateCaches(): void
$this->purgeCachedPages(); $this->purgeCachedPages();
} }
public function getCachePage(string $pageUrl): CachedPage /**
* Get CachedPage instance for provided URL.
*
* @param string $pageUrl
* @param ?DatePeriod $validityPeriod Optionally specify a time limit on requested cache
*
* @return CachedPage
*/
public function getCachePage(string $pageUrl, DatePeriod $validityPeriod = null): CachedPage
{ {
return new CachedPage( return new CachedPage(
$this->pageCacheDir, $this->pageCacheDir,
$pageUrl, $pageUrl,
false === $this->isLoggedIn false === $this->isLoggedIn,
$validityPeriod
); );
} }
} }

View file

@ -152,7 +152,7 @@ public function updateMethodMigrateExistingNotesUrl(): bool
{ {
$updated = false; $updated = false;
foreach ($this->bookmarkService->search() as $bookmark) { foreach ($this->bookmarkService->search()->getBookmarks() as $bookmark) {
if ( if (
$bookmark->isNote() $bookmark->isNote()
&& startsWith($bookmark->getUrl(), '?') && startsWith($bookmark->getUrl(), '?')

View file

@ -4,7 +4,11 @@ const sendBookmarkForm = (basePath, formElement) => {
const formData = new FormData(); const formData = new FormData();
[...inputs].forEach((input) => { [...inputs].forEach((input) => {
if (input.getAttribute('type') === 'checkbox') {
formData.append(input.getAttribute('name'), input.checked);
} else {
formData.append(input.getAttribute('name'), input.value); formData.append(input.getAttribute('name'), input.value);
}
}); });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -26,9 +30,9 @@ const sendBookmarkForm = (basePath, formElement) => {
const sendBookmarkDelete = (buttonElement, formElement) => ( const sendBookmarkDelete = (buttonElement, formElement) => (
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('GET', buttonElement.href); xhr.open('GET', `${buttonElement.href}&source=batch`);
xhr.onload = () => { xhr.onload = () => {
if (xhr.status !== 200) { if (xhr.status !== 204) {
alert(`An error occurred. Return code: ${xhr.status}`); alert(`An error occurred. Return code: ${xhr.status}`);
reject(); reject();
} else { } else {
@ -100,7 +104,7 @@ const redirectIfEmptyBatch = (basePath, formElements, path) => {
}); });
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
window.location.href = basePath || '/'; window.location.href = `${basePath}/`;
}); });
}); });
}); });

12
composer.lock generated
View file

@ -8,16 +8,16 @@
"packages": [ "packages": [
{ {
"name": "arthurhoaro/web-thumbnailer", "name": "arthurhoaro/web-thumbnailer",
"version": "v2.0.3", "version": "v2.0.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/ArthurHoaro/web-thumbnailer.git", "url": "https://github.com/ArthurHoaro/web-thumbnailer.git",
"reference": "39bfd4f3136d9e6096496b9720e877326cfe4775" "reference": "7780ddc0f44fccdce6cddb86d1db0354810290d0"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/39bfd4f3136d9e6096496b9720e877326cfe4775", "url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/7780ddc0f44fccdce6cddb86d1db0354810290d0",
"reference": "39bfd4f3136d9e6096496b9720e877326cfe4775", "reference": "7780ddc0f44fccdce6cddb86d1db0354810290d0",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -53,9 +53,9 @@
"description": "PHP library which will retrieve a thumbnail for any given URL", "description": "PHP library which will retrieve a thumbnail for any given URL",
"support": { "support": {
"issues": "https://github.com/ArthurHoaro/web-thumbnailer/issues", "issues": "https://github.com/ArthurHoaro/web-thumbnailer/issues",
"source": "https://github.com/ArthurHoaro/web-thumbnailer/tree/v2.0.3" "source": "https://github.com/ArthurHoaro/web-thumbnailer/tree/v2.0.4"
}, },
"time": "2020-09-29T15:51:03+00:00" "time": "2021-02-22T10:43:01+00:00"
}, },
{ {
"name": "erusev/parsedown", "name": "erusev/parsedown",

View file

@ -194,7 +194,7 @@ $ docker logs -f <container-name-or-first-letters-of-id>
# delete unused images to free up disk space # delete unused images to free up disk space
$ docker system prune --images $ docker system prune --images
# delete unused volumes to free up disk space (CAUTION all data in unused volumes will be lost) # delete unused volumes to free up disk space (CAUTION all data in unused volumes will be lost)
$ docker system prunt --volumes $ docker system prune --volumes
# delete unused containers # delete unused containers
$ docker system prune $ docker system prune
``` ```

View file

@ -73,7 +73,7 @@ var_dump(getInfo($baseUrl, $secret));
### Authentication ### Authentication
- All requests to Shaarli's API must include a **JWT token** to verify their authenticity. - All requests to Shaarli's API must include a **JWT token** to verify their authenticity.
- This token must be included as an HTTP header called `Authentication: Bearer <jwt token>`. - This token must be included as an HTTP header called `Authorization: Bearer <jwt token>`.
- JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64: - JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64:
``` ```

View file

@ -199,6 +199,8 @@ sudo nano /etc/apache2/sites-available/shaarli.mydomain.org.conf
Require all denied Require all denied
</FilesMatch> </FilesMatch>
DirectoryIndex index.php
<Files "index.php"> <Files "index.php">
Require all granted Require all granted
</Files> </Files>

View file

@ -27,7 +27,6 @@ You should have the following tree view:
| |---| demo_plugin.php | |---| demo_plugin.php
``` ```
### Plugin initialization ### Plugin initialization
At the beginning of Shaarli execution, all enabled plugins are loaded. At this point, the plugin system looks for an `init()` function in the <plugin_name>.php to execute and run it if it exists. This function must be named this way, and takes the `ConfigManager` as parameter. At the beginning of Shaarli execution, all enabled plugins are loaded. At this point, the plugin system looks for an `init()` function in the <plugin_name>.php to execute and run it if it exists. This function must be named this way, and takes the `ConfigManager` as parameter.
@ -139,6 +138,31 @@ Each file contain two keys:
> Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file. > Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file.
### Register plugin's routes
Shaarli lets you register custom Slim routes for your plugin.
To register a route, the plugin must include a function called `function <plugin_name>_register_routes(): array`.
This method must return an array of routes, each entry must contain the following keys:
- `method`: HTTP method, `GET/POST/PUT/PATCH/DELETE`
- `route` (path): without prefix, e.g. `/up/{variable}`
It will be later prefixed by `/plugin/<plugin name>/`.
- `callable` string, function name or FQN class's method to execute, e.g. `demo_plugin_custom_controller`.
Callable functions or methods must have `Slim\Http\Request` and `Slim\Http\Response` parameters
and return a `Slim\Http\Response`. We recommend creating a dedicated class and extend either
`ShaarliVisitorController` or `ShaarliAdminController` to use helper functions they provide.
A dedicated plugin template is available for rendering content: `pluginscontent.html` using `content` placeholder.
> **Warning**: plugins are not able to use RainTPL template engine for their content due to technical restrictions.
> RainTPL does not allow to register multiple template folders, so all HTML rendering must be done within plugin
> custom controller.
Check out the `demo_plugin` for a live example: `GET <shaarli_url>/plugin/demo_plugin/custom`.
### Understanding relative paths ### Understanding relative paths
Because Shaarli is a self-hosted tool, an instance can either be installed at the root directory, or under a subfolder. Because Shaarli is a self-hosted tool, an instance can either be installed at the root directory, or under a subfolder.
@ -184,6 +208,7 @@ If it's still not working, please [open an issue](https://github.com/shaarli/Sha
| [save_link](#save_link) | Allow to alter the link being saved in the datastore. | | [save_link](#save_link) | Allow to alter the link being saved in the datastore. |
| [delete_link](#delete_link) | Allow to do an action before a link is deleted from the datastore. | | [delete_link](#delete_link) | Allow to do an action before a link is deleted from the datastore. |
| [save_plugin_parameters](#save_plugin_parameters) | Allow to manipulate plugin parameters before they're saved. | | [save_plugin_parameters](#save_plugin_parameters) | Allow to manipulate plugin parameters before they're saved. |
| [filter_search_entry](#filter_search_entry) | Add custom filters to Shaarli search engine |
#### render_header #### render_header
@ -540,6 +565,23 @@ the array will contain an entry with `MYPLUGIN_PARAMETER` as a key.
Also [special data](#special-data). Also [special data](#special-data).
#### filter_search_entry
Triggered for *every* bookmark when Shaarli's BookmarkService method `search()` is used.
Any custom filter can be added to filter out bookmarks from search results.
The hook **must** return either:
- `true` to keep bookmark entry in search result set
- `false` to discard bookmark entry in result set
> Note: custom filters are called *before* default filters are applied.
##### Parameters
- `Shaarli\Bookmark\Bookmark` object: entry to evaluate
- $context `array`: additional information provided depending on what search is currently used,
the user request, etc.
## Guide for template designers ## Guide for template designers
### Plugin administration ### Plugin administration

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,8 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Shaarli\n" "Project-Id-Version: Shaarli\n"
"POT-Creation-Date: 2020-11-09 14:39+0100\n" "POT-Creation-Date: 2020-11-24 13:13+0100\n"
"PO-Revision-Date: 2020-11-09 14:42+0100\n" "PO-Revision-Date: 2020-11-24 13:14+0100\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: Shaarli\n" "Language-Team: Shaarli\n"
"Language: fr_FR\n" "Language: fr_FR\n"
@ -20,31 +20,31 @@ msgstr ""
"X-Poedit-SearchPath-3: init.php\n" "X-Poedit-SearchPath-3: init.php\n"
"X-Poedit-SearchPath-4: plugins\n" "X-Poedit-SearchPath-4: plugins\n"
#: application/History.php:180 #: application/History.php:181
msgid "History file isn't readable or writable" msgid "History file isn't readable or writable"
msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture" msgstr "Le fichier d'historique n'est pas accessible en lecture ou en écriture"
#: application/History.php:191 #: application/History.php:192
msgid "Could not parse history file" msgid "Could not parse history file"
msgstr "Format incorrect pour le fichier d'historique" msgstr "Format incorrect pour le fichier d'historique"
#: application/Languages.php:181 #: application/Languages.php:184
msgid "Automatic" msgid "Automatic"
msgstr "Automatique" msgstr "Automatique"
#: application/Languages.php:182 #: application/Languages.php:185
msgid "German" msgid "German"
msgstr "Allemand" msgstr "Allemand"
#: application/Languages.php:183 #: application/Languages.php:186
msgid "English" msgid "English"
msgstr "Anglais" msgstr "Anglais"
#: application/Languages.php:184 #: application/Languages.php:187
msgid "French" msgid "French"
msgstr "Français" msgstr "Français"
#: application/Languages.php:185 #: application/Languages.php:188
msgid "Japanese" msgid "Japanese"
msgstr "Japonais" msgstr "Japonais"
@ -56,46 +56,46 @@ msgstr ""
"l'extension php-gd doit être chargée pour utiliser les miniatures. Les " "l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
"miniatures sont désormais désactivées. Rechargez la page." "miniatures sont désormais désactivées. Rechargez la page."
#: application/Utils.php:402 #: application/Utils.php:405
msgid "Setting not set" msgid "Setting not set"
msgstr "Paramètre non défini" msgstr "Paramètre non défini"
#: application/Utils.php:409 #: application/Utils.php:412
msgid "Unlimited" msgid "Unlimited"
msgstr "Illimité" msgstr "Illimité"
#: application/Utils.php:412 #: application/Utils.php:415
msgid "B" msgid "B"
msgstr "o" msgstr "o"
#: application/Utils.php:412 #: application/Utils.php:415
msgid "kiB" msgid "kiB"
msgstr "ko" msgstr "ko"
#: application/Utils.php:412 #: application/Utils.php:415
msgid "MiB" msgid "MiB"
msgstr "Mo" msgstr "Mo"
#: application/Utils.php:412 #: application/Utils.php:415
msgid "GiB" msgid "GiB"
msgstr "Go" msgstr "Go"
#: application/bookmark/BookmarkFileService.php:183 #: application/bookmark/BookmarkFileService.php:185
#: application/bookmark/BookmarkFileService.php:205 #: application/bookmark/BookmarkFileService.php:207
#: application/bookmark/BookmarkFileService.php:227 #: application/bookmark/BookmarkFileService.php:229
#: application/bookmark/BookmarkFileService.php:241 #: application/bookmark/BookmarkFileService.php:243
msgid "You're not authorized to alter the datastore" msgid "You're not authorized to alter the datastore"
msgstr "Vous n'êtes pas autorisé à modifier les données" msgstr "Vous n'êtes pas autorisé à modifier les données"
#: application/bookmark/BookmarkFileService.php:208 #: application/bookmark/BookmarkFileService.php:210
msgid "This bookmarks already exists" msgid "This bookmarks already exists"
msgstr "Ce marque-page existe déjà" msgstr "Ce marque-page existe déjà"
#: application/bookmark/BookmarkInitializer.php:39 #: application/bookmark/BookmarkInitializer.php:42
msgid "(private bookmark with thumbnail demo)" msgid "(private bookmark with thumbnail demo)"
msgstr "(marque page privé avec une miniature)" msgstr "(marque page privé avec une miniature)"
#: application/bookmark/BookmarkInitializer.php:42 #: application/bookmark/BookmarkInitializer.php:45
msgid "" msgid ""
"Shaarli will automatically pick up the thumbnail for links to a variety of " "Shaarli will automatically pick up the thumbnail for links to a variety of "
"websites.\n" "websites.\n"
@ -118,11 +118,11 @@ msgstr ""
"\n" "\n"
"Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n" "Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n"
#: application/bookmark/BookmarkInitializer.php:55 #: application/bookmark/BookmarkInitializer.php:58
msgid "Note: Shaare descriptions" msgid "Note: Shaare descriptions"
msgstr "Note : Description des Shaares" msgstr "Note : Description des Shaares"
#: application/bookmark/BookmarkInitializer.php:57 #: application/bookmark/BookmarkInitializer.php:60
msgid "" msgid ""
"Adding a shaare without entering a URL creates a text-only \"note\" post " "Adding a shaare without entering a URL creates a text-only \"note\" post "
"such as this one.\n" "such as this one.\n"
@ -186,7 +186,7 @@ msgstr ""
"| Citron | Fruit | Jaune | 30 |\n" "| Citron | Fruit | Jaune | 30 |\n"
"| Carotte | Légume | Orange | 14 |\n" "| Carotte | Légume | Orange | 14 |\n"
#: application/bookmark/BookmarkInitializer.php:91 #: application/bookmark/BookmarkInitializer.php:94
#: application/legacy/LegacyLinkDB.php:246 #: application/legacy/LegacyLinkDB.php:246
#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
@ -198,7 +198,7 @@ msgstr ""
"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de " "Le gestionnaire de marque-pages personnel, minimaliste, et sans base de "
"données" "données"
#: application/bookmark/BookmarkInitializer.php:94 #: application/bookmark/BookmarkInitializer.php:97
msgid "" msgid ""
"Welcome to Shaarli!\n" "Welcome to Shaarli!\n"
"\n" "\n"
@ -247,11 +247,11 @@ msgstr ""
"issues) si vous avez une suggestion ou si vous rencontrez un problème.\n" "issues) si vous avez une suggestion ou si vous rencontrez un problème.\n"
" \n" " \n"
#: application/bookmark/exception/BookmarkNotFoundException.php:13 #: application/bookmark/exception/BookmarkNotFoundException.php:14
msgid "The link you are trying to reach does not exist or has been deleted." msgid "The link you are trying to reach does not exist or has been deleted."
msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé." msgstr "Le lien que vous essayez de consulter n'existe pas ou a été supprimé."
#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:129 #: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:131
msgid "" msgid ""
"Shaarli could not create the config file. Please make sure Shaarli has the " "Shaarli could not create the config file. Please make sure Shaarli has the "
"right to write in the folder is it installed in." "right to write in the folder is it installed in."
@ -259,12 +259,12 @@ msgstr ""
"Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que " "Shaarli n'a pas pu créer le fichier de configuration. Merci de vérifier que "
"Shaarli a les droits d'écriture dans le dossier dans lequel il est installé." "Shaarli a les droits d'écriture dans le dossier dans lequel il est installé."
#: application/config/ConfigManager.php:136 #: application/config/ConfigManager.php:137
#: application/config/ConfigManager.php:163 #: application/config/ConfigManager.php:164
msgid "Invalid setting key parameter. String expected, got: " msgid "Invalid setting key parameter. String expected, got: "
msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : " msgstr "Clé de paramétrage invalide. Chaîne de caractères obtenue, attendu : "
#: application/config/exception/MissingFieldConfigException.php:21 #: application/config/exception/MissingFieldConfigException.php:20
#, php-format #, php-format
msgid "Configuration value is required for %s" msgid "Configuration value is required for %s"
msgstr "Le paramètre %s est obligatoire" msgstr "Le paramètre %s est obligatoire"
@ -274,48 +274,48 @@ msgid "An error occurred while trying to save plugins loading order."
msgstr "" msgstr ""
"Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions." "Une erreur s'est produite lors de la sauvegarde de l'ordre des extensions."
#: application/config/exception/UnauthorizedConfigException.php:16 #: application/config/exception/UnauthorizedConfigException.php:15
msgid "You are not authorized to alter config." msgid "You are not authorized to alter config."
msgstr "Vous n'êtes pas autorisé à modifier la configuration." msgstr "Vous n'êtes pas autorisé à modifier la configuration."
#: application/exceptions/IOException.php:22 #: application/exceptions/IOException.php:23
msgid "Error accessing" msgid "Error accessing"
msgstr "Une erreur s'est produite en accédant à" msgstr "Une erreur s'est produite en accédant à"
#: application/feed/FeedBuilder.php:179 #: application/feed/FeedBuilder.php:180
msgid "Direct link" msgid "Direct link"
msgstr "Liens directs" msgstr "Liens directs"
#: application/feed/FeedBuilder.php:181 #: application/feed/FeedBuilder.php:182
#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 #: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 #: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
msgid "Permalink" msgid "Permalink"
msgstr "Permalien" msgstr "Permalien"
#: application/front/controller/admin/ConfigureController.php:54 #: application/front/controller/admin/ConfigureController.php:56
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
msgid "Configure" msgid "Configure"
msgstr "Configurer" msgstr "Configurer"
#: application/front/controller/admin/ConfigureController.php:102 #: application/front/controller/admin/ConfigureController.php:106
#: application/legacy/LegacyUpdater.php:537 #: application/legacy/LegacyUpdater.php:539
msgid "You have enabled or changed thumbnails mode." msgid "You have enabled or changed thumbnails mode."
msgstr "Vous avez activé ou changé le mode de miniatures." msgstr "Vous avez activé ou changé le mode de miniatures."
#: application/front/controller/admin/ConfigureController.php:103 #: application/front/controller/admin/ConfigureController.php:108
#: application/front/controller/admin/ServerController.php:75 #: application/front/controller/admin/ServerController.php:76
#: application/legacy/LegacyUpdater.php:538 #: application/legacy/LegacyUpdater.php:540
msgid "Please synchronize them." msgid "Please synchronize them."
msgstr "Merci de les synchroniser." msgstr "Merci de les synchroniser."
#: application/front/controller/admin/ConfigureController.php:113 #: application/front/controller/admin/ConfigureController.php:119
#: application/front/controller/visitor/InstallController.php:146 #: application/front/controller/visitor/InstallController.php:149
msgid "Error while writing config file after configuration update." msgid "Error while writing config file after configuration update."
msgstr "" msgstr ""
"Une erreur s'est produite lors de la sauvegarde du fichier de configuration." "Une erreur s'est produite lors de la sauvegarde du fichier de configuration."
#: application/front/controller/admin/ConfigureController.php:122 #: application/front/controller/admin/ConfigureController.php:128
msgid "Configuration was saved." msgid "Configuration was saved."
msgstr "La configuration a été sauvegardée." msgstr "La configuration a été sauvegardée."
@ -433,7 +433,7 @@ msgstr "Administration serveur"
msgid "Thumbnails cache has been cleared." msgid "Thumbnails cache has been cleared."
msgstr "Le cache des miniatures a été vidé." msgstr "Le cache des miniatures a été vidé."
#: application/front/controller/admin/ServerController.php:83 #: application/front/controller/admin/ServerController.php:85
msgid "Shaarli's cache folder has been cleared!" msgid "Shaarli's cache folder has been cleared!"
msgstr "Le dossier de cache de Shaarli a été vidé !" msgstr "Le dossier de cache de Shaarli a été vidé !"
@ -459,18 +459,18 @@ msgstr "Le lien avec l'identifiant %s n'a pas pu être trouvé."
msgid "Invalid visibility provided." msgid "Invalid visibility provided."
msgstr "Visibilité du lien non valide." msgstr "Visibilité du lien non valide."
#: application/front/controller/admin/ShaarePublishController.php:171 #: application/front/controller/admin/ShaarePublishController.php:173
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
msgid "Edit" msgid "Edit"
msgstr "Modifier" msgstr "Modifier"
#: application/front/controller/admin/ShaarePublishController.php:174 #: application/front/controller/admin/ShaarePublishController.php:176
#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 #: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 #: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
msgid "Shaare" msgid "Shaare"
msgstr "Shaare" msgstr "Shaare"
#: application/front/controller/admin/ShaarePublishController.php:205 #: application/front/controller/admin/ShaarePublishController.php:208
msgid "Note: " msgid "Note: "
msgstr "Note : " msgstr "Note : "
@ -485,7 +485,7 @@ msgstr "Mise à jour des miniatures"
msgid "Tools" msgid "Tools"
msgstr "Outils" msgstr "Outils"
#: application/front/controller/visitor/BookmarkListController.php:120 #: application/front/controller/visitor/BookmarkListController.php:121
msgid "Search: " msgid "Search: "
msgstr "Recherche : " msgstr "Recherche : "
@ -535,12 +535,12 @@ msgstr "Une erreur inattendue s'est produite."
msgid "Requested page could not be found." msgid "Requested page could not be found."
msgstr "La page demandée n'a pas pu être trouvée." msgstr "La page demandée n'a pas pu être trouvée."
#: application/front/controller/visitor/InstallController.php:64 #: application/front/controller/visitor/InstallController.php:65
#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
msgid "Install Shaarli" msgid "Install Shaarli"
msgstr "Installation de Shaarli" msgstr "Installation de Shaarli"
#: application/front/controller/visitor/InstallController.php:83 #: application/front/controller/visitor/InstallController.php:85
#, php-format #, php-format
msgid "" msgid ""
"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the " "<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
@ -559,14 +559,14 @@ msgstr ""
"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " "des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
"adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>" "adresse IP ou un <em>Fully Qualified Domain Name</em>.<br>"
#: application/front/controller/visitor/InstallController.php:154 #: application/front/controller/visitor/InstallController.php:157
msgid "" msgid ""
"Shaarli is now configured. Please login and start shaaring your bookmarks!" "Shaarli is now configured. Please login and start shaaring your bookmarks!"
msgstr "" msgstr ""
"Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à " "Shaarli est maintenant configuré. Vous pouvez vous connecter et commencez à "
"shaare vos liens !" "shaare vos liens !"
#: application/front/controller/visitor/InstallController.php:168 #: application/front/controller/visitor/InstallController.php:171
msgid "Insufficient permissions:" msgid "Insufficient permissions:"
msgstr "Permissions insuffisantes :" msgstr "Permissions insuffisantes :"
@ -580,7 +580,7 @@ msgstr "Permissions insuffisantes :"
msgid "Login" msgid "Login"
msgstr "Connexion" msgstr "Connexion"
#: application/front/controller/visitor/LoginController.php:77 #: application/front/controller/visitor/LoginController.php:78
msgid "Wrong login/password." msgid "Wrong login/password."
msgstr "Nom d'utilisateur ou mot de passe incorrect(s)." msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
@ -620,7 +620,7 @@ msgstr ""
msgid "Wrong token." msgid "Wrong token."
msgstr "Jeton invalide." msgstr "Jeton invalide."
#: application/helper/ApplicationUtils.php:162 #: application/helper/ApplicationUtils.php:165
#, php-format #, php-format
msgid "" msgid ""
"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
@ -631,52 +631,60 @@ msgstr ""
"peut donc pas fonctionner. Votre version de PHP a des failles de sécurités " "peut donc pas fonctionner. Votre version de PHP a des failles de sécurités "
"connues et devrait être mise à jour au plus tôt." "connues et devrait être mise à jour au plus tôt."
#: application/helper/ApplicationUtils.php:195 #: application/helper/ApplicationUtils.php:200
#: application/helper/ApplicationUtils.php:215 #: application/helper/ApplicationUtils.php:220
msgid "directory is not readable" msgid "directory is not readable"
msgstr "le répertoire n'est pas accessible en lecture" msgstr "le répertoire n'est pas accessible en lecture"
#: application/helper/ApplicationUtils.php:218 #: application/helper/ApplicationUtils.php:223
msgid "directory is not writable" msgid "directory is not writable"
msgstr "le répertoire n'est pas accessible en écriture" msgstr "le répertoire n'est pas accessible en écriture"
#: application/helper/ApplicationUtils.php:240 #: application/helper/ApplicationUtils.php:247
msgid "file is not readable" msgid "file is not readable"
msgstr "le fichier n'est pas accessible en lecture" msgstr "le fichier n'est pas accessible en lecture"
#: application/helper/ApplicationUtils.php:243 #: application/helper/ApplicationUtils.php:250
msgid "file is not writable" msgid "file is not writable"
msgstr "le fichier n'est pas accessible en écriture" msgstr "le fichier n'est pas accessible en écriture"
#: application/helper/ApplicationUtils.php:277 #: application/helper/ApplicationUtils.php:260
msgid ""
"Lock can not be acquired on the datastore. You might encounter concurrent "
"access issues."
msgstr ""
"Le fichier datastore ne peut pas être verrouillé. Vous pourriez rencontrer "
"des problèmes d'accès concurrents."
#: application/helper/ApplicationUtils.php:293
msgid "Configuration parsing" msgid "Configuration parsing"
msgstr "Chargement de la configuration" msgstr "Chargement de la configuration"
#: application/helper/ApplicationUtils.php:278 #: application/helper/ApplicationUtils.php:294
msgid "Slim Framework (routing, etc.)" msgid "Slim Framework (routing, etc.)"
msgstr "Slim Framwork (routage, etc.)" msgstr "Slim Framwork (routage, etc.)"
#: application/helper/ApplicationUtils.php:279 #: application/helper/ApplicationUtils.php:295
msgid "Multibyte (Unicode) string support" msgid "Multibyte (Unicode) string support"
msgstr "Support des chaînes de caractère multibytes (Unicode)" msgstr "Support des chaînes de caractère multibytes (Unicode)"
#: application/helper/ApplicationUtils.php:280 #: application/helper/ApplicationUtils.php:296
msgid "Required to use thumbnails" msgid "Required to use thumbnails"
msgstr "Obligatoire pour utiliser les miniatures" msgstr "Obligatoire pour utiliser les miniatures"
#: application/helper/ApplicationUtils.php:281 #: application/helper/ApplicationUtils.php:297
msgid "Localized text sorting (e.g. e->è->f)" msgid "Localized text sorting (e.g. e->è->f)"
msgstr "Tri des textes traduits (ex : e->è->f)" msgstr "Tri des textes traduits (ex : e->è->f)"
#: application/helper/ApplicationUtils.php:282 #: application/helper/ApplicationUtils.php:298
msgid "Better retrieval of bookmark metadata and thumbnail" msgid "Better retrieval of bookmark metadata and thumbnail"
msgstr "Meilleure récupération des meta-données des marque-pages et minatures" msgstr "Meilleure récupération des meta-données des marque-pages et minatures"
#: application/helper/ApplicationUtils.php:283 #: application/helper/ApplicationUtils.php:299
msgid "Use the translation system in gettext mode" msgid "Use the translation system in gettext mode"
msgstr "Utiliser le système de traduction en mode gettext" msgstr "Utiliser le système de traduction en mode gettext"
#: application/helper/ApplicationUtils.php:284 #: application/helper/ApplicationUtils.php:300
msgid "Login using LDAP server" msgid "Login using LDAP server"
msgstr "Authentification via un serveur LDAP" msgstr "Authentification via un serveur LDAP"
@ -750,7 +758,7 @@ msgstr ""
msgid "Couldn't retrieve updater class methods." msgid "Couldn't retrieve updater class methods."
msgstr "Impossible de récupérer les méthodes de la classe Updater." msgstr "Impossible de récupérer les méthodes de la classe Updater."
#: application/legacy/LegacyUpdater.php:538 #: application/legacy/LegacyUpdater.php:540
msgid "<a href=\"./admin/thumbnails\">" msgid "<a href=\"./admin/thumbnails\">"
msgstr "<a href=\"./admin/thumbnails\">" msgstr "<a href=\"./admin/thumbnails\">"
@ -776,11 +784,11 @@ msgstr ""
"a été importé avec succès en %d secondes : %d liens importés, %d liens " "a été importé avec succès en %d secondes : %d liens importés, %d liens "
"écrasés, %d liens ignorés." "écrasés, %d liens ignorés."
#: application/plugin/PluginManager.php:124 #: application/plugin/PluginManager.php:125
msgid " [plugin incompatibility]: " msgid " [plugin incompatibility]: "
msgstr " [incompatibilité de l'extension] : " msgstr " [incompatibilité de l'extension] : "
#: application/plugin/exception/PluginFileNotFoundException.php:21 #: application/plugin/exception/PluginFileNotFoundException.php:22
#, php-format #, php-format
msgid "Plugin \"%s\" files not found." msgid "Plugin \"%s\" files not found."
msgstr "Les fichiers de l'extension \"%s\" sont introuvables." msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
@ -794,7 +802,7 @@ msgstr "Impossible de purger %s : le répertoire n'existe pas"
msgid "An error occurred while running the update " msgid "An error occurred while running the update "
msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour " msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
#: index.php:80 #: index.php:81
msgid "Shared bookmarks on " msgid "Shared bookmarks on "
msgstr "Liens partagés sur " msgstr "Liens partagés sur "
@ -811,11 +819,11 @@ msgstr "Shaare"
msgid "Adds the addlink input on the linklist page." msgid "Adds the addlink input on the linklist page."
msgstr "Ajoute le formulaire d'ajout de liens sur la page principale." msgstr "Ajoute le formulaire d'ajout de liens sur la page principale."
#: plugins/archiveorg/archiveorg.php:28 #: plugins/archiveorg/archiveorg.php:29
msgid "View on archive.org" msgid "View on archive.org"
msgstr "Voir sur archive.org" msgstr "Voir sur archive.org"
#: plugins/archiveorg/archiveorg.php:41 #: plugins/archiveorg/archiveorg.php:42
msgid "For each link, add an Archive.org icon." msgid "For each link, add an Archive.org icon."
msgstr "Pour chaque lien, ajoute une icône pour Archive.org." msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
@ -845,7 +853,7 @@ msgstr "Couleur de fond (gris léger)"
msgid "Dark main color (e.g. visited links)" msgid "Dark main color (e.g. visited links)"
msgstr "Couleur principale sombre (ex : les liens visités)" msgstr "Couleur principale sombre (ex : les liens visités)"
#: plugins/demo_plugin/demo_plugin.php:477 #: plugins/demo_plugin/demo_plugin.php:478
msgid "" msgid ""
"A demo plugin covering all use cases for template designers and plugin " "A demo plugin covering all use cases for template designers and plugin "
"developers." "developers."
@ -853,11 +861,11 @@ msgstr ""
"Une extension de démonstration couvrant tous les cas d'utilisation pour les " "Une extension de démonstration couvrant tous les cas d'utilisation pour les "
"designers de thèmes et les développeurs d'extensions." "designers de thèmes et les développeurs d'extensions."
#: plugins/demo_plugin/demo_plugin.php:478 #: plugins/demo_plugin/demo_plugin.php:479
msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé." msgstr "Ceci est un paramètre dédié au plugin de démo. Il sera suffixé."
#: plugins/demo_plugin/demo_plugin.php:479 #: plugins/demo_plugin/demo_plugin.php:480
msgid "Other demo parameter" msgid "Other demo parameter"
msgstr "Un autre paramètre de démo" msgstr "Un autre paramètre de démo"
@ -879,7 +887,7 @@ msgstr ""
msgid "Isso server URL (without 'http://')" msgid "Isso server URL (without 'http://')"
msgstr "URL du serveur Isso (sans 'http://')" msgstr "URL du serveur Isso (sans 'http://')"
#: plugins/piwik/piwik.php:23 #: plugins/piwik/piwik.php:24
msgid "" msgid ""
"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " "Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
"administration page." "administration page."
@ -887,27 +895,27 @@ msgstr ""
"Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et " "Erreur de l'extension Piwik : Merci de définir les paramètres PIWIK_URL et "
"PIWIK_SITEID dans la page d'administration des extensions." "PIWIK_SITEID dans la page d'administration des extensions."
#: plugins/piwik/piwik.php:72 #: plugins/piwik/piwik.php:73
msgid "A plugin that adds Piwik tracking code to Shaarli pages." msgid "A plugin that adds Piwik tracking code to Shaarli pages."
msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli." msgstr "Ajoute le code de traçage de Piwik sur les pages de Shaarli."
#: plugins/piwik/piwik.php:73 #: plugins/piwik/piwik.php:74
msgid "Piwik URL" msgid "Piwik URL"
msgstr "URL de Piwik" msgstr "URL de Piwik"
#: plugins/piwik/piwik.php:74 #: plugins/piwik/piwik.php:75
msgid "Piwik site ID" msgid "Piwik site ID"
msgstr "Site ID de Piwik" msgstr "Site ID de Piwik"
#: plugins/playvideos/playvideos.php:25 #: plugins/playvideos/playvideos.php:26
msgid "Video player" msgid "Video player"
msgstr "Lecteur vidéo" msgstr "Lecteur vidéo"
#: plugins/playvideos/playvideos.php:28 #: plugins/playvideos/playvideos.php:29
msgid "Play Videos" msgid "Play Videos"
msgstr "Jouer les vidéos" msgstr "Jouer les vidéos"
#: plugins/playvideos/playvideos.php:59 #: plugins/playvideos/playvideos.php:60
msgid "Add a button in the toolbar allowing to watch all videos." msgid "Add a button in the toolbar allowing to watch all videos."
msgstr "" msgstr ""
"Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos." "Ajoute un bouton dans la barre de menu pour regarder toutes les vidéos."
@ -935,11 +943,11 @@ msgstr "Mauvaise réponse du hub %s"
msgid "Enable PubSubHubbub feed publishing." msgid "Enable PubSubHubbub feed publishing."
msgstr "Active la publication de flux vers PubSubHubbub." msgstr "Active la publication de flux vers PubSubHubbub."
#: plugins/qrcode/qrcode.php:73 plugins/wallabag/wallabag.php:71 #: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72
msgid "For each link, add a QRCode icon." msgid "For each link, add a QRCode icon."
msgstr "Pour chaque lien, ajouter une icône de QRCode." msgstr "Pour chaque lien, ajouter une icône de QRCode."
#: plugins/wallabag/wallabag.php:21 #: plugins/wallabag/wallabag.php:22
msgid "" msgid ""
"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the " "Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
"plugin administration page." "plugin administration page."
@ -947,15 +955,15 @@ msgstr ""
"Erreur de l'extension Wallabag : Merci de définir le paramètre « " "Erreur de l'extension Wallabag : Merci de définir le paramètre « "
"WALLABAG_URL » dans la page d'administration des extensions." "WALLABAG_URL » dans la page d'administration des extensions."
#: plugins/wallabag/wallabag.php:48 #: plugins/wallabag/wallabag.php:49
msgid "Save to wallabag" msgid "Save to wallabag"
msgstr "Sauvegarder dans Wallabag" msgstr "Sauvegarder dans Wallabag"
#: plugins/wallabag/wallabag.php:72 #: plugins/wallabag/wallabag.php:73
msgid "Wallabag API URL" msgid "Wallabag API URL"
msgstr "URL de l'API Wallabag" msgstr "URL de l'API Wallabag"
#: plugins/wallabag/wallabag.php:73 #: plugins/wallabag/wallabag.php:74
msgid "Wallabag API version (1 or 2)" msgid "Wallabag API version (1 or 2)"
msgstr "Version de l'API Wallabag (1 ou 2)" msgstr "Version de l'API Wallabag (1 ou 2)"

View file

@ -3,14 +3,14 @@ msgstr ""
"Project-Id-Version: Shaarli\n" "Project-Id-Version: Shaarli\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-10-19 10:19+0900\n" "POT-Creation-Date: 2020-10-19 10:19+0900\n"
"PO-Revision-Date: 2020-10-19 10:25+0900\n" "PO-Revision-Date: 2021-01-04 18:54+0900\n"
"Last-Translator: yude <yudesleepy@gmail.com>\n" "Last-Translator: yude <yudesleepy@gmail.com>\n"
"Language-Team: Shaarli\n" "Language-Team: Shaarli\n"
"Language: ja\n" "Language: ja\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.2.3\n" "X-Generator: Poedit 2.4.2\n"
"X-Poedit-Basepath: ../../../..\n" "X-Poedit-Basepath: ../../../..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n" "X-Poedit-SourceCharset: UTF-8\n"
@ -79,8 +79,8 @@ msgid ""
"php-gd extension must be loaded to use thumbnails. Thumbnails are now " "php-gd extension must be loaded to use thumbnails. Thumbnails are now "
"disabled. Please reload the page." "disabled. Please reload the page."
msgstr "" msgstr ""
"サムネイルを使用するには、php-gd エクステンションが読み込まれている必要があり" "サムネイルを使用するには、php-gd 拡張機能が読み込まれている必要があります。サ"
"ます。サムネイルは無効化されました。ページを再読込してください。" "ムネイルは無効化されました。ページを再読込してください。"
#: application/Utils.php:383 tests/UtilsTest.php:343 #: application/Utils.php:383 tests/UtilsTest.php:343
msgid "Setting not set" msgid "Setting not set"
@ -118,7 +118,7 @@ msgstr "設定を変更する権限がありません"
#: application/bookmark/BookmarkFileService.php:205 #: application/bookmark/BookmarkFileService.php:205
msgid "This bookmarks already exists" msgid "This bookmarks already exists"
msgstr "このブックマークは既に存在します" msgstr "このブックマークは既に存在します"
#: application/bookmark/BookmarkInitializer.php:39 #: application/bookmark/BookmarkInitializer.php:39
msgid "(private bookmark with thumbnail demo)" msgid "(private bookmark with thumbnail demo)"
@ -594,8 +594,6 @@ msgstr ""
"す。" "す。"
#: application/legacy/LegacyUpdater.php:104 #: application/legacy/LegacyUpdater.php:104
#, fuzzy
#| msgid "Couldn't retrieve Updater class methods."
msgid "Couldn't retrieve updater class methods." msgid "Couldn't retrieve updater class methods."
msgstr "アップデーターのクラスメゾットを受信できませんでした。" msgstr "アップデーターのクラスメゾットを受信できませんでした。"
@ -617,10 +615,7 @@ msgid "has an unknown file format. Nothing was imported."
msgstr "は不明なファイル形式です。インポートは中止されました。" msgstr "は不明なファイル形式です。インポートは中止されました。"
#: application/netscape/NetscapeBookmarkUtils.php:221 #: application/netscape/NetscapeBookmarkUtils.php:221
#, fuzzy, php-format #, php-format
#| msgid ""
#| "was successfully processed in %d seconds: %d links imported, %d links "
#| "overwritten, %d links skipped."
msgid "" msgid ""
"was successfully processed in %d seconds: %d bookmarks imported, %d " "was successfully processed in %d seconds: %d bookmarks imported, %d "
"bookmarks overwritten, %d bookmarks skipped." "bookmarks overwritten, %d bookmarks skipped."

0
inc/languages/ru/LC_MESSAGES/shaarli.po Executable file → Normal file
View file

View file

@ -31,6 +31,7 @@
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Container\ContainerBuilder; use Shaarli\Container\ContainerBuilder;
use Shaarli\Languages; use Shaarli\Languages;
use Shaarli\Plugin\PluginManager;
use Shaarli\Security\BanManager; use Shaarli\Security\BanManager;
use Shaarli\Security\CookieManager; use Shaarli\Security\CookieManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
@ -87,7 +88,17 @@
$loginManager->checkLoginState(client_ip_id($_SERVER)); $loginManager->checkLoginState(client_ip_id($_SERVER));
$containerBuilder = new ContainerBuilder($conf, $sessionManager, $cookieManager, $loginManager, $logger); $pluginManager = new PluginManager($conf);
$pluginManager->load($conf->get('general.enabled_plugins', []));
$containerBuilder = new ContainerBuilder(
$conf,
$sessionManager,
$cookieManager,
$loginManager,
$pluginManager,
$logger
);
$container = $containerBuilder->build(); $container = $containerBuilder->build();
$app = new App($container); $app = new App($container);
@ -154,6 +165,15 @@
$this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility'); $this->get('/visibility/{visibility}', '\Shaarli\Front\Controller\Admin\SessionFilterController:visibility');
})->add('\Shaarli\Front\ShaarliAdminMiddleware'); })->add('\Shaarli\Front\ShaarliAdminMiddleware');
$app->group('/plugin', function () use ($pluginManager) {
foreach ($pluginManager->getRegisteredRoutes() as $pluginName => $routes) {
$this->group('/' . $pluginName, function () use ($routes) {
foreach ($routes as $route) {
$this->{strtolower($route['method'])}('/' . ltrim($route['route'], '/'), $route['callable']);
}
});
}
})->add('\Shaarli\Front\ShaarliMiddleware');
// REST API routes // REST API routes
$app->group('/api/v1', function () { $app->group('/api/v1', function () {

View file

@ -18,5 +18,6 @@
<rule ref="PSR1.Files.SideEffects.FoundWithSymbols"> <rule ref="PSR1.Files.SideEffects.FoundWithSymbols">
<!-- index.php bootstraps everything, so yes mixed symbols with side effects --> <!-- index.php bootstraps everything, so yes mixed symbols with side effects -->
<exclude-pattern>index.php</exclude-pattern> <exclude-pattern>index.php</exclude-pattern>
<exclude-pattern>plugins/*</exclude-pattern>
</rule> </rule>
</ruleset> </ruleset>

View file

@ -46,6 +46,20 @@ function default_colors_init($conf)
} }
} }
/**
* When plugin parameters are saved, we regenerate the custom CSS file with provided settings.
*
* @param array $data $_POST array
*
* @return array Updated $_POST array
*/
function hook_default_colors_save_plugin_parameters($data)
{
default_colors_generate_css_file($data);
return $data;
}
/** /**
* When linklist is displayed, include default_colors CSS file. * When linklist is displayed, include default_colors CSS file.
* *

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Shaarli\DemoPlugin;
use Shaarli\Front\Controller\Admin\ShaarliAdminController;
use Slim\Http\Request;
use Slim\Http\Response;
class DemoPluginController extends ShaarliAdminController
{
public function index(Request $request, Response $response): Response
{
$this->assignView(
'content',
'<div class="center">' .
'This is a demo page. I have access to Shaarli container, so I\'m free to do whatever I want here.' .
'</div>'
);
return $response->write($this->render('pluginscontent'));
}
}

View file

@ -7,6 +7,8 @@
* Can be used by plugin developers to make their own plugin. * Can be used by plugin developers to make their own plugin.
*/ */
require_once __DIR__ . '/DemoPluginController.php';
/* /*
* RENDER HEADER, INCLUDES, FOOTER * RENDER HEADER, INCLUDES, FOOTER
* *
@ -15,6 +17,7 @@
* and check user status with _LOGGEDIN_. * and check user status with _LOGGEDIN_.
*/ */
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager; use Shaarli\Plugin\PluginManager;
use Shaarli\Render\TemplatePage; use Shaarli\Render\TemplatePage;
@ -60,6 +63,17 @@ function demo_plugin_init($conf)
return $errors; return $errors;
} }
function demo_plugin_register_routes(): array
{
return [
[
'method' => 'GET',
'route' => '/custom',
'callable' => 'Shaarli\DemoPlugin\DemoPluginController:index',
],
];
}
/** /**
* Hook render_header. * Hook render_header.
* Executed on every page render. * Executed on every page render.
@ -250,6 +264,17 @@ function hook_demo_plugin_render_linklist($data)
} }
$data['action_plugin'][] = $action; $data['action_plugin'][] = $action;
// Action to trigger custom filter hiding bookmarks not containing 'e' letter in their description
$action = [
'attr' => [
'href' => '?e',
'title' => 'Hide bookmarks without "e" in their description.',
],
'html' => 'e',
'on' => isset($_GET['e'])
];
$data['action_plugin'][] = $action;
// link_plugin (for each link) // link_plugin (for each link)
foreach ($data['links'] as &$value) { foreach ($data['links'] as &$value) {
$value['link_plugin'][] = ' DEMO \o/'; $value['link_plugin'][] = ' DEMO \o/';
@ -304,7 +329,11 @@ function hook_demo_plugin_render_editlink($data)
function hook_demo_plugin_render_tools($data) function hook_demo_plugin_render_tools($data)
{ {
// field_plugin // field_plugin
$data['tools_plugin'][] = 'tools_plugin'; $data['tools_plugin'][] = '<div class="tools-item">
<a href="' . $data['_BASE_PATH_'] . '/plugin/demo_plugin/custom">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">Demo Plugin Custom Route</span>
</a>
</div>';
return $data; return $data;
} }
@ -469,6 +498,27 @@ function hook_demo_plugin_save_plugin_parameters($data)
return $data; return $data;
} }
/**
* This hook is called when a search is performed, on every search entry.
* It allows to add custom filters, and filter out additional link.
*
* For exemple here, we hide all bookmarks not containing the letter 'e' in their description.
*
* @param Bookmark $bookmark Search entry. Note that this is a Bookmark object, and not a link array.
* It should NOT be altered.
* @param array $context Additional info on the search performed.
*
* @return bool True if the bookmark should be kept in the search result, false to discard it.
*/
function hook_demo_plugin_filter_search_entry(Bookmark $bookmark, array $context): bool
{
if (isset($_GET['e'])) {
return strpos($bookmark->getDescription(), 'e') !== false;
}
return true;
}
/** /**
* This function is never called, but contains translation calls for GNU gettext extraction. * This function is never called, but contains translation calls for GNU gettext extraction.
*/ */

View file

@ -2,6 +2,7 @@
namespace Shaarli\Plugin; namespace Shaarli\Plugin;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
/** /**
@ -120,4 +121,58 @@ public function testGetPluginsMeta(): void
$this->assertEquals('test plugin', $meta[self::$pluginName]['description']); $this->assertEquals('test plugin', $meta[self::$pluginName]['description']);
$this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']); $this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']);
} }
/**
* Test plugin custom routes - note that there is no check on callable functions
*/
public function testRegisteredRoutes(): void
{
PluginManager::$PLUGINS_PATH = self::$pluginPath;
$this->pluginManager->load([self::$pluginName]);
$expectedParameters = [
[
'method' => 'GET',
'route' => '/test',
'callable' => 'getFunction',
],
[
'method' => 'POST',
'route' => '/custom',
'callable' => 'postFunction',
],
];
$meta = $this->pluginManager->getRegisteredRoutes();
static::assertSame($expectedParameters, $meta[self::$pluginName]);
}
/**
* Test plugin custom routes with invalid route
*/
public function testRegisteredRoutesInvalid(): void
{
$plugin = 'test_route_invalid';
$this->pluginManager->load([$plugin]);
$meta = $this->pluginManager->getRegisteredRoutes();
static::assertSame([], $meta);
$errors = $this->pluginManager->getErrors();
static::assertSame(['test_route_invalid [plugin incompatibility]: trying to register invalid route.'], $errors);
}
public function testSearchFilterPlugin(): void
{
PluginManager::$PLUGINS_PATH = self::$pluginPath;
$this->pluginManager->load([self::$pluginName]);
static::assertNull($this->pluginManager->getFilterSearchEntryHooks());
static::assertTrue($this->pluginManager->filterSearchEntry(new Bookmark(), ['_result' => true]));
static::assertCount(1, $this->pluginManager->getFilterSearchEntryHooks());
static::assertSame('hook_test_filter_search_entry', $this->pluginManager->getFilterSearchEntryHooks()[0]);
static::assertFalse($this->pluginManager->filterSearchEntry(new Bookmark(), ['_result' => false]));
}
} }

View file

@ -3,6 +3,7 @@
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -56,6 +57,7 @@ protected function setUp(): void
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['history'] = $history; $this->container['history'] = $history;
$this->container['pluginManager'] = new PluginManager($this->conf);
} }
/** /**

View file

@ -5,6 +5,7 @@
use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
@ -55,12 +56,18 @@ protected function setUp(): void
$this->conf->set('resource.datastore', self::$testDatastore); $this->conf->set('resource.datastore', self::$testDatastore);
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$this->pluginManager = new PluginManager($this->conf);
$history = new History('sandbox/history.php'); $history = new History('sandbox/history.php');
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true); $this->container['db'] = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$history,
$mutex,
true
);
$this->container['history'] = null; $this->container['history'] = null;
$this->controller = new Info($this->container); $this->controller = new Info($this->container);

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -57,6 +58,9 @@ class DeleteLinkTest extends \Shaarli\TestCase
/** @var NoMutex */ /** @var NoMutex */
protected $mutex; protected $mutex;
/** @var PluginManager */
protected $pluginManager;
/** /**
* Before each test, instantiate a new Api with its config, plugins and bookmarks. * Before each test, instantiate a new Api with its config, plugins and bookmarks.
*/ */
@ -70,7 +74,14 @@ protected function setUp(): void
$refHistory = new \ReferenceHistory(); $refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory); $refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory); $this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->pluginManager = new PluginManager($this->conf);
$this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
@ -105,7 +116,13 @@ public function testDeleteLinkValid()
$this->assertEquals(204, $response->getStatusCode()); $this->assertEquals(204, $response->getStatusCode());
$this->assertEmpty((string) $response->getBody()); $this->assertEmpty((string) $response->getBody());
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$this->assertFalse($this->bookmarkService->exists($id)); $this->assertFalse($this->bookmarkService->exists($id));
$historyEntry = $this->history->getHistory()[0]; $historyEntry = $this->history->getHistory()[0];

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -67,7 +68,14 @@ protected function setUp(): void
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true); $pluginManager = new PluginManager($this->conf);
$this->container['db'] = new BookmarkFileService(
$this->conf,
$pluginManager,
$history,
$mutex,
true
);
$this->container['history'] = null; $this->container['history'] = null;
$this->controller = new Links($this->container); $this->controller = new Links($this->container);

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -67,7 +68,14 @@ protected function setUp(): void
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true); $pluginManager = new PluginManager($this->conf);
$this->container['db'] = new BookmarkFileService(
$this->conf,
$pluginManager,
$history,
$mutex,
true
);
$this->container['history'] = null; $this->container['history'] = null;
$this->controller = new Links($this->container); $this->controller = new Links($this->container);

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
@ -81,8 +82,14 @@ protected function setUp(): void
$refHistory = new \ReferenceHistory(); $refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory); $refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory); $this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true); $pluginManager = new PluginManager($this->conf);
$this->bookmarkService = new BookmarkFileService(
$this->conf,
$pluginManager,
$this->history,
$mutex,
true
);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = $this->bookmarkService; $this->container['db'] = $this->bookmarkService;
@ -229,4 +236,52 @@ public function testPostLinkDuplicate()
\DateTime::createFromFormat(\DateTime::ATOM, $data['updated']) \DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
); );
} }
/**
* Test link creation with a tag string provided
*/
public function testPostLinkWithTagString(): void
{
$link = [
'tags' => 'one two',
];
$env = Environment::mock([
'REQUEST_METHOD' => 'POST',
'CONTENT_TYPE' => 'application/json'
]);
$request = Request::createFromEnvironment($env);
$request = $request->withParsedBody($link);
$response = $this->controller->postLink($request, new Response());
$this->assertEquals(201, $response->getStatusCode());
$this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]);
$data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(['one', 'two'], $data['tags']);
}
/**
* Test link creation with a tag string provided
*/
public function testPostLinkWithTagString2(): void
{
$link = [
'tags' => ['one two'],
];
$env = Environment::mock([
'REQUEST_METHOD' => 'POST',
'CONTENT_TYPE' => 'application/json'
]);
$request = Request::createFromEnvironment($env);
$request = $request->withParsedBody($link);
$response = $this->controller->postLink($request, new Response());
$this->assertEquals(201, $response->getStatusCode());
$this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]);
$data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(['one', 'two'], $data['tags']);
}
} }

View file

@ -8,6 +8,7 @@
use Shaarli\Bookmark\BookmarkFileService; use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -73,8 +74,14 @@ protected function setUp(): void
$refHistory = new \ReferenceHistory(); $refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory); $refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory); $this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true); $pluginManager = new PluginManager($this->conf);
$this->bookmarkService = new BookmarkFileService(
$this->conf,
$pluginManager,
$this->history,
$mutex,
true
);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = $this->bookmarkService; $this->container['db'] = $this->bookmarkService;
@ -233,4 +240,52 @@ public function testGetLink404()
$this->controller->putLink($request, new Response(), ['id' => -1]); $this->controller->putLink($request, new Response(), ['id' => -1]);
} }
/**
* Test link creation with a tag string provided
*/
public function testPutLinkWithTagString(): void
{
$link = [
'tags' => 'one two',
];
$id = '41';
$env = Environment::mock([
'REQUEST_METHOD' => 'PUT',
'CONTENT_TYPE' => 'application/json'
]);
$request = Request::createFromEnvironment($env);
$request = $request->withParsedBody($link);
$response = $this->controller->putLink($request, new Response(), ['id' => $id]);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(['one', 'two'], $data['tags']);
}
/**
* Test link creation with a tag string provided
*/
public function testPutLinkWithTagString2(): void
{
$link = [
'tags' => ['one two'],
];
$id = '41';
$env = Environment::mock([
'REQUEST_METHOD' => 'PUT',
'CONTENT_TYPE' => 'application/json'
]);
$request = Request::createFromEnvironment($env);
$request = $request->withParsedBody($link);
$response = $this->controller->putLink($request, new Response(), ['id' => $id]);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(['one', 'two'], $data['tags']);
}
} }

View file

@ -8,6 +8,7 @@
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -55,6 +56,9 @@ class DeleteTagTest extends \Shaarli\TestCase
*/ */
protected $controller; protected $controller;
/** @var PluginManager */
protected $pluginManager;
/** @var NoMutex */ /** @var NoMutex */
protected $mutex; protected $mutex;
@ -71,7 +75,14 @@ protected function setUp(): void
$refHistory = new \ReferenceHistory(); $refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory); $refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory); $this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->pluginManager = new PluginManager($this->conf);
$this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
@ -107,7 +118,13 @@ public function testDeleteTagValid()
$this->assertEquals(204, $response->getStatusCode()); $this->assertEquals(204, $response->getStatusCode());
$this->assertEmpty((string) $response->getBody()); $this->assertEmpty((string) $response->getBody());
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$tags = $this->bookmarkService->bookmarksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertFalse(isset($tags[$tagName])); $this->assertFalse(isset($tags[$tagName]));
@ -141,7 +158,13 @@ public function testDeleteTagCaseSensitivity()
$this->assertEquals(204, $response->getStatusCode()); $this->assertEquals(204, $response->getStatusCode());
$this->assertEmpty((string) $response->getBody()); $this->assertEmpty((string) $response->getBody());
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$tags = $this->bookmarkService->bookmarksCountPerTag(); $tags = $this->bookmarkService->bookmarksCountPerTag();
$this->assertFalse(isset($tags[$tagName])); $this->assertFalse(isset($tags[$tagName]));
$this->assertTrue($tags[strtolower($tagName)] > 0); $this->assertTrue($tags[strtolower($tagName)] > 0);

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -46,6 +47,9 @@ class GetTagNameTest extends \Shaarli\TestCase
*/ */
protected $controller; protected $controller;
/** @var PluginManager */
protected $pluginManager;
/** /**
* Number of JSON fields per link. * Number of JSON fields per link.
*/ */
@ -65,7 +69,14 @@ protected function setUp(): void
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = new BookmarkFileService($this->conf, $history, $mutex, true); $this->pluginManager = new PluginManager($this->conf);
$this->container['db'] = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$history,
$mutex,
true
);
$this->container['history'] = null; $this->container['history'] = null;
$this->controller = new Tags($this->container); $this->controller = new Tags($this->container);

View file

@ -6,6 +6,7 @@
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -50,6 +51,9 @@ class GetTagsTest extends \Shaarli\TestCase
*/ */
protected $controller; protected $controller;
/** @var PluginManager */
protected $pluginManager;
/** /**
* Number of JSON field per link. * Number of JSON field per link.
*/ */
@ -66,9 +70,14 @@ protected function setUp(): void
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php'); $history = new History('sandbox/history.php');
$this->pluginManager = new PluginManager($this->conf);
$this->bookmarkService = new BookmarkFileService($this->conf, $history, $mutex, true); $this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$history,
$mutex,
true
);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;
$this->container['db'] = $this->bookmarkService; $this->container['db'] = $this->bookmarkService;

View file

@ -8,6 +8,7 @@
use Shaarli\Bookmark\LinkDB; use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container; use Slim\Container;
use Slim\Http\Environment; use Slim\Http\Environment;
use Slim\Http\Request; use Slim\Http\Request;
@ -55,6 +56,9 @@ class PutTagTest extends \Shaarli\TestCase
*/ */
protected $controller; protected $controller;
/** @var PluginManager */
protected $pluginManager;
/** /**
* Number of JSON field per link. * Number of JSON field per link.
*/ */
@ -73,7 +77,14 @@ protected function setUp(): void
$refHistory = new \ReferenceHistory(); $refHistory = new \ReferenceHistory();
$refHistory->write(self::$testHistory); $refHistory->write(self::$testHistory);
$this->history = new History(self::$testHistory); $this->history = new History(self::$testHistory);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true); $this->pluginManager = new PluginManager($this->conf);
$this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$mutex,
true
);
$this->container = new Container(); $this->container = new Container();
$this->container['conf'] = $this->conf; $this->container['conf'] = $this->conf;

View file

@ -14,6 +14,7 @@
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\BookmarkMarkdownFormatter; use Shaarli\Formatter\BookmarkMarkdownFormatter;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
/** /**
@ -56,6 +57,9 @@ class BookmarkFileServiceTest extends TestCase
/** @var NoMutex */ /** @var NoMutex */
protected $mutex; protected $mutex;
/** @var PluginManager */
protected $pluginManager;
/** /**
* Instantiates public and private LinkDBs with test data * Instantiates public and private LinkDBs with test data
* *
@ -93,8 +97,21 @@ protected function setUp(): void
$this->refDB = new \ReferenceLinkDB(); $this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore); $this->refDB->write(self::$testDatastore);
$this->history = new History('sandbox/history.php'); $this->history = new History('sandbox/history.php');
$this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); $this->pluginManager = new PluginManager($this->conf);
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->publicLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
false
);
$this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
} }
/** /**
@ -111,7 +128,13 @@ public function testDatabaseMigration()
$db = self::getMethod('migrate'); $db = self::getMethod('migrate');
$db->invokeArgs($this->privateLinkDB, []); $db->invokeArgs($this->privateLinkDB, []);
$db = new \FakeBookmarkService($this->conf, $this->history, $this->mutex, true); $db = new \FakeBookmarkService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks()); $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
$this->assertEquals($this->refDB->countLinks(), $db->count()); $this->assertEquals($this->refDB->countLinks(), $db->count());
} }
@ -180,7 +203,13 @@ public function testAddFull()
$this->assertEquals($updated, $bookmark->getUpdated()); $this->assertEquals($updated, $bookmark->getUpdated());
// reload from file // reload from file
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->privateLinkDB = new \FakeBookmarkService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$bookmark = $this->privateLinkDB->get(43); $bookmark = $this->privateLinkDB->get(43);
$this->assertEquals(43, $bookmark->getId()); $this->assertEquals(43, $bookmark->getId());
@ -218,7 +247,13 @@ public function testAddMinimal()
$this->assertNull($bookmark->getUpdated()); $this->assertNull($bookmark->getUpdated());
// reload from file // reload from file
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$bookmark = $this->privateLinkDB->get(43); $bookmark = $this->privateLinkDB->get(43);
$this->assertEquals(43, $bookmark->getId()); $this->assertEquals(43, $bookmark->getId());
@ -248,7 +283,13 @@ public function testAddMinimalNoWrite()
$this->assertEquals(43, $bookmark->getId()); $this->assertEquals(43, $bookmark->getId());
// reload from file // reload from file
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$this->privateLinkDB->get(43); $this->privateLinkDB->get(43);
} }
@ -309,7 +350,13 @@ public function testSetFull()
$this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated()); $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
// reload from file // reload from file
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$bookmark = $this->privateLinkDB->get(42); $bookmark = $this->privateLinkDB->get(42);
$this->assertEquals(42, $bookmark->getId()); $this->assertEquals(42, $bookmark->getId());
@ -350,7 +397,13 @@ public function testSetMinimal()
$this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated()); $this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
// reload from file // reload from file
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$bookmark = $this->privateLinkDB->get(42); $bookmark = $this->privateLinkDB->get(42);
$this->assertEquals(42, $bookmark->getId()); $this->assertEquals(42, $bookmark->getId());
@ -383,7 +436,13 @@ public function testSetMinimalNoWrite()
$this->assertEquals($title, $bookmark->getTitle()); $this->assertEquals($title, $bookmark->getTitle());
// reload from file // reload from file
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$bookmark = $this->privateLinkDB->get(42); $bookmark = $this->privateLinkDB->get(42);
$this->assertEquals(42, $bookmark->getId()); $this->assertEquals(42, $bookmark->getId());
@ -436,7 +495,13 @@ public function testAddOrSetNew()
$this->assertEquals(43, $bookmark->getId()); $this->assertEquals(43, $bookmark->getId());
// reload from file // reload from file
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$bookmark = $this->privateLinkDB->get(43); $bookmark = $this->privateLinkDB->get(43);
$this->assertEquals(43, $bookmark->getId()); $this->assertEquals(43, $bookmark->getId());
@ -456,7 +521,13 @@ public function testAddOrSetExisting()
$this->assertEquals($title, $bookmark->getTitle()); $this->assertEquals($title, $bookmark->getTitle());
// reload from file // reload from file
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$bookmark = $this->privateLinkDB->get(42); $bookmark = $this->privateLinkDB->get(42);
$this->assertEquals(42, $bookmark->getId()); $this->assertEquals(42, $bookmark->getId());
@ -488,7 +559,13 @@ public function testAddOrSetMinimalNoWrite()
$this->assertEquals($title, $bookmark->getTitle()); $this->assertEquals($title, $bookmark->getTitle());
// reload from file // reload from file
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$bookmark = $this->privateLinkDB->get(42); $bookmark = $this->privateLinkDB->get(42);
$this->assertEquals(42, $bookmark->getId()); $this->assertEquals(42, $bookmark->getId());
@ -514,7 +591,13 @@ public function testRemoveExisting()
$this->assertInstanceOf(BookmarkNotFoundException::class, $exception); $this->assertInstanceOf(BookmarkNotFoundException::class, $exception);
// reload from file // reload from file
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$this->privateLinkDB->get(42); $this->privateLinkDB->get(42);
} }
@ -607,7 +690,7 @@ public function testConstructDatastoreNotWriteable()
$conf = new ConfigManager('tests/utils/config/configJson'); $conf = new ConfigManager('tests/utils/config/configJson');
$conf->set('resource.datastore', 'null/store.db'); $conf->set('resource.datastore', 'null/store.db');
new BookmarkFileService($conf, $this->history, $this->mutex, true); new BookmarkFileService($conf, $this->pluginManager, $this->history, $this->mutex, true);
} }
/** /**
@ -617,7 +700,7 @@ public function testCheckDBNewLoggedIn()
{ {
unlink(self::$testDatastore); unlink(self::$testDatastore);
$this->assertFileNotExists(self::$testDatastore); $this->assertFileNotExists(self::$testDatastore);
new BookmarkFileService($this->conf, $this->history, $this->mutex, true); new BookmarkFileService($this->conf, $this->pluginManager, $this->history, $this->mutex, true);
$this->assertFileExists(self::$testDatastore); $this->assertFileExists(self::$testDatastore);
// ensure the correct data has been written // ensure the correct data has been written
@ -631,7 +714,7 @@ public function testCheckDBNewLoggedOut()
{ {
unlink(self::$testDatastore); unlink(self::$testDatastore);
$this->assertFileNotExists(self::$testDatastore); $this->assertFileNotExists(self::$testDatastore);
$db = new \FakeBookmarkService($this->conf, $this->history, $this->mutex, false); $db = new \FakeBookmarkService($this->conf, $this->pluginManager, $this->history, $this->mutex, false);
$this->assertFileNotExists(self::$testDatastore); $this->assertFileNotExists(self::$testDatastore);
$this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks()); $this->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
$this->assertCount(0, $db->getBookmarks()); $this->assertCount(0, $db->getBookmarks());
@ -664,13 +747,13 @@ public function testReadPrivateDB()
*/ */
public function testSave() public function testSave()
{ {
$testDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $testDB = new BookmarkFileService($this->conf, $this->pluginManager, $this->history, $this->mutex, true);
$dbSize = $testDB->count(); $dbSize = $testDB->count();
$bookmark = new Bookmark(); $bookmark = new Bookmark();
$testDB->add($bookmark); $testDB->add($bookmark);
$testDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $testDB = new BookmarkFileService($this->conf, $this->pluginManager, $this->history, $this->mutex, true);
$this->assertEquals($dbSize + 1, $testDB->count()); $this->assertEquals($dbSize + 1, $testDB->count());
} }
@ -680,7 +763,7 @@ public function testSave()
public function testCountHiddenPublic() public function testCountHiddenPublic()
{ {
$this->conf->set('privacy.hide_public_links', true); $this->conf->set('privacy.hide_public_links', true);
$linkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); $linkDB = new BookmarkFileService($this->conf, $this->pluginManager, $this->history, $this->mutex, false);
$this->assertEquals(0, $linkDB->count()); $this->assertEquals(0, $linkDB->count());
} }
@ -807,7 +890,7 @@ public function testFilterString()
$request = ['searchtags' => $tags]; $request = ['searchtags' => $tags];
$this->assertEquals( $this->assertEquals(
2, 2,
count($this->privateLinkDB->search($request, null, true)) count($this->privateLinkDB->search($request, null, true)->getBookmarks())
); );
} }
@ -820,7 +903,7 @@ public function testFilterArray()
$request = ['searchtags' => $tags]; $request = ['searchtags' => $tags];
$this->assertEquals( $this->assertEquals(
2, 2,
count($this->privateLinkDB->search($request, null, true)) count($this->privateLinkDB->search($request, null, true)->getBookmarks())
); );
} }
@ -834,12 +917,12 @@ public function testHiddenTags()
$request = ['searchtags' => $tags]; $request = ['searchtags' => $tags];
$this->assertEquals( $this->assertEquals(
1, 1,
count($this->privateLinkDB->search($request, 'all', true)) count($this->privateLinkDB->search($request, 'all', true)->getBookmarks())
); );
$this->assertEquals( $this->assertEquals(
0, 0,
count($this->publicLinkDB->search($request, 'public', true)) count($this->publicLinkDB->search($request, 'public', true)->getBookmarks())
); );
} }
@ -906,7 +989,13 @@ public function testFilterHashWithPrivateKey()
$bookmark->addAdditionalContentEntry('private_key', $privateKey); $bookmark->addAdditionalContentEntry('private_key', $privateKey);
$this->privateLinkDB->save(); $this->privateLinkDB->save();
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); $this->privateLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
false
);
$bookmark = $this->privateLinkDB->findByHash($hash, $privateKey); $bookmark = $this->privateLinkDB->findByHash($hash, $privateKey);
static::assertSame(6, $bookmark->getId()); static::assertSame(6, $bookmark->getId());
@ -1152,7 +1241,13 @@ public function testGetLatestWithSticky(): void
public function testGetLatestEmptyDatastore(): void public function testGetLatestEmptyDatastore(): void
{ {
unlink($this->conf->get('resource.datastore')); unlink($this->conf->get('resource.datastore'));
$this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false); $this->publicLinkDB = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
false
);
$bookmark = $this->publicLinkDB->getLatest(); $bookmark = $this->publicLinkDB->getLatest();

View file

@ -6,6 +6,7 @@
use ReferenceLinkDB; use ReferenceLinkDB;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
/** /**
@ -32,19 +33,24 @@ class BookmarkFilterTest extends TestCase
*/ */
protected static $bookmarkService; protected static $bookmarkService;
/** @var PluginManager */
protected static $pluginManager;
/** /**
* Instantiate linkFilter with ReferenceLinkDB data. * Instantiate linkFilter with ReferenceLinkDB data.
*/ */
public static function setUpBeforeClass(): void public static function setUpBeforeClass(): void
{ {
$mutex = new NoMutex(); $mutex = new NoMutex();
$conf = new ConfigManager('tests/utils/config/configJson'); $conf = new ConfigManager('tests/utils/config/configJson');
$conf->set('resource.datastore', self::$testDatastore); $conf->set('resource.datastore', self::$testDatastore);
static::$pluginManager = new PluginManager($conf);
self::$refDB = new \ReferenceLinkDB(); self::$refDB = new \ReferenceLinkDB();
self::$refDB->write(self::$testDatastore); self::$refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php'); $history = new History('sandbox/history.php');
self::$bookmarkService = new \FakeBookmarkService($conf, $history, $mutex, true); self::$bookmarkService = new \FakeBookmarkService($conf, static::$pluginManager, $history, $mutex, true);
self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks(), $conf); self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks(), $conf, static::$pluginManager);
} }
/** /**
@ -178,61 +184,6 @@ public function testFilterUnknownTag()
); );
} }
/**
* Return bookmarks for a given day
*/
public function testFilterDay()
{
$this->assertEquals(
4,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20121206'))
);
}
/**
* Return bookmarks for a given day
*/
public function testFilterDayRestrictedVisibility(): void
{
$this->assertEquals(
3,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20121206', false, BookmarkFilter::$PUBLIC))
);
}
/**
* 404 - day not found
*/
public function testFilterUnknownDay()
{
$this->assertEquals(
0,
count(self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '19700101'))
);
}
/**
* Use an invalid date format
*/
public function testFilterInvalidDayWithChars()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessageRegExp('/Invalid date format/');
self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, 'Rainy day, dream away');
}
/**
* Use an invalid date format
*/
public function testFilterInvalidDayDigits()
{
$this->expectException(\Exception::class);
$this->expectExceptionMessageRegExp('/Invalid date format/');
self::$linkFilter->filter(BookmarkFilter::$FILTER_DAY, '20');
}
/** /**
* Retrieve a link entry with its hash * Retrieve a link entry with its hash
*/ */

View file

@ -5,6 +5,7 @@
use malkusch\lock\mutex\NoMutex; use malkusch\lock\mutex\NoMutex;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
/** /**
@ -38,6 +39,9 @@ class BookmarkInitializerTest extends TestCase
/** @var NoMutex */ /** @var NoMutex */
protected $mutex; protected $mutex;
/** @var PluginManager */
protected $pluginManager;
/** /**
* Initialize an empty BookmarkFileService * Initialize an empty BookmarkFileService
*/ */
@ -51,8 +55,15 @@ public function setUp(): void
copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php'); copy('tests/utils/config/configJson.json.php', self::$testConf .'.json.php');
$this->conf = new ConfigManager(self::$testConf); $this->conf = new ConfigManager(self::$testConf);
$this->conf->set('resource.datastore', self::$testDatastore); $this->conf->set('resource.datastore', self::$testDatastore);
$this->pluginManager = new PluginManager($this->conf);
$this->history = new History('sandbox/history.php'); $this->history = new History('sandbox/history.php');
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$this->initializer = new BookmarkInitializer($this->bookmarkService); $this->initializer = new BookmarkInitializer($this->bookmarkService);
} }
@ -64,7 +75,13 @@ public function testInitializeNotEmptyDataStore(): void
{ {
$refDB = new \ReferenceLinkDB(); $refDB = new \ReferenceLinkDB();
$refDB->write(self::$testDatastore); $refDB->write(self::$testDatastore);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$this->initializer = new BookmarkInitializer($this->bookmarkService); $this->initializer = new BookmarkInitializer($this->bookmarkService);
$this->initializer->initialize(); $this->initializer->initialize();
@ -95,7 +112,13 @@ public function testInitializeNotEmptyDataStore(): void
$this->bookmarkService->save(); $this->bookmarkService->save();
// Reload from file // Reload from file
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$this->assertEquals($refDB->countLinks() + 3, $this->bookmarkService->count()); $this->assertEquals($refDB->countLinks() + 3, $this->bookmarkService->count());
$bookmark = $this->bookmarkService->get(43); $bookmark = $this->bookmarkService->get(43);
@ -126,7 +149,13 @@ public function testInitializeNotEmptyDataStore(): void
public function testInitializeNonExistentDataStore(): void public function testInitializeNonExistentDataStore(): void
{ {
$this->conf->set('resource.datastore', static::$testDatastore . '_empty'); $this->conf->set('resource.datastore', static::$testDatastore . '_empty');
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $this->mutex, true); $this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$this->mutex,
true
);
$this->initializer->initialize(); $this->initializer->initialize();

View file

@ -245,6 +245,16 @@ public function testHtmlExtractNonExistentOgTag()
$this->assertFalse(html_extract_tag('description', $html)); $this->assertFalse(html_extract_tag('description', $html));
} }
public function testHtmlExtractDescriptionFromGoogleRealCase(): void
{
$html = 'id="gsr"><meta content="Fêtes de fin d\'année" property="twitter:title"><meta '.
'content="Bonnes fêtes de fin d\'année ! #GoogleDoodle" property="twitter:description">'.
'<meta content="Bonnes fêtes de fin d\'année ! #GoogleDoodle" property="og:description">'.
'<meta content="summary_large_image" property="twitter:card"><meta co'
;
$this->assertSame('Bonnes fêtes de fin d\'année ! #GoogleDoodle', html_extract_tag('description', $html));
}
/** /**
* Test the header callback with valid value * Test the header callback with valid value
*/ */

View file

@ -0,0 +1,125 @@
<?php
declare(strict_types=1);
namespace Shaarli\Bookmark;
use Shaarli\TestCase;
/**
* Test SearchResult class.
*/
class SearchResultTest extends TestCase
{
/** Create a SearchResult without any pagination parameter. */
public function testResultNoParameters(): void
{
$searchResult = SearchResult::getSearchResult($data = ['a', 'b', 'c', 'd', 'e', 'f']);
static::assertSame($data, $searchResult->getBookmarks());
static::assertSame(6, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(null, $searchResult->getLimit());
static::assertSame(0, $searchResult->getOffset());
static::assertSame(1, $searchResult->getPage());
static::assertSame(1, $searchResult->getLastPage());
static::assertTrue($searchResult->isFirstPage());
static::assertTrue($searchResult->isLastPage());
}
/** Create a SearchResult with only an offset parameter */
public function testResultWithOffset(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 2);
static::assertSame([2 => 'c', 3 => 'd', 4 => 'e', 5 => 'f'], $searchResult->getBookmarks());
static::assertSame(4, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(null, $searchResult->getLimit());
static::assertSame(2, $searchResult->getOffset());
static::assertSame(2, $searchResult->getPage());
static::assertSame(2, $searchResult->getLastPage());
static::assertFalse($searchResult->isFirstPage());
static::assertTrue($searchResult->isLastPage());
}
/** Create a SearchResult with only a limit parameter */
public function testResultWithLimit(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 0, 2);
static::assertSame([0 => 'a', 1 => 'b'], $searchResult->getBookmarks());
static::assertSame(2, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(2, $searchResult->getLimit());
static::assertSame(0, $searchResult->getOffset());
static::assertSame(1, $searchResult->getPage());
static::assertSame(3, $searchResult->getLastPage());
static::assertTrue($searchResult->isFirstPage());
static::assertFalse($searchResult->isLastPage());
}
/** Create a SearchResult with offset and limit parameters */
public function testResultWithLimitAndOffset(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 2, 2);
static::assertSame([2 => 'c', 3 => 'd'], $searchResult->getBookmarks());
static::assertSame(2, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(2, $searchResult->getLimit());
static::assertSame(2, $searchResult->getOffset());
static::assertSame(2, $searchResult->getPage());
static::assertSame(3, $searchResult->getLastPage());
static::assertFalse($searchResult->isFirstPage());
static::assertFalse($searchResult->isLastPage());
}
/** Create a SearchResult with offset and limit parameters displaying the last page */
public function testResultWithLimitAndOffsetLastPage(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 4, 2);
static::assertSame([4 => 'e', 5 => 'f'], $searchResult->getBookmarks());
static::assertSame(2, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(2, $searchResult->getLimit());
static::assertSame(4, $searchResult->getOffset());
static::assertSame(3, $searchResult->getPage());
static::assertSame(3, $searchResult->getLastPage());
static::assertFalse($searchResult->isFirstPage());
static::assertTrue($searchResult->isLastPage());
}
/** Create a SearchResult with offset and limit parameters out of bound (display the last page) */
public function testResultWithLimitAndOffsetOutOfBounds(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 12, 2);
static::assertSame([4 => 'e', 5 => 'f'], $searchResult->getBookmarks());
static::assertSame(2, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(2, $searchResult->getLimit());
static::assertSame(-2, $searchResult->getOffset());
static::assertSame(3, $searchResult->getPage());
static::assertSame(3, $searchResult->getLastPage());
static::assertFalse($searchResult->isFirstPage());
static::assertTrue($searchResult->isLastPage());
}
/** Create a SearchResult with offset and limit parameters out of bound (no result) */
public function testResultWithLimitAndOffsetOutOfBoundsNoResult(): void
{
$searchResult = SearchResult::getSearchResult(['a', 'b', 'c', 'd', 'e', 'f'], 12, 2, true);
static::assertSame([], $searchResult->getBookmarks());
static::assertSame(0, $searchResult->getResultCount());
static::assertSame(6, $searchResult->getTotalCount());
static::assertSame(2, $searchResult->getLimit());
static::assertSame(12, $searchResult->getOffset());
static::assertSame(7, $searchResult->getPage());
static::assertSame(3, $searchResult->getLastPage());
static::assertFalse($searchResult->isFirstPage());
static::assertFalse($searchResult->isLastPage());
}
}

View file

@ -43,11 +43,15 @@ class ContainerBuilderTest extends TestCase
/** @var CookieManager */ /** @var CookieManager */
protected $cookieManager; protected $cookieManager;
/** @var PluginManager */
protected $pluginManager;
public function setUp(): void public function setUp(): void
{ {
$this->conf = new ConfigManager('tests/utils/config/configJson'); $this->conf = new ConfigManager('tests/utils/config/configJson');
$this->sessionManager = $this->createMock(SessionManager::class); $this->sessionManager = $this->createMock(SessionManager::class);
$this->cookieManager = $this->createMock(CookieManager::class); $this->cookieManager = $this->createMock(CookieManager::class);
$this->pluginManager = $this->createMock(PluginManager::class);
$this->loginManager = $this->createMock(LoginManager::class); $this->loginManager = $this->createMock(LoginManager::class);
$this->loginManager->method('isLoggedIn')->willReturn(true); $this->loginManager->method('isLoggedIn')->willReturn(true);
@ -57,6 +61,7 @@ public function setUp(): void
$this->sessionManager, $this->sessionManager,
$this->cookieManager, $this->cookieManager,
$this->loginManager, $this->loginManager,
$this->pluginManager,
$this->createMock(LoggerInterface::class) $this->createMock(LoggerInterface::class)
); );
} }

View file

@ -40,10 +40,10 @@ protected function setUp(): void
*/ */
public function testConstruct() public function testConstruct()
{ {
new CachedPage(self::$testCacheDir, '', true); new CachedPage(self::$testCacheDir, '', true, null);
new CachedPage(self::$testCacheDir, '', false); new CachedPage(self::$testCacheDir, '', false, null);
new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true); new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/rss', true, null);
new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false); new CachedPage(self::$testCacheDir, 'http://shaar.li/feed/atom', false, null);
$this->addToAssertionCount(1); $this->addToAssertionCount(1);
} }
@ -52,7 +52,7 @@ public function testConstruct()
*/ */
public function testCache() public function testCache()
{ {
$page = new CachedPage(self::$testCacheDir, self::$url, true); $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
$this->assertFileNotExists(self::$filename); $this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>'); $page->cache('<p>Some content</p>');
@ -68,7 +68,7 @@ public function testCache()
*/ */
public function testShouldNotCache() public function testShouldNotCache()
{ {
$page = new CachedPage(self::$testCacheDir, self::$url, false); $page = new CachedPage(self::$testCacheDir, self::$url, false, null);
$this->assertFileNotExists(self::$filename); $this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>'); $page->cache('<p>Some content</p>');
@ -80,7 +80,7 @@ public function testShouldNotCache()
*/ */
public function testCachedVersion() public function testCachedVersion()
{ {
$page = new CachedPage(self::$testCacheDir, self::$url, true); $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
$this->assertFileNotExists(self::$filename); $this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>'); $page->cache('<p>Some content</p>');
@ -96,7 +96,7 @@ public function testCachedVersion()
*/ */
public function testCachedVersionNoFile() public function testCachedVersionNoFile()
{ {
$page = new CachedPage(self::$testCacheDir, self::$url, true); $page = new CachedPage(self::$testCacheDir, self::$url, true, null);
$this->assertFileNotExists(self::$filename); $this->assertFileNotExists(self::$filename);
$this->assertEquals( $this->assertEquals(
@ -110,7 +110,7 @@ public function testCachedVersionNoFile()
*/ */
public function testNoCachedVersion() public function testNoCachedVersion()
{ {
$page = new CachedPage(self::$testCacheDir, self::$url, false); $page = new CachedPage(self::$testCacheDir, self::$url, false, null);
$this->assertFileNotExists(self::$filename); $this->assertFileNotExists(self::$filename);
$this->assertEquals( $this->assertEquals(
@ -118,4 +118,43 @@ public function testNoCachedVersion()
$page->cachedVersion() $page->cachedVersion()
); );
} }
/**
* Return a page's cached content within date period
*/
public function testCachedVersionInDatePeriod()
{
$period = new \DatePeriod(
new \DateTime('yesterday'),
new \DateInterval('P1D'),
new \DateTime('tomorrow')
);
$page = new CachedPage(self::$testCacheDir, self::$url, true, $period);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
$this->assertFileExists(self::$filename);
$this->assertEquals(
'<p>Some content</p>',
$page->cachedVersion()
);
}
/**
* Return a page's cached content outside of date period
*/
public function testCachedVersionNotInDatePeriod()
{
$period = new \DatePeriod(
new \DateTime('yesterday noon'),
new \DateInterval('P1D'),
new \DateTime('yesterday midnight')
);
$page = new CachedPage(self::$testCacheDir, self::$url, true, $period);
$this->assertFileNotExists(self::$filename);
$page->cache('<p>Some content</p>');
$this->assertFileExists(self::$filename);
$this->assertNull($page->cachedVersion());
}
} }

View file

@ -11,6 +11,7 @@
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\FormatterFactory; use Shaarli\Formatter\FormatterFactory;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
/** /**
@ -55,8 +56,15 @@ public static function setUpBeforeClass(): void
$refLinkDB->write(self::$testDatastore); $refLinkDB->write(self::$testDatastore);
$history = new History('sandbox/history.php'); $history = new History('sandbox/history.php');
$factory = new FormatterFactory($conf, true); $factory = new FormatterFactory($conf, true);
$pluginManager = new PluginManager($conf);
self::$formatter = $factory->getFormatter(); self::$formatter = $factory->getFormatter();
self::$bookmarkService = new BookmarkFileService($conf, $history, $mutex, true); self::$bookmarkService = new BookmarkFileService(
$conf,
$pluginManager,
$history,
$mutex,
true
);
self::$serverInfo = array( self::$serverInfo = array(
'HTTPS' => 'Off', 'HTTPS' => 'Off',

View file

@ -211,13 +211,17 @@ public function testFormatDescriptionWithSearchHighlight(): void
$this->formatter = new BookmarkDefaultFormatter($this->conf, false); $this->formatter = new BookmarkDefaultFormatter($this->conf, false);
$bookmark = new Bookmark(); $bookmark = new Bookmark();
$bookmark->setDescription('This guide extends and expands on PSR-1, the basic coding standard.'); $bookmark->setDescription(
'This guide extends and expands on PSR-1, the basic coding standard.' . PHP_EOL .
'https://www.php-fig.org/psr/psr-1/'
);
$bookmark->addAdditionalContentEntry( $bookmark->addAdditionalContentEntry(
'search_highlight', 'search_highlight',
['description' => [ ['description' => [
['start' => 0, 'end' => 10], // "This guide" ['start' => 0, 'end' => 10], // "This guide"
['start' => 45, 'end' => 50], // basic ['start' => 45, 'end' => 50], // basic
['start' => 58, 'end' => 67], // standard. ['start' => 58, 'end' => 67], // standard.
['start' => 84, 'end' => 87], // fig
]] ]]
); );
@ -226,7 +230,10 @@ public function testFormatDescriptionWithSearchHighlight(): void
$this->assertSame( $this->assertSame(
'<span class="search-highlight">This guide</span> extends and expands on PSR-1, the ' . '<span class="search-highlight">This guide</span> extends and expands on PSR-1, the ' .
'<span class="search-highlight">basic</span> coding ' . '<span class="search-highlight">basic</span> coding ' .
'<span class="search-highlight">standard.</span>', '<span class="search-highlight">standard.</span><br />' . PHP_EOL .
'<a href="https://www.php-fig.org/psr/psr-1/">' .
'https://www.php-<span class="search-highlight">fig</span>.org/psr/psr-1/' .
'</a>',
$link['description'] $link['description']
); );
} }

View file

@ -132,6 +132,49 @@ public function testFormatDescription()
$this->assertEquals($description, $link['description']); $this->assertEquals($description, $link['description']);
} }
/**
* Make sure that the description is properly formatted by the default formatter.
*/
public function testFormatDescriptionWithSearchHighlight()
{
$description = 'This a <strong>description</strong>'. PHP_EOL;
$description .= 'text https://sub.domain.tld?query=here&for=real#hash more text'. PHP_EOL;
$description .= 'Also, there is an #hashtag added'. PHP_EOL;
$description .= ' A N D KEEP SPACES ! '. PHP_EOL;
$description .= 'And [yet another link](https://other.domain.tld)'. PHP_EOL;
$bookmark = new Bookmark();
$bookmark->setDescription($description);
$bookmark->addAdditionalContentEntry(
'search_highlight',
['description' => [
['start' => 18, 'end' => 26], // cription
['start' => 49, 'end' => 52], // sub
['start' => 84, 'end' => 88], // hash
['start' => 118, 'end' => 123], // hasht
['start' => 203, 'end' => 215], // other.domain
]]
);
$link = $this->formatter->format($bookmark);
$description = '<div class="markdown"><p>';
$description .= 'This a &lt;strong&gt;des<span class="search-highlight">cription</span>&lt;/strong&gt;<br />' .
PHP_EOL;
$url = 'https://sub.domain.tld?query=here&amp;for=real#hash';
$highlighted = 'https://<span class="search-highlight">sub</span>.domain.tld';
$highlighted .= '?query=here&amp;for=real#<span class="search-highlight">hash</span>';
$description .= 'text <a href="'. $url .'">'. $highlighted .'</a> more text<br />'. PHP_EOL;
$description .= 'Also, there is an <a href="./add-tag/hashtag">#<span class="search-highlight">hasht</span>' .
'ag</a> added<br />'. PHP_EOL;
$description .= 'A N D KEEP SPACES !<br />' . PHP_EOL;
$description .= 'And <a href="https://other.domain.tld">' .
'<span class="search-highlight">yet another link</span></a>';
$description .= '</p></div>';
$this->assertEquals($description, $link['description']);
}
/** /**
* Test formatting URL with an index_url set * Test formatting URL with an index_url set
* It should prepend relative links. * It should prepend relative links.

View file

@ -62,7 +62,7 @@ public function testIndex(): void
static::assertSame('privacy.hide_public_links', $assignedVariables['hide_public_links']); static::assertSame('privacy.hide_public_links', $assignedVariables['hide_public_links']);
static::assertSame('api.enabled', $assignedVariables['api_enabled']); static::assertSame('api.enabled', $assignedVariables['api_enabled']);
static::assertSame('api.secret', $assignedVariables['api_secret']); static::assertSame('api.secret', $assignedVariables['api_secret']);
static::assertCount(5, $assignedVariables['languages']); static::assertCount(6, $assignedVariables['languages']);
static::assertArrayHasKey('gd_enabled', $assignedVariables); static::assertArrayHasKey('gd_enabled', $assignedVariables);
static::assertSame('thumbnails.mode', $assignedVariables['thumbnails_mode']); static::assertSame('thumbnails.mode', $assignedVariables['thumbnails_mode']);
} }

View file

@ -6,6 +6,7 @@
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\BookmarkFilter; use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Bookmark\SearchResult;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Front\Exception\WrongTokenException; use Shaarli\Front\Exception\WrongTokenException;
use Shaarli\Security\SessionManager; use Shaarli\Security\SessionManager;
@ -100,11 +101,11 @@ public function testSaveRenameTagValid(): void
->expects(static::once()) ->expects(static::once())
->method('search') ->method('search')
->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true) ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
->willReturnCallback(function () use ($bookmark1, $bookmark2): array { ->willReturnCallback(function () use ($bookmark1, $bookmark2): SearchResult {
$bookmark1->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag'); $bookmark1->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
$bookmark2->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag'); $bookmark2->expects(static::once())->method('renameTag')->with('old-tag', 'new-tag');
return [$bookmark1, $bookmark2]; return SearchResult::getSearchResult([$bookmark1, $bookmark2]);
}) })
; ;
$this->container->bookmarkService $this->container->bookmarkService
@ -153,11 +154,11 @@ public function testSaveDeleteTagValid(): void
->expects(static::once()) ->expects(static::once())
->method('search') ->method('search')
->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true) ->with(['searchtags' => 'old-tag'], BookmarkFilter::$ALL, true)
->willReturnCallback(function () use ($bookmark1, $bookmark2): array { ->willReturnCallback(function () use ($bookmark1, $bookmark2): SearchResult {
$bookmark1->expects(static::once())->method('deleteTag')->with('old-tag'); $bookmark1->expects(static::once())->method('deleteTag')->with('old-tag');
$bookmark2->expects(static::once())->method('deleteTag')->with('old-tag'); $bookmark2->expects(static::once())->method('deleteTag')->with('old-tag');
return [$bookmark1, $bookmark2]; return SearchResult::getSearchResult([$bookmark1, $bookmark2]);
}) })
; ;
$this->container->bookmarkService $this->container->bookmarkService

View file

@ -363,6 +363,7 @@ public function testDeleteBookmarkFromBookmarklet(): void
$this->container->bookmarkService->method('get')->with('123')->willReturn( $this->container->bookmarkService->method('get')->with('123')->willReturn(
(new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123') (new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')
); );
$this->container->bookmarkService->expects(static::once())->method('remove');
$this->container->formatterFactory = $this->createMock(FormatterFactory::class); $this->container->formatterFactory = $this->createMock(FormatterFactory::class);
$this->container->formatterFactory $this->container->formatterFactory
@ -379,6 +380,48 @@ public function testDeleteBookmarkFromBookmarklet(): void
$result = $this->controller->deleteBookmark($request, $response); $result = $this->controller->deleteBookmark($request, $response);
static::assertSame(200, $result->getStatusCode()); static::assertSame(200, $result->getStatusCode());
static::assertSame('<script>self.close();</script>', (string) $result->getBody('location')); static::assertSame('<script>self.close();</script>', (string) $result->getBody());
}
/**
* Delete bookmark - from batch view
*/
public function testDeleteBookmarkFromBatch(): void
{
$parameters = [
'id' => '123',
'source' => 'batch',
];
$request = $this->createMock(Request::class);
$request
->method('getParam')
->willReturnCallback(function (string $key) use ($parameters): ?string {
return $parameters[$key] ?? null;
})
;
$response = new Response();
$this->container->bookmarkService->method('get')->with('123')->willReturn(
(new Bookmark())->setId(123)->setUrl('http://domain.tld')->setTitle('Title 123')
);
$this->container->bookmarkService->expects(static::once())->method('remove');
$this->container->formatterFactory = $this->createMock(FormatterFactory::class);
$this->container->formatterFactory
->expects(static::once())
->method('getFormatter')
->willReturnCallback(function (): BookmarkFormatter {
$formatter = $this->createMock(BookmarkFormatter::class);
$formatter->method('format')->willReturn(['formatted']);
return $formatter;
})
;
$result = $this->controller->deleteBookmark($request, $response);
static::assertSame(204, $result->getStatusCode());
static::assertEmpty((string) $result->getBody());
} }
} }

View file

@ -6,6 +6,7 @@
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\SearchResult;
use Shaarli\TestCase; use Shaarli\TestCase;
use Shaarli\Thumbnailer; use Shaarli\Thumbnailer;
use Slim\Http\Request; use Slim\Http\Request;
@ -40,12 +41,12 @@ public function testIndex(): void
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::once()) ->expects(static::once())
->method('search') ->method('search')
->willReturn([ ->willReturn(SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'), (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
(new Bookmark())->setId(2)->setUrl('?abcdef')->setTitle('Note 1'), (new Bookmark())->setId(2)->setUrl('?abcdef')->setTitle('Note 1'),
(new Bookmark())->setId(3)->setUrl('http://url2.tld')->setTitle('Title 2'), (new Bookmark())->setId(3)->setUrl('http://url2.tld')->setTitle('Title 2'),
(new Bookmark())->setId(4)->setUrl('ftp://domain.tld', ['ftp'])->setTitle('FTP'), (new Bookmark())->setId(4)->setUrl('ftp://domain.tld', ['ftp'])->setTitle('FTP'),
]) ]))
; ;
$result = $this->controller->index($request, $response); $result = $this->controller->index($request, $response);

View file

@ -6,6 +6,7 @@
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException; use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Bookmark\SearchResult;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Security\LoginManager; use Shaarli\Security\LoginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
@ -45,13 +46,15 @@ public function testIndexDefaultFirstPage(): void
['searchtags' => '', 'searchterm' => ''], ['searchtags' => '', 'searchterm' => ''],
null, null,
false, false,
false false,
false,
['offset' => 0, 'limit' => 2]
) )
->willReturn([ ->willReturn(SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'), (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
(new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'), (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
(new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'), (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
] ], 0, 2)
); );
$this->container->sessionManager $this->container->sessionManager
@ -119,13 +122,15 @@ public function testIndexDefaultSecondPage(): void
['searchtags' => '', 'searchterm' => ''], ['searchtags' => '', 'searchterm' => ''],
null, null,
false, false,
false false,
false,
['offset' => 2, 'limit' => 2]
) )
->willReturn([ ->willReturn(SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'), (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
(new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'), (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
(new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'), (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
]) ], 2, 2))
; ;
$this->container->sessionManager $this->container->sessionManager
@ -207,13 +212,15 @@ public function testIndexDefaultWithFilters(): void
['searchtags' => 'abc@def', 'searchterm' => 'ghi jkl'], ['searchtags' => 'abc@def', 'searchterm' => 'ghi jkl'],
'private', 'private',
false, false,
true true,
false,
['offset' => 0, 'limit' => 2]
) )
->willReturn([ ->willReturn(SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'), (new Bookmark())->setId(1)->setUrl('http://url1.tld')->setTitle('Title 1'),
(new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'), (new Bookmark())->setId(2)->setUrl('http://url2.tld')->setTitle('Title 2'),
(new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'), (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setTitle('Title 3'),
]) ], 0, 2))
; ;
$result = $this->controller->index($request, $response); $result = $this->controller->index($request, $response);
@ -358,13 +365,13 @@ public function testThumbnailUpdateFromLinkList(): void
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::once()) ->expects(static::once())
->method('search') ->method('search')
->willReturn([ ->willReturn(SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('https://url1.tld')->setTitle('Title 1')->setThumbnail(false), (new Bookmark())->setId(1)->setUrl('https://url1.tld')->setTitle('Title 1')->setThumbnail(false),
$b1 = (new Bookmark())->setId(2)->setUrl('https://url2.tld')->setTitle('Title 2'), $b1 = (new Bookmark())->setId(2)->setUrl('https://url2.tld')->setTitle('Title 2'),
(new Bookmark())->setId(3)->setUrl('https://url3.tld')->setTitle('Title 3')->setThumbnail(false), (new Bookmark())->setId(3)->setUrl('https://url3.tld')->setTitle('Title 3')->setThumbnail(false),
$b2 = (new Bookmark())->setId(2)->setUrl('https://url4.tld')->setTitle('Title 4'), $b2 = (new Bookmark())->setId(2)->setUrl('https://url4.tld')->setTitle('Title 4'),
(new Bookmark())->setId(2)->setUrl('ftp://url5.tld', ['ftp'])->setTitle('Title 5'), (new Bookmark())->setId(2)->setUrl('ftp://url5.tld', ['ftp'])->setTitle('Title 5'),
]) ]))
; ;
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::exactly(2)) ->expects(static::exactly(2))

View file

@ -5,6 +5,7 @@
namespace Shaarli\Front\Controller\Visitor; namespace Shaarli\Front\Controller\Visitor;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\SearchResult;
use Shaarli\Feed\CachedPage; use Shaarli\Feed\CachedPage;
use Shaarli\TestCase; use Shaarli\TestCase;
use Slim\Http\Request; use Slim\Http\Request;
@ -347,13 +348,15 @@ public function testValidRssControllerInvokeDefault(): void
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
$response = new Response(); $response = new Response();
$this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ $this->container->bookmarkService->expects(static::once())->method('search')->willReturn(
SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
(new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
(new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
(new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'), (new Bookmark())->setId(4)->setCreated($dates[2])->setUrl('http://domain.tld/4'),
(new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'), (new Bookmark())->setId(5)->setCreated($dates[3])->setUrl('http://domain.tld/5'),
]); ])
);
$this->container->pageCacheManager $this->container->pageCacheManager
->expects(static::once()) ->expects(static::once())
@ -454,7 +457,9 @@ public function testValidRssControllerInvokeNoBookmark(): void
$request = $this->createMock(Request::class); $request = $this->createMock(Request::class);
$response = new Response(); $response = new Response();
$this->container->bookmarkService->expects(static::once())->method('search')->willReturn([]); $this->container->bookmarkService
->expects(static::once())->method('search')
->willReturn(SearchResult::getSearchResult([]));
// Save RainTPL assigned variables // Save RainTPL assigned variables
$assignedVariables = []; $assignedVariables = [];
@ -613,11 +618,13 @@ public function testSimpleRssWeekly(): void
}); });
$response = new Response(); $response = new Response();
$this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ $this->container->bookmarkService->expects(static::once())->method('search')->willReturn(
SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
(new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
(new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
]); ])
);
// Save RainTPL assigned variables // Save RainTPL assigned variables
$assignedVariables = []; $assignedVariables = [];
@ -674,11 +681,13 @@ public function testSimpleRssMonthly(): void
}); });
$response = new Response(); $response = new Response();
$this->container->bookmarkService->expects(static::once())->method('search')->willReturn([ $this->container->bookmarkService->expects(static::once())->method('search')->willReturn(
SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'), (new Bookmark())->setId(1)->setCreated($dates[0])->setUrl('http://domain.tld/1'),
(new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'), (new Bookmark())->setId(2)->setCreated($dates[1])->setUrl('http://domain.tld/2'),
(new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'), (new Bookmark())->setId(3)->setCreated($dates[1])->setUrl('http://domain.tld/3'),
]); ])
);
// Save RainTPL assigned variables // Save RainTPL assigned variables
$assignedVariables = []; $assignedVariables = [];

View file

@ -5,6 +5,7 @@
namespace Shaarli\Front\Controller\Visitor; namespace Shaarli\Front\Controller\Visitor;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\SearchResult;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\Front\Exception\ThumbnailsDisabledException; use Shaarli\Front\Exception\ThumbnailsDisabledException;
use Shaarli\TestCase; use Shaarli\TestCase;
@ -50,17 +51,17 @@ public function testValidControllerInvokeDefault(): void
$this->container->bookmarkService $this->container->bookmarkService
->expects(static::once()) ->expects(static::once())
->method('search') ->method('search')
->willReturnCallback(function (array $parameters, ?string $visibility): array { ->willReturnCallback(function (array $parameters, ?string $visibility): SearchResult {
// Visibility is set through the container, not the call // Visibility is set through the container, not the call
static::assertNull($visibility); static::assertNull($visibility);
// No query parameters // No query parameters
if (count($parameters) === 0) { if (count($parameters) === 0) {
return [ return SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'), (new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'),
(new Bookmark())->setId(2)->setUrl('http://url2.tld'), (new Bookmark())->setId(2)->setUrl('http://url2.tld'),
(new Bookmark())->setId(3)->setUrl('http://url3.tld')->setThumbnail('thumb2'), (new Bookmark())->setId(3)->setUrl('http://url3.tld')->setThumbnail('thumb2'),
]; ]);
} }
}) })
; ;

View file

@ -93,6 +93,9 @@ public function testRender(): void
static::assertSame('templateName', $render); static::assertSame('templateName', $render);
static::assertSame('templateName', $this->assignedValues['_PAGE_']);
static::assertSame('templateName', $this->assignedValues['template']);
static::assertSame(10, $this->assignedValues['linkcount']); static::assertSame(10, $this->assignedValues['linkcount']);
static::assertSame(5, $this->assignedValues['privateLinkcount']); static::assertSame(5, $this->assignedValues['privateLinkcount']);
static::assertSame(['error'], $this->assignedValues['plugin_errors']); static::assertSame(['error'], $this->assignedValues['plugin_errors']);

View file

@ -4,6 +4,8 @@
namespace Shaarli\Helper; namespace Shaarli\Helper;
use DateTimeImmutable;
use DateTimeInterface;
use Shaarli\Bookmark\Bookmark; use Shaarli\Bookmark\Bookmark;
use Shaarli\TestCase; use Shaarli\TestCase;
use Slim\Http\Request; use Slim\Http\Request;
@ -32,7 +34,7 @@ public function testExtractRequestedDateTime(
string $type, string $type,
string $input, string $input,
?Bookmark $bookmark, ?Bookmark $bookmark,
\DateTimeInterface $expectedDateTime, DateTimeInterface $expectedDateTime,
string $compareFormat = 'Ymd' string $compareFormat = 'Ymd'
): void { ): void {
$dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark); $dateTime = DailyPageHelper::extractRequestedDateTime($type, $input, $bookmark);
@ -71,8 +73,8 @@ public function testGetFormatByTypeExceptionUnknownType(): void
*/ */
public function testGetStartDatesByType( public function testGetStartDatesByType(
string $type, string $type,
\DateTimeImmutable $dateTime, DateTimeImmutable $dateTime,
\DateTimeInterface $expectedDateTime DateTimeInterface $expectedDateTime
): void { ): void {
$startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime); $startDateTime = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
@ -84,7 +86,7 @@ public function testGetStartDatesByTypeExceptionUnknownType(): void
$this->expectException(\Exception::class); $this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type'); $this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getStartDateTimeByType('nope', new \DateTimeImmutable()); DailyPageHelper::getStartDateTimeByType('nope', new DateTimeImmutable());
} }
/** /**
@ -92,8 +94,8 @@ public function testGetStartDatesByTypeExceptionUnknownType(): void
*/ */
public function testGetEndDatesByType( public function testGetEndDatesByType(
string $type, string $type,
\DateTimeImmutable $dateTime, DateTimeImmutable $dateTime,
\DateTimeInterface $expectedDateTime DateTimeInterface $expectedDateTime
): void { ): void {
$endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime); $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
@ -105,7 +107,7 @@ public function testGetEndDatesByTypeExceptionUnknownType(): void
$this->expectException(\Exception::class); $this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type'); $this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getEndDateTimeByType('nope', new \DateTimeImmutable()); DailyPageHelper::getEndDateTimeByType('nope', new DateTimeImmutable());
} }
/** /**
@ -113,7 +115,7 @@ public function testGetEndDatesByTypeExceptionUnknownType(): void
*/ */
public function testGeDescriptionsByType( public function testGeDescriptionsByType(
string $type, string $type,
\DateTimeImmutable $dateTime, DateTimeImmutable $dateTime,
string $expectedDescription string $expectedDescription
): void { ): void {
$description = DailyPageHelper::getDescriptionByType($type, $dateTime); $description = DailyPageHelper::getDescriptionByType($type, $dateTime);
@ -121,12 +123,25 @@ public function testGeDescriptionsByType(
static::assertEquals($expectedDescription, $description); static::assertEquals($expectedDescription, $description);
} }
/**
* @dataProvider getDescriptionsByTypeNotIncludeRelative
*/
public function testGeDescriptionsByTypeNotIncludeRelative(
string $type,
\DateTimeImmutable $dateTime,
string $expectedDescription
): void {
$description = DailyPageHelper::getDescriptionByType($type, $dateTime, false);
static::assertEquals($expectedDescription, $description);
}
public function getDescriptionByTypeExceptionUnknownType(): void public function getDescriptionByTypeExceptionUnknownType(): void
{ {
$this->expectException(\Exception::class); $this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type'); $this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getDescriptionByType('nope', new \DateTimeImmutable()); DailyPageHelper::getDescriptionByType('nope', new DateTimeImmutable());
} }
/** /**
@ -146,6 +161,29 @@ public function testGeRssLengthsByTypeExceptionUnknownType(): void
DailyPageHelper::getRssLengthByType('nope'); DailyPageHelper::getRssLengthByType('nope');
} }
/**
* @dataProvider getCacheDatePeriodByType
*/
public function testGetCacheDatePeriodByType(
string $type,
DateTimeImmutable $requested,
DateTimeInterface $start,
DateTimeInterface $end
): void {
$period = DailyPageHelper::getCacheDatePeriodByType($type, $requested);
static::assertEquals($start, $period->getStartDate());
static::assertEquals($end, $period->getEndDate());
}
public function testGetCacheDatePeriodByTypeExceptionUnknownType(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Unsupported daily format type');
DailyPageHelper::getCacheDatePeriodByType('nope');
}
/** /**
* Data provider for testExtractRequestedType() test method. * Data provider for testExtractRequestedType() test method.
*/ */
@ -216,9 +254,9 @@ public function getFormatsByType(): array
public function getStartDatesByType(): array public function getStartDatesByType(): array
{ {
return [ return [
[DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')], [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 00:00:00')],
[DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')], [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-05 00:00:00')],
[DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')], [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-01 00:00:00')],
]; ];
} }
@ -228,9 +266,9 @@ public function getStartDatesByType(): array
public function getEndDatesByType(): array public function getEndDatesByType(): array
{ {
return [ return [
[DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')], [DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-09 23:59:59')],
[DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')], [DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-11 23:59:59')],
[DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')], [DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), new \DateTime('2020-10-31 23:59:59')],
]; ];
} }
@ -240,8 +278,22 @@ public function getEndDatesByType(): array
public function getDescriptionsByType(): array public function getDescriptionsByType(): array
{ {
return [ return [
[DailyPageHelper::DAY, $date = new \DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')], [DailyPageHelper::DAY, $date = new DateTimeImmutable(), 'Today - ' . $date->format('F j, Y')],
[DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')], [DailyPageHelper::DAY, $date = new DateTimeImmutable('-1 day'), 'Yesterday - ' . $date->format('F j, Y')],
[DailyPageHelper::DAY, new DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
[DailyPageHelper::WEEK, new DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
[DailyPageHelper::MONTH, new DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
];
}
/**
* Data provider for testGeDescriptionsByTypeNotIncludeRelative() test method.
*/
public function getDescriptionsByTypeNotIncludeRelative(): array
{
return [
[DailyPageHelper::DAY, $date = new \DateTimeImmutable(), $date->format('F j, Y')],
[DailyPageHelper::DAY, $date = new \DateTimeImmutable('-1 day'), $date->format('F j, Y')],
[DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'], [DailyPageHelper::DAY, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October 9, 2020'],
[DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'], [DailyPageHelper::WEEK, new \DateTimeImmutable('2020-10-09 04:05:06'), 'Week 41 (October 5, 2020)'],
[DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'], [DailyPageHelper::MONTH, new \DateTimeImmutable('2020-10-09 04:05:06'), 'October, 2020'],
@ -249,7 +301,7 @@ public function getDescriptionsByType(): array
} }
/** /**
* Data provider for testGetDescriptionsByType() test method. * Data provider for testGetRssLengthsByType() test method.
*/ */
public function getRssLengthsByType(): array public function getRssLengthsByType(): array
{ {
@ -259,4 +311,31 @@ public function getRssLengthsByType(): array
[DailyPageHelper::MONTH], [DailyPageHelper::MONTH],
]; ];
} }
/**
* Data provider for testGetCacheDatePeriodByType() test method.
*/
public function getCacheDatePeriodByType(): array
{
return [
[
DailyPageHelper::DAY,
new DateTimeImmutable('2020-10-09 04:05:06'),
new \DateTime('2020-10-09 00:00:00'),
new \DateTime('2020-10-09 23:59:59'),
],
[
DailyPageHelper::WEEK,
new DateTimeImmutable('2020-10-09 04:05:06'),
new \DateTime('2020-10-05 00:00:00'),
new \DateTime('2020-10-11 23:59:59'),
],
[
DailyPageHelper::MONTH,
new DateTimeImmutable('2020-10-09 04:05:06'),
new \DateTime('2020-10-01 00:00:00'),
new \DateTime('2020-10-31 23:59:59'),
],
];
}
} }

View file

@ -41,7 +41,7 @@ public function testFullRetrieval(): void
$remoteCharset = 'utf-8'; $remoteCharset = 'utf-8';
$expectedResult = [ $expectedResult = [
'title' => $remoteTitle, 'title' => trim($remoteTitle),
'description' => $remoteDesc, 'description' => $remoteDesc,
'tags' => $remoteTags, 'tags' => $remoteTags,
]; ];

View file

@ -8,6 +8,7 @@
use Shaarli\Formatter\BookmarkFormatter; use Shaarli\Formatter\BookmarkFormatter;
use Shaarli\Formatter\FormatterFactory; use Shaarli\Formatter\FormatterFactory;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
require_once 'tests/utils/ReferenceLinkDB.php'; require_once 'tests/utils/ReferenceLinkDB.php';
@ -47,6 +48,9 @@ class BookmarkExportTest extends TestCase
*/ */
protected static $history; protected static $history;
/** @var PluginManager */
protected static $pluginManager;
/** /**
* @var NetscapeBookmarkUtils * @var NetscapeBookmarkUtils
*/ */
@ -63,7 +67,14 @@ public static function setUpBeforeClass(): void
static::$refDb = new \ReferenceLinkDB(); static::$refDb = new \ReferenceLinkDB();
static::$refDb->write(static::$testDatastore); static::$refDb->write(static::$testDatastore);
static::$history = new History('sandbox/history.php'); static::$history = new History('sandbox/history.php');
static::$bookmarkService = new BookmarkFileService(static::$conf, static::$history, $mutex, true); static::$pluginManager = new PluginManager(static::$conf);
static::$bookmarkService = new BookmarkFileService(
static::$conf,
static::$pluginManager,
static::$history,
$mutex,
true
);
$factory = new FormatterFactory(static::$conf, true); $factory = new FormatterFactory(static::$conf, true);
static::$formatter = $factory->getFormatter('raw'); static::$formatter = $factory->getFormatter('raw');
} }

View file

@ -10,6 +10,7 @@
use Shaarli\Bookmark\BookmarkFilter; use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
use Slim\Http\UploadedFile; use Slim\Http\UploadedFile;
@ -71,6 +72,9 @@ class BookmarkImportTest extends TestCase
*/ */
protected $netscapeBookmarkUtils; protected $netscapeBookmarkUtils;
/** @var PluginManager */
protected $pluginManager;
/** /**
* @var string Save the current timezone. * @var string Save the current timezone.
*/ */
@ -99,7 +103,14 @@ protected function setUp(): void
$this->conf->set('resource.page_cache', $this->pagecache); $this->conf->set('resource.page_cache', $this->pagecache);
$this->conf->set('resource.datastore', self::$testDatastore); $this->conf->set('resource.datastore', self::$testDatastore);
$this->history = new History(self::$historyFilePath); $this->history = new History(self::$historyFilePath);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->history, $mutex, true); $this->pluginManager = new PluginManager($this->conf);
$this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->pluginManager,
$this->history,
$mutex,
true
);
$this->netscapeBookmarkUtils = new NetscapeBookmarkUtils($this->bookmarkService, $this->conf, $this->history); $this->netscapeBookmarkUtils = new NetscapeBookmarkUtils($this->bookmarkService, $this->conf, $this->history);
} }

View file

@ -193,4 +193,27 @@ public function testFormatCssRuleInvalid()
$result = default_colors_format_css_rule($data, ''); $result = default_colors_format_css_rule($data, '');
$this->assertEmpty($result); $this->assertEmpty($result);
} }
/**
* Make sure that a new CSS file is generated when save_plugin_parameters hook is triggered.
*/
public function testHookSavePluginParameters(): void
{
$params = [
'other1' => true,
'DEFAULT_COLORS_BACKGROUND' => 'pink',
'other2' => ['yep'],
'DEFAULT_COLORS_DARK_MAIN' => '',
];
hook_default_colors_save_plugin_parameters($params);
$this->assertFileExists($file = 'sandbox/default_colors/default_colors.css');
$content = file_get_contents($file);
$expected = ':root {
--background-color: pink;
}
';
$this->assertEquals($expected, $content);
}
} }

View file

@ -1,5 +1,7 @@
<?php <?php
use Shaarli\Bookmark\Bookmark;
/** /**
* Hook for test. * Hook for test.
* *
@ -27,3 +29,24 @@ function hook_test_error()
{ {
new Unknown(); new Unknown();
} }
function test_register_routes(): array
{
return [
[
'method' => 'GET',
'route' => '/test',
'callable' => 'getFunction',
],
[
'method' => 'POST',
'route' => '/custom',
'callable' => 'postFunction',
],
];
}
function hook_test_filter_search_entry(Bookmark $bookmark, array $context): bool
{
return $context['_result'];
}

View file

@ -0,0 +1,12 @@
<?php
function test_route_invalid_register_routes(): array
{
return [
[
'method' => 'GET',
'route' => 'not a route',
'callable' => 'getFunction',
],
];
}

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkServiceInterface; use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager; use Shaarli\Config\ConfigManager;
use Shaarli\History; use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase; use Shaarli\TestCase;
@ -51,7 +52,13 @@ protected function setUp(): void
copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php'); copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
$this->conf = new ConfigManager(self::$configFile); $this->conf = new ConfigManager(self::$configFile);
$this->bookmarkService = new BookmarkFileService($this->conf, $this->createMock(History::class), $mutex, true); $this->bookmarkService = new BookmarkFileService(
$this->conf,
$this->createMock(PluginManager::class),
$this->createMock(History::class),
$mutex,
true
);
$this->updater = new Updater([], $this->bookmarkService, $this->conf, true); $this->updater = new Updater([], $this->bookmarkService, $this->conf, true);
} }

View file

@ -20,6 +20,7 @@
</div> </div>
{loop="$links"} {loop="$links"}
{$batchId=$key}
{include="editlink"} {include="editlink"}
{/loop} {/loop}

View file

@ -1,3 +1,4 @@
{$batchId=isset($batchId) ? $batchId : ''}
{if="empty($batch_mode)"} {if="empty($batch_mode)"}
<!DOCTYPE html> <!DOCTYPE html>
<html{if="$language !== 'auto'"} lang="{$language}"{/if}> <html{if="$language !== 'auto'"} lang="{$language}"{/if}>
@ -10,7 +11,7 @@
{ignore}Lil hack: when included in a loop in batch mode, `$value` is assigned by RainTPL with template vars.{/ignore} {ignore}Lil hack: when included in a loop in batch mode, `$value` is assigned by RainTPL with template vars.{/ignore}
{function="extract($value) ? '' : ''"} {function="extract($value) ? '' : ''"}
{/if} {/if}
<div id="editlinkform" class="edit-link-container" class="pure-g"> <div id="editlinkform{$batchId}" class="edit-link-container" class="pure-g">
<div class="pure-u-lg-1-5 pure-u-1-24"></div> <div class="pure-u-lg-1-5 pure-u-1-24"></div>
<form method="post" <form method="post"
name="linkform" name="linkform"
@ -27,16 +28,16 @@ <h2 class="window-title">
{/if} {/if}
{if="!$link_is_new"}<div class="created-date">{'Created:'|t} {$link.created|format_date}</div>{/if} {if="!$link_is_new"}<div class="created-date">{'Created:'|t} {$link.created|format_date}</div>{/if}
<div> <div>
<label for="lf_url">{'URL'|t}</label> <label for="lf_url{$batchId}">{'URL'|t}</label>
</div> </div>
<div> <div>
<input type="text" name="lf_url" id="lf_url" value="{$link.url}" class="lf_input"> <input type="text" name="lf_url" id="lf_url{$batchId}" value="{$link.url}" class="lf_input">
</div> </div>
<div> <div>
<label for="lf_title">{'Title'|t}</label> <label for="lf_title{$batchId}">{'Title'|t}</label>
</div> </div>
<div class="{$asyncLoadClass}"> <div class="{$asyncLoadClass}">
<input type="text" name="lf_title" id="lf_title" value="{$link.title}" <input type="text" name="lf_title" id="lf_title{$batchId}" value="{$link.title}"
class="lf_input {if="!$async_metadata"}autofocus{/if}" class="lf_input {if="!$async_metadata"}autofocus{/if}"
> >
<div class="icon-container"> <div class="icon-container">
@ -44,19 +45,19 @@ <h2 class="window-title">
</div> </div>
</div> </div>
<div> <div>
<label for="lf_description">{'Description'|t}</label> <label for="lf_description{$batchId}">{'Description'|t}</label>
</div> </div>
<div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}"> <div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}">
<textarea name="lf_description" id="lf_description" class="autofocus">{$link.description}</textarea> <textarea name="lf_description" id="lf_description{$batchId}" class="autofocus">{$link.description}</textarea>
<div class="icon-container"> <div class="icon-container">
<i class="loader"></i> <i class="loader"></i>
</div> </div>
</div> </div>
<div> <div>
<label for="lf_tags">{'Tags'|t}</label> <label for="lf_tags{$batchId}">{'Tags'|t}</label>
</div> </div>
<div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}"> <div class="{if="$retrieve_description"}{$asyncLoadClass}{/if}">
<input type="text" name="lf_tags" id="lf_tags" value="{$link.tags}" class="lf_input autofocus" <input type="text" name="lf_tags" id="lf_tags{$batchId}" value="{$link.tags}" class="lf_input autofocus"
data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off" > data-list="{loop="$tags"}{$key}, {/loop}" data-multiple data-autofirst autocomplete="off" >
<div class="icon-container"> <div class="icon-container">
<i class="loader"></i> <i class="loader"></i>
@ -64,11 +65,11 @@ <h2 class="window-title">
</div> </div>
<div> <div>
<input type="checkbox" name="lf_private" id="lf_private" <input type="checkbox" name="lf_private" id="lf_private{$batchId}"
{if="$link.private === true"} {if="$link.private === true"}
checked="checked" checked="checked"
{/if}> {/if}>
&nbsp;<label for="lf_private">{'Private'|t}</label> &nbsp;<label for="lf_private{$batchId}">{'Private'|t}</label>
</div> </div>
{if="$formatter==='markdown'"} {if="$formatter==='markdown'"}

View file

@ -19,7 +19,7 @@
{/if} {/if}
<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" <link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
title="Shaarli search - {$shaarlititle}" /> title="Shaarli search - {$shaarlititle}" />
{if="! empty($links) && count($links) === 1"} {if="$template === 'linklist' && ! empty($links) && count($links) === 1"}
{$link=reset($links)} {$link=reset($links)}
<meta property="og:title" content="{$link.title}" /> <meta property="og:title" content="{$link.title}" />
<meta property="og:type" content="article" /> <meta property="og:type" content="article" />

View file

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html{if="$language !== 'auto'"} lang="{$language}"{/if}>
<head>
{include="includes"}
</head>
<body>
{include="page.header"}
{$content}
{include="page.footer"}
</body>
</html>

View file

@ -56,11 +56,11 @@ <h3 class="window-subtitle">{'General'|t}</h3>
{include="server.requirements"} {include="server.requirements"}
<h3 class="window-subtitle">Version</h3> <h3 class="window-subtitle">{'Version'|t}</h3>
<div class="pure-g server-row"> <div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label"> <div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>Current version</p> <p>{'Current version'|t}</p>
</div> </div>
<div class="pure-u-lg-1-2 pure-u-1"> <div class="pure-u-lg-1-2 pure-u-1">
<p>{$current_version}</p> <p>{$current_version}</p>
@ -69,7 +69,7 @@ <h3 class="window-subtitle">Version</h3>
<div class="pure-g server-row"> <div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label"> <div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>Latest release</p> <p>{'Latest release'|t}</p>
</div> </div>
<div class="pure-u-lg-1-2 pure-u-1"> <div class="pure-u-lg-1-2 pure-u-1">
<p> <p>
@ -80,11 +80,11 @@ <h3 class="window-subtitle">Version</h3>
</div> </div>
</div> </div>
<h3 class="window-subtitle">Thumbnails</h3> <h3 class="window-subtitle">{'Thumbnails'|t}</h3>
<div class="pure-g server-row"> <div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label"> <div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>Thumbnails status</p> <p>{'Thumbnails status'|t}</p>
</div> </div>
<div class="pure-u-lg-1-2 pure-u-1"> <div class="pure-u-lg-1-2 pure-u-1">
<p> <p>
@ -107,17 +107,17 @@ <h3 class="window-subtitle">Thumbnails</h3>
</div> </div>
{/if} {/if}
<h3 class="window-subtitle">Cache</h3> <h3 class="window-subtitle">{'Cache'|t}</h3>
<div class="center tools-item"> <div class="center tools-item">
<a href="{$base_path}/admin/clear-cache?type=main"> <a href="{$base_path}/admin/clear-cache?type=main">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear main cache</span> <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Clear main cache'|t}</span>
</a> </a>
</div> </div>
<div class="center tools-item"> <div class="center tools-item">
<a href="{$base_path}/admin/clear-cache?type=thumbnails"> <a href="{$base_path}/admin/clear-cache?type=thumbnails">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">Clear thumbnails cache</span> <span class="pure-button pure-u-lg-2-3 pure-u-3-4">{'Clear thumbnails cache'|t}</span>
</a> </a>
</div> </div>
</div> </div>

View file

@ -16,7 +16,7 @@
{if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />{/if} {if="is_file('data/user.css')"}<link type="text/css" rel="stylesheet" href="{$base_path}/data/user.css#" />{/if}
<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" <link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
title="Shaarli search - {$shaarlititle|htmlspecialchars}" /> title="Shaarli search - {$shaarlititle|htmlspecialchars}" />
{if="! empty($links) && count($links) === 1"} {if="$template === 'linklist' && ! empty($links) && count($links) === 1"}
{$link=reset($links)} {$link=reset($links)}
<meta property="og:title" content="{$link.title}" /> <meta property="og:title" content="{$link.title}" />
<meta property="og:type" content="article" /> <meta property="og:type" content="article" />

View file

@ -1369,10 +1369,10 @@ bluebird@^3.5.5:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
version "4.11.9" version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
bn.js@^5.1.1: bn.js@^5.1.1:
version "5.1.3" version "5.1.3"
@ -1410,7 +1410,7 @@ braces@^3.0.1, braces@~3.0.2:
dependencies: dependencies:
fill-range "^7.0.1" fill-range "^7.0.1"
brorand@^1.0.1: brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
@ -2130,17 +2130,17 @@ electron-to-chromium@^1.3.570:
integrity sha512-Y6OCoVQgFQBP5py6A/06+yWxUZHDlNr/gNDGatjH8AZqXl8X0tE4LfjLJsXGz/JmWJz8a6K7bR1k+QzZ+k//fg== integrity sha512-Y6OCoVQgFQBP5py6A/06+yWxUZHDlNr/gNDGatjH8AZqXl8X0tE4LfjLJsXGz/JmWJz8a6K7bR1k+QzZ+k//fg==
elliptic@^6.5.3: elliptic@^6.5.3:
version "6.5.3" version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
dependencies: dependencies:
bn.js "^4.4.0" bn.js "^4.11.9"
brorand "^1.0.1" brorand "^1.1.0"
hash.js "^1.0.0" hash.js "^1.0.0"
hmac-drbg "^1.0.0" hmac-drbg "^1.0.1"
inherits "^2.0.1" inherits "^2.0.4"
minimalistic-assert "^1.0.0" minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.0" minimalistic-crypto-utils "^1.0.1"
emoji-regex@^7.0.1: emoji-regex@^7.0.1:
version "7.0.3" version "7.0.3"
@ -2917,7 +2917,7 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hmac-drbg@^1.0.0: hmac-drbg@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
@ -3052,9 +3052,9 @@ inherits@2.0.3:
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ini@^1.3.4, ini@^1.3.5: ini@^1.3.4, ini@^1.3.5:
version "1.3.5" version "1.3.7"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
interpret@^1.4.0: interpret@^1.4.0:
version "1.4.0" version "1.4.0"
@ -3714,7 +3714,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: minimalistic-crypto-utils@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=