Merge pull request #3 from shaarli/master

Merge upstream
This commit is contained in:
yude 2021-04-04 11:25:48 +09:00 committed by GitHub
commit 0a47426f88
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 2632 additions and 1021 deletions

View file

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

View file

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

View file

@ -36,13 +36,6 @@ class Links extends ApiController
public function getLinks($request, $response)
{
$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.
$offset = $request->getParam('offset');
@ -50,9 +43,6 @@ public function getLinks($request, $response)
throw new ApiBadParametersException('Invalid offset');
}
$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 = $request->getParam('limit');
@ -61,23 +51,33 @@ public function getLinks($request, $response)
} elseif (ctype_digit($limit)) {
$limit = intval($limit);
} elseif ($limit === 'all') {
$limit = count($bookmarks);
$limit = null;
} else {
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.
$indexUrl = index_url($this->ci['environment']);
$out = [];
$index = 0;
foreach ($bookmarks as $bookmark) {
if (count($out) >= $limit) {
break;
}
if ($index++ >= $offset) {
$out[] = ApiUtils::formatLink($bookmark, $indexUrl);
}
foreach ($searchResult->getBookmarks() as $bookmark) {
$out[] = ApiUtils::formatLink($bookmark, $indexUrl);
}
return $response->withJson($out, 200, $this->jsonStyle);

View file

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

View file

@ -15,6 +15,7 @@
use Shaarli\History;
use Shaarli\Legacy\LegacyLinkDB;
use Shaarli\Legacy\LegacyUpdater;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\PageCacheManager;
use Shaarli\Updater\UpdaterUtils;
@ -40,6 +41,9 @@ class BookmarkFileService implements BookmarkServiceInterface
/** @var ConfigManager instance */
protected $conf;
/** @var PluginManager */
protected $pluginManager;
/** @var History instance */
protected $history;
@ -55,8 +59,13 @@ class BookmarkFileService implements BookmarkServiceInterface
/**
* @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->history = $history;
$this->mutex = $mutex;
@ -65,7 +74,7 @@ public function __construct(ConfigManager $conf, History $history, Mutex $mutex,
$this->isLoggedIn = $isLoggedIn;
if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
$this->bookmarks = [];
$this->bookmarks = new BookmarkArray();
} else {
try {
$this->bookmarks = $this->bookmarksIO->read();
@ -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,
bool $caseSensitive = false,
bool $untaggedOnly = false,
bool $ignoreSticky = false
) {
bool $ignoreSticky = false,
array $pagination = []
): SearchResult {
if ($visibility === null) {
$visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
}
@ -143,13 +154,20 @@ public function search(
$this->bookmarks->reorder('DESC', true);
}
return $this->bookmarkFilter->filter(
$bookmarks = $this->bookmarkFilter->filter(
BookmarkFilter::$FILTER_TAG | BookmarkFilter::$FILTER_TEXT,
[$searchTags, $searchTerm],
$caseSensitive,
$visibility,
$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
{
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
{
$bookmarks = $this->search(['searchtags' => $filteringTags], $visibility);
$searchResult = $this->search(['searchtags' => $filteringTags], $visibility);
$tags = [];
$caseMapping = [];
foreach ($bookmarks as $bookmark) {
foreach ($searchResult->getBookmarks() as $bookmark) {
foreach ($bookmark->getTags() as $tag) {
if (
empty($tag)
@ -357,7 +375,7 @@ public function findByDate(
$previous = 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()) {
$next = $bookmark->getCreated();
} elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
@ -378,7 +396,7 @@ public function findByDate(
*/
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;
}

View file

@ -4,9 +4,9 @@
namespace Shaarli\Bookmark;
use Exception;
use Shaarli\Bookmark\Exception\BookmarkNotFoundException;
use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager;
/**
* Class LinkFilter.
@ -30,11 +30,6 @@ class BookmarkFilter
*/
public static $FILTER_TAG = 'tags';
/**
* @var string filter by day.
*/
public static $FILTER_DAY = 'FILTER_DAY';
/**
* @var string filter by day.
*/
@ -62,13 +57,17 @@ class BookmarkFilter
/** @var ConfigManager */
protected $conf;
/** @var PluginManager */
protected $pluginManager;
/**
* @param Bookmark[] $bookmarks initialization.
*/
public function __construct($bookmarks, ConfigManager $conf)
public function __construct($bookmarks, ConfigManager $conf, PluginManager $pluginManager)
{
$this->bookmarks = $bookmarks;
$this->conf = $conf;
$this->pluginManager = $pluginManager;
}
/**
@ -112,12 +111,12 @@ public function filter(
$filtered = $this->bookmarks;
}
if (!empty($request[0])) {
$filtered = (new BookmarkFilter($filtered, $this->conf))
$filtered = (new BookmarkFilter($filtered, $this->conf, $this->pluginManager))
->filterTags($request[0], $casesensitive, $visibility)
;
}
if (!empty($request[1])) {
$filtered = (new BookmarkFilter($filtered, $this->conf))
$filtered = (new BookmarkFilter($filtered, $this->conf, $this->pluginManager))
->filterFulltext($request[1], $visibility)
;
}
@ -130,8 +129,6 @@ public function filter(
} else {
return $this->filterTags($request, $casesensitive, $visibility);
}
case self::$FILTER_DAY:
return $this->filterDay($request, $visibility);
default:
return $this->noFilter($visibility);
}
@ -146,13 +143,20 @@ public function filter(
*/
private function noFilter(string $visibility = 'all')
{
if ($visibility === 'all') {
return $this->bookmarks;
}
$out = [];
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;
} elseif (!$value->isPrivate() && $visibility === 'public') {
$out[$key] = $value;
@ -233,18 +237,34 @@ private function filterFulltext(string $searchterms, string $visibility = 'all')
}
// 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.
if ($visibility !== 'all') {
if (!$link->isPrivate() && $visibility === 'private') {
if (!$bookmark->isPrivate() && $visibility === 'private') {
continue;
} elseif ($link->isPrivate() && $visibility === 'public') {
} elseif ($bookmark->isPrivate() && $visibility === 'public') {
continue;
}
}
$lengths = [];
$content = $this->buildFullTextSearchableLink($link, $lengths);
$content = $this->buildFullTextSearchableLink($bookmark, $lengths);
// Be optimistic
$found = true;
@ -270,68 +290,18 @@ private function filterFulltext(string $searchterms, string $visibility = 'all')
}
if ($found !== false) {
$link->addAdditionalContentEntry(
$bookmark->addAdditionalContentEntry(
'search_highlight',
$this->postProcessFoundPositions($lengths, $foundPositions)
);
$filtered[$id] = $link;
$filtered[$id] = $bookmark;
}
}
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
*
@ -381,25 +351,39 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
$filtered = [];
// 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
// ignore non private bookmarks when 'privateonly' is on.
if ($visibility !== 'all') {
if (!$link->isPrivate() && $visibility === 'private') {
if (!$bookmark->isPrivate() && $visibility === 'private') {
continue;
} elseif ($link->isPrivate() && $visibility === 'public') {
} elseif ($bookmark->isPrivate() && $visibility === 'public') {
continue;
}
}
// build search string, start with tags of current link
$search = $link->getTagsString($tagsSeparator);
if (strlen(trim($link->getDescription())) && strpos($link->getDescription(), '#') !== false) {
$search = $bookmark->getTagsString($tagsSeparator);
if (strlen(trim($bookmark->getDescription())) && strpos($bookmark->getDescription(), '#') !== false) {
// description given and at least one possible tag found
$descTags = [];
// find all tags in the form of #tag in the description
preg_match_all(
'/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
$link->getDescription(),
$bookmark->getDescription(),
$descTags
);
if (count($descTags[1])) {
@ -412,8 +396,9 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
// this entry does _not_ match our regex
continue;
}
$filtered[$key] = $link;
$filtered[$key] = $bookmark;
}
return $filtered;
}
@ -427,55 +412,30 @@ public function filterTags($tags, bool $casesensitive = false, string $visibilit
public function filterUntagged(string $visibility)
{
$filtered = [];
foreach ($this->bookmarks as $key => $link) {
foreach ($this->bookmarks as $key => $bookmark) {
if (
!$this->pluginManager->filterSearchEntry(
$bookmark,
['source' => 'untagged', 'visibility' => $visibility]
)
) {
continue;
}
if ($visibility !== 'all') {
if (!$link->isPrivate() && $visibility === 'private') {
if (!$bookmark->isPrivate() && $visibility === 'private') {
continue;
} elseif ($link->isPrivate() && $visibility === 'public') {
} elseif ($bookmark->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 = [];
foreach ($this->bookmarks as $key => $bookmark) {
if ($visibility === static::$PUBLIC && $bookmark->isPrivate()) {
continue;
}
if ($bookmark->getCreated()->format('Ymd') == $day) {
if (empty($bookmark->getTags())) {
$filtered[$key] = $bookmark;
}
}
// sort by date ASC
return array_reverse($filtered, true);
return $filtered;
}
/**
@ -497,6 +457,56 @@ public static function tagsStrToArray(string $tags, bool $casesensitive): array
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,
* by associated all search results to their associated bookmark field,

View file

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

View file

@ -1,6 +1,7 @@
<?php
use Shaarli\Bookmark\Bookmark;
use Shaarli\Formatter\BookmarkDefaultFormatter;
/**
* Extract title from an HTML document.
@ -98,7 +99,18 @@ function html_extract_tag($tag, $html)
function text2clickable($text)
{
$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);
}
/**
@ -111,6 +123,9 @@ function text2clickable($text)
*/
function hashtag_autolink($description, $indexUrl = '')
{
$tokens = '(?:' . BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_OPEN . ')' .
'(?:' . BookmarkDefaultFormatter::SEARCH_HIGHLIGHT_CLOSE . ')'
;
/*
* To support unicode: http://stackoverflow.com/a/35498078/1484919
* \p{Pc} - to match underscore
@ -118,9 +133,20 @@ function hashtag_autolink($description, $indexUrl = '')
* \p{L} - letter from any language
* \p{Mn} - any non marking space (accents, umlauts, etc)
*/
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
$replacement = '$1<a href="' . $indexUrl . './add-tag/$2" title="Hashtag $2">#$2</a>';
return preg_replace($regex, $replacement, $description);
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}' . $tokens . ']+)/mui';
$format = function (array $match) use ($indexUrl): string {
$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

@ -95,6 +95,7 @@ public function build(): ShaarliContainer
$container['bookmarkService'] = function (ShaarliContainer $container): BookmarkServiceInterface {
return new BookmarkFileService(
$container->conf,
$container->pluginManager,
$container->history,
new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
$container->loginManager->isLoggedIn()

View file

@ -102,22 +102,16 @@ public function buildData(string $feedType, ?array $userInput)
$userInput['searchtags'] = false;
}
$limit = $this->getLimit($userInput);
// Optionally filter the results:
$linksToDisplay = $this->linkDB->search($userInput ?? [], null, false, false, true);
$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;
}
$searchResult = $this->linkDB->search($userInput ?? [], null, false, false, true, ['limit' => $limit]);
$pageaddr = escape(index_url($this->serverInfo));
$this->formatter->addContextData('index_url', $pageaddr);
$linkDisplayed = [];
for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
$linkDisplayed[$keys[$i]] = $this->buildItem($feedType, $linksToDisplay[$keys[$i]], $pageaddr);
$links = [];
foreach ($searchResult->getBookmarks() as $key => $bookmark) {
$links[$key] = $this->buildItem($feedType, $bookmark, $pageaddr);
}
$data['language'] = $this->getTypeLanguage($feedType);
@ -128,7 +122,7 @@ public function buildData(string $feedType, ?array $userInput)
$data['self_link'] = $pageaddr . $requestUri;
$data['index_url'] = $pageaddr;
$data['usepermalinks'] = $this->usePermalinks === true;
$data['links'] = $linkDisplayed;
$data['links'] = $links;
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' is set to 'all', display all filtered bookmarks (max parameter).
*
* @param int $max maximum number of bookmarks to display.
* @param array $userInput $_GET.
*
* @return int number of bookmarks to display.
*/
protected function getNbLinks($max, ?array $userInput)
protected function getLimit(?array $userInput)
{
if (empty($userInput['nb'])) {
return self::$DEFAULT_NB_LINKS;
}
if ($userInput['nb'] == 'all') {
return $max;
return null;
}
$intNb = intval($userInput['nb']);

View file

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

View file

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

View file

@ -3,6 +3,7 @@
namespace Shaarli\Formatter;
use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\Parsedown\ShaarliParsedown;
/**
* Class BookmarkMarkdownFormatter
@ -42,7 +43,7 @@ public function __construct(ConfigManager $conf, bool $isLoggedIn)
{
parent::__construct($conf, $isLoggedIn);
$this->parsedown = new \Parsedown();
$this->parsedown = new ShaarliParsedown();
$this->escape = $conf->get('security.markdown_escape', true);
$this->allowedProtocols = $conf->get('security.allowed_protocols', []);
}
@ -128,6 +129,9 @@ function ($match) use ($allowedProtocols, $indexUrl) {
protected function formatHashTags($description)
{
$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
@ -136,8 +140,15 @@ protected function formatHashTags($description)
* \p{L} - letter from any language
* \p{Mn} - any non marking space (accents, umlauts, etc)
*/
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}]+)/mui';
$replacement = '$1[#$2](' . $indexUrl . './add-tag/$2)';
$regex = '/(^|\s)#([\p{Pc}\p{N}\p{L}\p{Mn}' . $tokens . ']+)/mui';
$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);
$descriptionOut = '';
@ -156,7 +167,7 @@ protected function formatHashTags($description)
}
if (!$codeBlockOn && !$codeLineOn) {
$descriptionLine = preg_replace($regex, $replacement, $descriptionLine);
$descriptionLine = preg_replace_callback($regex, $replacement, $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
$count = 0;
$bookmarks = $this->container->bookmarkService->search(['searchtags' => $fromTag], BookmarkFilter::$ALL, true);
foreach ($bookmarks as $bookmark) {
$searchResult = $this->container->bookmarkService->search(
['searchtags' => $fromTag],
BookmarkFilter::$ALL,
true
);
foreach ($searchResult->getBookmarks() as $bookmark) {
if (false === $isDelete) {
$bookmark->renameTag($fromTag, $toTag);
} else {
@ -68,11 +71,11 @@ public function save(Request $request, Response $response): Response
$this->container->bookmarkService->set($bookmark, false);
$this->container->history->updateLink($bookmark);
$count++;
}
$this->container->bookmarkService->save();
$count = $searchResult->getResultCount();
if (true === $isDelete) {
$alert = sprintf(
t('The tag was removed from %d bookmark.', 'The tag was removed from %d bookmarks.', $count),

View file

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

View file

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

View file

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

View file

@ -100,7 +100,7 @@ public function rss(Request $request, Response $response): Response
$days = [];
$format = DailyPageHelper::getFormatByType($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);
// Stop iterating after DAILY_RSS_NB_DAYS entries

View file

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

View file

@ -56,6 +56,10 @@ protected function assignAllView(array $data): self
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('privateLinkcount', $this->container->bookmarkService->count(BookmarkFilter::$PRIVATE));

View file

@ -37,7 +37,7 @@ public static function getLatestGitVersionCode($url, $timeout = 2)
{
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);
return false;
}

View file

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

View file

@ -2,6 +2,7 @@
namespace Shaarli\Plugin;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\Exception\PluginFileNotFoundException;
use Shaarli\Plugin\Exception\PluginInvalidRouteException;
@ -45,6 +46,9 @@ class PluginManager
*/
protected $errors;
/** @var callable[]|null Preloaded list of hook function for filterSearchEntry() */
protected $filterSearchEntryHooks = null;
/**
* Plugins subdirectory.
*
@ -273,6 +277,14 @@ 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.
*
@ -283,6 +295,50 @@ public function getErrors()
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).

View file

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

View file

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

12
composer.lock generated
View file

@ -8,16 +8,16 @@
"packages": [
{
"name": "arthurhoaro/web-thumbnailer",
"version": "v2.0.3",
"version": "v2.0.4",
"source": {
"type": "git",
"url": "https://github.com/ArthurHoaro/web-thumbnailer.git",
"reference": "39bfd4f3136d9e6096496b9720e877326cfe4775"
"reference": "7780ddc0f44fccdce6cddb86d1db0354810290d0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/39bfd4f3136d9e6096496b9720e877326cfe4775",
"reference": "39bfd4f3136d9e6096496b9720e877326cfe4775",
"url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/7780ddc0f44fccdce6cddb86d1db0354810290d0",
"reference": "7780ddc0f44fccdce6cddb86d1db0354810290d0",
"shasum": ""
},
"require": {
@ -53,9 +53,9 @@
"description": "PHP library which will retrieve a thumbnail for any given URL",
"support": {
"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",

View file

@ -194,7 +194,7 @@ $ docker logs -f <container-name-or-first-letters-of-id>
# delete unused images to free up disk space
$ docker system prune --images
# 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
$ docker system prune
```

View file

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

View file

@ -27,7 +27,6 @@ You should have the following tree view:
| |---| demo_plugin.php
```
### 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.
@ -209,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. |
| [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. |
| [filter_search_entry](#filter_search_entry) | Add custom filters to Shaarli search engine |
#### render_header
@ -565,6 +565,23 @@ the array will contain an entry with `MYPLUGIN_PARAMETER` as a key.
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
### Plugin administration

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@
* and check user status with _LOGGEDIN_.
*/
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\PluginManager;
use Shaarli\Render\TemplatePage;
@ -263,6 +264,17 @@ function hook_demo_plugin_render_linklist($data)
}
$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)
foreach ($data['links'] as &$value) {
$value['link_plugin'][] = ' DEMO \o/';
@ -486,6 +498,27 @@ function hook_demo_plugin_save_plugin_parameters($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.
*/

View file

@ -2,6 +2,7 @@
namespace Shaarli\Plugin;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
/**
@ -159,4 +160,19 @@ public function testRegisteredRoutesInvalid(): void
$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\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container;
use Slim\Http\Environment;
use Slim\Http\Request;
@ -56,6 +57,7 @@ protected function setUp(): void
$this->container = new Container();
$this->container['conf'] = $this->conf;
$this->container['history'] = $history;
$this->container['pluginManager'] = new PluginManager($this->conf);
}
/**

View file

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

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container;
use Slim\Http\Environment;
use Slim\Http\Request;
@ -57,6 +58,9 @@ class DeleteLinkTest extends \Shaarli\TestCase
/** @var NoMutex */
protected $mutex;
/** @var PluginManager */
protected $pluginManager;
/**
* 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->write(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['conf'] = $this->conf;
@ -105,7 +116,13 @@ public function testDeleteLinkValid()
$this->assertEquals(204, $response->getStatusCode());
$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));
$historyEntry = $this->history->getHistory()[0];

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container;
use Slim\Http\Environment;
use Slim\Http\Request;
@ -67,7 +68,14 @@ protected function setUp(): void
$this->container = new Container();
$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->controller = new Links($this->container);

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container;
use Slim\Http\Environment;
use Slim\Http\Request;
@ -67,7 +68,14 @@ protected function setUp(): void
$this->container = new Container();
$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->controller = new Links($this->container);

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase;
use Slim\Container;
use Slim\Http\Environment;
@ -81,8 +82,14 @@ protected function setUp(): void
$refHistory = new \ReferenceHistory();
$refHistory->write(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['conf'] = $this->conf;
$this->container['db'] = $this->bookmarkService;

View file

@ -8,6 +8,7 @@
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container;
use Slim\Http\Environment;
use Slim\Http\Request;
@ -73,8 +74,14 @@ protected function setUp(): void
$refHistory = new \ReferenceHistory();
$refHistory->write(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['conf'] = $this->conf;
$this->container['db'] = $this->bookmarkService;

View file

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

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\LinkDB;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Slim\Container;
use Slim\Http\Environment;
use Slim\Http\Request;
@ -46,6 +47,9 @@ class GetTagNameTest extends \Shaarli\TestCase
*/
protected $controller;
/** @var PluginManager */
protected $pluginManager;
/**
* Number of JSON fields per link.
*/
@ -65,7 +69,14 @@ protected function setUp(): void
$this->container = new Container();
$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->controller = new Tags($this->container);

View file

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

View file

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

View file

@ -14,6 +14,7 @@
use Shaarli\Config\ConfigManager;
use Shaarli\Formatter\BookmarkMarkdownFormatter;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase;
/**
@ -56,6 +57,9 @@ class BookmarkFileServiceTest extends TestCase
/** @var NoMutex */
protected $mutex;
/** @var PluginManager */
protected $pluginManager;
/**
* Instantiates public and private LinkDBs with test data
*
@ -93,8 +97,21 @@ protected function setUp(): void
$this->refDB = new \ReferenceLinkDB();
$this->refDB->write(self::$testDatastore);
$this->history = new History('sandbox/history.php');
$this->publicLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, false);
$this->privateLinkDB = new BookmarkFileService($this->conf, $this->history, $this->mutex, true);
$this->pluginManager = new PluginManager($this->conf);
$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->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->assertEquals($this->refDB->countLinks(), $db->count());
}
@ -180,7 +203,13 @@ public function testAddFull()
$this->assertEquals($updated, $bookmark->getUpdated());
// 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);
$this->assertEquals(43, $bookmark->getId());
@ -218,7 +247,13 @@ public function testAddMinimal()
$this->assertNull($bookmark->getUpdated());
// 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);
$this->assertEquals(43, $bookmark->getId());
@ -248,7 +283,13 @@ public function testAddMinimalNoWrite()
$this->assertEquals(43, $bookmark->getId());
// 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);
}
@ -309,7 +350,13 @@ public function testSetFull()
$this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
// 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);
$this->assertEquals(42, $bookmark->getId());
@ -350,7 +397,13 @@ public function testSetMinimal()
$this->assertTrue(new \DateTime('5 seconds ago') < $bookmark->getUpdated());
// 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);
$this->assertEquals(42, $bookmark->getId());
@ -383,7 +436,13 @@ public function testSetMinimalNoWrite()
$this->assertEquals($title, $bookmark->getTitle());
// 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);
$this->assertEquals(42, $bookmark->getId());
@ -436,7 +495,13 @@ public function testAddOrSetNew()
$this->assertEquals(43, $bookmark->getId());
// 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);
$this->assertEquals(43, $bookmark->getId());
@ -456,7 +521,13 @@ public function testAddOrSetExisting()
$this->assertEquals($title, $bookmark->getTitle());
// 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);
$this->assertEquals(42, $bookmark->getId());
@ -488,7 +559,13 @@ public function testAddOrSetMinimalNoWrite()
$this->assertEquals($title, $bookmark->getTitle());
// 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);
$this->assertEquals(42, $bookmark->getId());
@ -514,7 +591,13 @@ public function testRemoveExisting()
$this->assertInstanceOf(BookmarkNotFoundException::class, $exception);
// 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);
}
@ -607,7 +690,7 @@ public function testConstructDatastoreNotWriteable()
$conf = new ConfigManager('tests/utils/config/configJson');
$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);
$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);
// ensure the correct data has been written
@ -631,7 +714,7 @@ public function testCheckDBNewLoggedOut()
{
unlink(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->assertInstanceOf(BookmarkArray::class, $db->getBookmarks());
$this->assertCount(0, $db->getBookmarks());
@ -664,13 +747,13 @@ public function testReadPrivateDB()
*/
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();
$bookmark = new 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());
}
@ -680,7 +763,7 @@ public function testSave()
public function testCountHiddenPublic()
{
$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());
}
@ -807,7 +890,7 @@ public function testFilterString()
$request = ['searchtags' => $tags];
$this->assertEquals(
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];
$this->assertEquals(
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];
$this->assertEquals(
1,
count($this->privateLinkDB->search($request, 'all', true))
count($this->privateLinkDB->search($request, 'all', true)->getBookmarks())
);
$this->assertEquals(
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);
$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);
static::assertSame(6, $bookmark->getId());
@ -1152,7 +1241,13 @@ public function testGetLatestWithSticky(): void
public function testGetLatestEmptyDatastore(): void
{
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();

View file

@ -6,6 +6,7 @@
use ReferenceLinkDB;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase;
/**
@ -32,19 +33,24 @@ class BookmarkFilterTest extends TestCase
*/
protected static $bookmarkService;
/** @var PluginManager */
protected static $pluginManager;
/**
* Instantiate linkFilter with ReferenceLinkDB data.
*/
public static function setUpBeforeClass(): void
{
$mutex = new NoMutex();
$conf = new ConfigManager('tests/utils/config/configJson');
$conf->set('resource.datastore', self::$testDatastore);
static::$pluginManager = new PluginManager($conf);
self::$refDB = new \ReferenceLinkDB();
self::$refDB->write(self::$testDatastore);
$history = new History('sandbox/history.php');
self::$bookmarkService = new \FakeBookmarkService($conf, $history, $mutex, true);
self::$linkFilter = new BookmarkFilter(self::$bookmarkService->getBookmarks(), $conf);
self::$bookmarkService = new \FakeBookmarkService($conf, static::$pluginManager, $history, $mutex, true);
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
*/

View file

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

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

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

View file

@ -211,13 +211,17 @@ public function testFormatDescriptionWithSearchHighlight(): void
$this->formatter = new BookmarkDefaultFormatter($this->conf, false);
$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(
'search_highlight',
['description' => [
['start' => 0, 'end' => 10], // "This guide"
['start' => 45, 'end' => 50], // basic
['start' => 58, 'end' => 67], // standard.
['start' => 84, 'end' => 87], // fig
]]
);
@ -226,7 +230,10 @@ public function testFormatDescriptionWithSearchHighlight(): void
$this->assertSame(
'<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">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']
);
}

View file

@ -132,6 +132,49 @@ public function testFormatDescription()
$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
* It should prepend relative links.

View file

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

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@
namespace Shaarli\Front\Controller\Visitor;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Bookmark\SearchResult;
use Shaarli\Config\ConfigManager;
use Shaarli\Front\Exception\ThumbnailsDisabledException;
use Shaarli\TestCase;
@ -50,17 +51,17 @@ public function testValidControllerInvokeDefault(): void
$this->container->bookmarkService
->expects(static::once())
->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
static::assertNull($visibility);
// No query parameters
if (count($parameters) === 0) {
return [
return SearchResult::getSearchResult([
(new Bookmark())->setId(1)->setUrl('http://url.tld')->setThumbnail('thumb1'),
(new Bookmark())->setId(2)->setUrl('http://url2.tld'),
(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', $this->assignedValues['_PAGE_']);
static::assertSame('templateName', $this->assignedValues['template']);
static::assertSame(10, $this->assignedValues['linkcount']);
static::assertSame(5, $this->assignedValues['privateLinkcount']);
static::assertSame(['error'], $this->assignedValues['plugin_errors']);

View file

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

View file

@ -10,6 +10,7 @@
use Shaarli\Bookmark\BookmarkFilter;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase;
use Slim\Http\UploadedFile;
@ -71,6 +72,9 @@ class BookmarkImportTest extends TestCase
*/
protected $netscapeBookmarkUtils;
/** @var PluginManager */
protected $pluginManager;
/**
* @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.datastore', self::$testDatastore);
$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);
}

View file

@ -1,5 +1,7 @@
<?php
use Shaarli\Bookmark\Bookmark;
/**
* Hook for test.
*
@ -43,3 +45,8 @@ function test_register_routes(): array
],
];
}
function hook_test_filter_search_entry(Bookmark $bookmark, array $context): bool
{
return $context['_result'];
}

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
use Shaarli\History;
use Shaarli\Plugin\PluginManager;
use Shaarli\TestCase;
@ -51,7 +52,13 @@ protected function setUp(): void
copy('tests/utils/config/configJson.json.php', self::$configFile .'.json.php');
$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);
}

View file

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

View file

@ -1,3 +1,4 @@
{$batchId=isset($batchId) ? $batchId : ''}
{if="empty($batch_mode)"}
<!DOCTYPE html>
<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}
{function="extract($value) ? '' : ''"}
{/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>
<form method="post"
name="linkform"
@ -27,16 +28,16 @@ <h2 class="window-title">
{/if}
{if="!$link_is_new"}<div class="created-date">{'Created:'|t} {$link.created|format_date}</div>{/if}
<div>
<label for="lf_url">{'URL'|t}</label>
<label for="lf_url{$batchId}">{'URL'|t}</label>
</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>
<label for="lf_title">{'Title'|t}</label>
<label for="lf_title{$batchId}">{'Title'|t}</label>
</div>
<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}"
>
<div class="icon-container">
@ -44,19 +45,19 @@ <h2 class="window-title">
</div>
</div>
<div>
<label for="lf_description">{'Description'|t}</label>
<label for="lf_description{$batchId}">{'Description'|t}</label>
</div>
<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">
<i class="loader"></i>
</div>
</div>
<div>
<label for="lf_tags">{'Tags'|t}</label>
<label for="lf_tags{$batchId}">{'Tags'|t}</label>
</div>
<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" >
<div class="icon-container">
<i class="loader"></i>
@ -64,11 +65,11 @@ <h2 class="window-title">
</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"}
checked="checked"
{/if}>
&nbsp;<label for="lf_private">{'Private'|t}</label>
&nbsp;<label for="lf_private{$batchId}">{'Private'|t}</label>
</div>
{if="$formatter==='markdown'"}

View file

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

View file

@ -56,11 +56,11 @@ <h3 class="window-subtitle">{'General'|t}</h3>
{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-u-lg-1-2 pure-u-1 server-label">
<p>Current version</p>
<p>{'Current version'|t}</p>
</div>
<div class="pure-u-lg-1-2 pure-u-1">
<p>{$current_version}</p>
@ -69,7 +69,7 @@ <h3 class="window-subtitle">Version</h3>
<div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>Latest release</p>
<p>{'Latest release'|t}</p>
</div>
<div class="pure-u-lg-1-2 pure-u-1">
<p>
@ -80,11 +80,11 @@ <h3 class="window-subtitle">Version</h3>
</div>
</div>
<h3 class="window-subtitle">Thumbnails</h3>
<h3 class="window-subtitle">{'Thumbnails'|t}</h3>
<div class="pure-g server-row">
<div class="pure-u-lg-1-2 pure-u-1 server-label">
<p>Thumbnails status</p>
<p>{'Thumbnails status'|t}</p>
</div>
<div class="pure-u-lg-1-2 pure-u-1">
<p>
@ -107,17 +107,17 @@ <h3 class="window-subtitle">Thumbnails</h3>
</div>
{/if}
<h3 class="window-subtitle">Cache</h3>
<h3 class="window-subtitle">{'Cache'|t}</h3>
<div class="center tools-item">
<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>
</div>
<div class="center tools-item">
<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>
</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}
<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#"
title="Shaarli search - {$shaarlititle|htmlspecialchars}" />
{if="! empty($links) && count($links) === 1"}
{if="$template === 'linklist' && ! empty($links) && count($links) === 1"}
{$link=reset($links)}
<meta property="og:title" content="{$link.title}" />
<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"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0:
version "4.11.9"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
bn.js@^5.1.1:
version "5.1.3"
@ -1410,7 +1410,7 @@ braces@^3.0.1, braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
brorand@^1.0.1:
brorand@^1.0.1, brorand@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
@ -2130,17 +2130,17 @@ electron-to-chromium@^1.3.570:
integrity sha512-Y6OCoVQgFQBP5py6A/06+yWxUZHDlNr/gNDGatjH8AZqXl8X0tE4LfjLJsXGz/JmWJz8a6K7bR1k+QzZ+k//fg==
elliptic@^6.5.3:
version "6.5.3"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6"
integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
dependencies:
bn.js "^4.4.0"
brorand "^1.0.1"
bn.js "^4.11.9"
brorand "^1.1.0"
hash.js "^1.0.0"
hmac-drbg "^1.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
hmac-drbg "^1.0.1"
inherits "^2.0.4"
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
emoji-regex@^7.0.1:
version "7.0.3"
@ -2917,7 +2917,7 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hmac-drbg@^1.0.0:
hmac-drbg@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
@ -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"
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"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
@ -5980,9 +5980,9 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
y18n@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
version "4.0.1"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.1.tgz#8db2b83c31c5d75099bb890b23f3094891e247d4"
integrity sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==
yallist@^3.0.2:
version "3.1.1"