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

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

View file

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

View file

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

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

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

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);
@ -117,7 +117,11 @@ public function getLink($request, $response, $args)
public function postLink($request, $response)
{
$data = (array) ($request->getParsedBody() ?? []);
$bookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links'));
$bookmark = ApiUtils::buildBookmarkFromRequest(
$data,
$this->conf->get('privacy.default_private_links'),
$this->conf->get('general.tags_separator', ' ')
);
// duplicate by URL, return 409 Conflict
if (
! empty($bookmark->getUrl())
@ -158,7 +162,11 @@ public function putLink($request, $response, $args)
$index = index_url($this->ci['environment']);
$data = $request->getParsedBody();
$requestBookmark = ApiUtils::buildBookmarkFromRequest($data, $this->conf->get('privacy.default_private_links'));
$requestBookmark = ApiUtils::buildBookmarkFromRequest(
$data,
$this->conf->get('privacy.default_private_links'),
$this->conf->get('general.tags_separator', ' ')
);
// duplicate URL on a different link, return 409 Conflict
if (
! empty($requestBookmark->getUrl())

View file

@ -122,12 +122,12 @@ public function putTag($request, $response, $args)
throw new ApiBadParametersException('New tag name is required in the request body');
}
$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;
@ -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

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

View file

@ -44,16 +44,18 @@ public function findByUrl(string $url): ?Bookmark;
* @param bool $caseSensitive
* @param bool $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.
@ -68,11 +69,13 @@ function html_extract_tag($tag, $html)
$properties = implode('|', $propertiesKey);
// We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"'
$orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]';
// Support quotes in double quoted content, and the other way around
$content = 'content=(["\'])((?:(?!\1).)*)\1';
// Try to retrieve OpenGraph tag.
$ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#';
$ogRegex = '#<meta[^>]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*' . $content . '.*?>#';
// If the attributes are not in the order property => content (e.g. Github)
// New regex to keep this readable... more or less.
$ogRegexReverse = '#<meta[^>]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#';
$ogRegexReverse = '#<meta[^>]+' . $content . '[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#';
if (
preg_match($ogRegex, $html, $matches) > 0
@ -96,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);
}
/**
@ -109,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
@ -116,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

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

View file

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

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

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

View file

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

View file

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

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

@ -86,9 +86,11 @@ public function index(Request $request, Response $response): Response
public function rss(Request $request, Response $response): Response
{
$response = $response->withHeader('Content-Type', 'application/rss+xml; charset=utf-8');
$type = DailyPageHelper::extractRequestedType($request);
$cacheDuration = DailyPageHelper::getCacheDatePeriodByType($type);
$pageUrl = page_url($this->container->environment);
$cache = $this->container->pageCacheManager->getCachePage($pageUrl);
$cache = $this->container->pageCacheManager->getCachePage($pageUrl, $cacheDuration);
$cached = $cache->cachedVersion();
if (!empty($cached)) {
@ -96,10 +98,9 @@ public function rss(Request $request, Response $response): Response
}
$days = [];
$type = DailyPageHelper::extractRequestedType($request);
$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
@ -131,7 +132,7 @@ public function rss(Request $request, Response $response): Response
$dataPerDay[$day] = [
'date' => $endDateTime,
'date_rss' => $endDateTime->format(DateTime::RSS),
'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime),
'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime, false),
'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day,
'links' => [],
];

View file

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

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

@ -3,6 +3,8 @@
namespace Shaarli\Helper;
use Exception;
use malkusch\lock\exception\LockAcquireException;
use malkusch\lock\mutex\FlockMutex;
use Shaarli\Config\ConfigManager;
/**
@ -35,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;
}
@ -252,6 +254,20 @@ public static function checkResourcePermissions(ConfigManager $conf, bool $minim
return $errors;
}
public static function checkDatastoreMutex(): array
{
$mutex = new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2);
try {
$mutex->synchronized(function () {
return true;
});
} catch (LockAcquireException $e) {
$errors[] = t('Lock can not be acquired on the datastore. You might encounter concurrent access issues.');
}
return $errors ?? [];
}
/**
* Returns a salted hash representing the current Shaarli version.
*

View file

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

View file

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

View file

@ -64,7 +64,7 @@ public function filterAndFormat(
}
$bookmarkLinks = [];
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,8 +2,10 @@
namespace Shaarli\Plugin;
use Shaarli\Bookmark\Bookmark;
use Shaarli\Config\ConfigManager;
use Shaarli\Plugin\Exception\PluginFileNotFoundException;
use Shaarli\Plugin\Exception\PluginInvalidRouteException;
/**
* Class PluginManager
@ -26,6 +28,14 @@ class PluginManager
*/
private $loadedPlugins = [];
/** @var array List of registered routes. Contains keys:
* - `method`: HTTP method, GET/POST/PUT/PATCH/DELETE
* - `route` (path): without prefix, e.g. `/up/{variable}`
* It will be later prefixed by `/plugin/<plugin name>/`.
* - `callable` string, function name or FQN class's method, e.g. `demo_plugin_custom_controller`.
*/
protected $registeredRoutes = [];
/**
* @var ConfigManager Configuration Manager instance.
*/
@ -36,6 +46,9 @@ class PluginManager
*/
protected $errors;
/** @var callable[]|null Preloaded list of hook function for filterSearchEntry() */
protected $filterSearchEntryHooks = null;
/**
* Plugins subdirectory.
*
@ -86,6 +99,9 @@ public function load($authorizedPlugins)
$this->loadPlugin($dirs[$index], $plugin);
} catch (PluginFileNotFoundException $e) {
error_log($e->getMessage());
} catch (\Throwable $e) {
$error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
$this->errors = array_unique(array_merge($this->errors, [$error]));
}
}
}
@ -166,6 +182,22 @@ private function loadPlugin($dir, $pluginName)
}
}
$registerRouteFunction = $pluginName . '_register_routes';
$routes = null;
if (function_exists($registerRouteFunction)) {
$routes = call_user_func($registerRouteFunction);
}
if ($routes !== null) {
foreach ($routes as $route) {
if (static::validateRouteRegistration($route)) {
$this->registeredRoutes[$pluginName][] = $route;
} else {
throw new PluginInvalidRouteException($pluginName);
}
}
}
$this->loadedPlugins[] = $pluginName;
}
@ -237,6 +269,22 @@ public function getPluginsMeta()
return $metaData;
}
/**
* @return array List of registered custom routes by plugins.
*/
public function getRegisteredRoutes(): array
{
return $this->registeredRoutes;
}
/**
* @return array List of registered filter_search_entry hooks
*/
public function getFilterSearchEntryHooks(): ?array
{
return $this->filterSearchEntryHooks;
}
/**
* Return the list of encountered errors.
*
@ -246,4 +294,76 @@ 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).
*
* @param string[] $input
*
* @return bool
*/
protected static function validateRouteRegistration(array $input): bool
{
if (
!array_key_exists('method', $input)
|| !in_array(strtoupper($input['method']), ['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
) {
return false;
}
if (!array_key_exists('route', $input) || !preg_match('#^[a-z\d/\.\-_]+$#', $input['route'])) {
return false;
}
if (!array_key_exists('callable', $input)) {
return false;
}
return true;
}
}

View file

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

View file

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

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

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

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.
@ -139,6 +138,31 @@ Each file contain two keys:
> Note: In PHP, `parse_ini_file()` seems to want strings to be between by quotes `"` in the ini file.
### Register plugin's routes
Shaarli lets you register custom Slim routes for your plugin.
To register a route, the plugin must include a function called `function <plugin_name>_register_routes(): array`.
This method must return an array of routes, each entry must contain the following keys:
- `method`: HTTP method, `GET/POST/PUT/PATCH/DELETE`
- `route` (path): without prefix, e.g. `/up/{variable}`
It will be later prefixed by `/plugin/<plugin name>/`.
- `callable` string, function name or FQN class's method to execute, e.g. `demo_plugin_custom_controller`.
Callable functions or methods must have `Slim\Http\Request` and `Slim\Http\Response` parameters
and return a `Slim\Http\Response`. We recommend creating a dedicated class and extend either
`ShaarliVisitorController` or `ShaarliAdminController` to use helper functions they provide.
A dedicated plugin template is available for rendering content: `pluginscontent.html` using `content` placeholder.
> **Warning**: plugins are not able to use RainTPL template engine for their content due to technical restrictions.
> RainTPL does not allow to register multiple template folders, so all HTML rendering must be done within plugin
> custom controller.
Check out the `demo_plugin` for a live example: `GET <shaarli_url>/plugin/demo_plugin/custom`.
### Understanding relative paths
Because Shaarli is a self-hosted tool, an instance can either be installed at the root directory, or under a subfolder.
@ -184,6 +208,7 @@ If it's still not working, please [open an issue](https://github.com/shaarli/Sha
| [save_link](#save_link) | Allow to alter the link being saved in the datastore. |
| [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
@ -540,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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,8 @@
* Can be used by plugin developers to make their own plugin.
*/
require_once __DIR__ . '/DemoPluginController.php';
/*
* RENDER HEADER, INCLUDES, FOOTER
*
@ -15,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;
@ -60,6 +63,17 @@ function demo_plugin_init($conf)
return $errors;
}
function demo_plugin_register_routes(): array
{
return [
[
'method' => 'GET',
'route' => '/custom',
'callable' => 'Shaarli\DemoPlugin\DemoPluginController:index',
],
];
}
/**
* Hook render_header.
* Executed on every page render.
@ -250,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/';
@ -304,7 +329,11 @@ function hook_demo_plugin_render_editlink($data)
function hook_demo_plugin_render_tools($data)
{
// field_plugin
$data['tools_plugin'][] = 'tools_plugin';
$data['tools_plugin'][] = '<div class="tools-item">
<a href="' . $data['_BASE_PATH_'] . '/plugin/demo_plugin/custom">
<span class="pure-button pure-u-lg-2-3 pure-u-3-4">Demo Plugin Custom Route</span>
</a>
</div>';
return $data;
}
@ -469,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;
/**
@ -120,4 +121,58 @@ public function testGetPluginsMeta(): void
$this->assertEquals('test plugin', $meta[self::$pluginName]['description']);
$this->assertEquals($expectedParameters, $meta[self::$pluginName]['parameters']);
}
/**
* Test plugin custom routes - note that there is no check on callable functions
*/
public function testRegisteredRoutes(): void
{
PluginManager::$PLUGINS_PATH = self::$pluginPath;
$this->pluginManager->load([self::$pluginName]);
$expectedParameters = [
[
'method' => 'GET',
'route' => '/test',
'callable' => 'getFunction',
],
[
'method' => 'POST',
'route' => '/custom',
'callable' => 'postFunction',
],
];
$meta = $this->pluginManager->getRegisteredRoutes();
static::assertSame($expectedParameters, $meta[self::$pluginName]);
}
/**
* Test plugin custom routes with invalid route
*/
public function testRegisteredRoutesInvalid(): void
{
$plugin = 'test_route_invalid';
$this->pluginManager->load([$plugin]);
$meta = $this->pluginManager->getRegisteredRoutes();
static::assertSame([], $meta);
$errors = $this->pluginManager->getErrors();
static::assertSame(['test_route_invalid [plugin incompatibility]: trying to register invalid route.'], $errors);
}
public function testSearchFilterPlugin(): void
{
PluginManager::$PLUGINS_PATH = self::$pluginPath;
$this->pluginManager->load([self::$pluginName]);
static::assertNull($this->pluginManager->getFilterSearchEntryHooks());
static::assertTrue($this->pluginManager->filterSearchEntry(new Bookmark(), ['_result' => true]));
static::assertCount(1, $this->pluginManager->getFilterSearchEntryHooks());
static::assertSame('hook_test_filter_search_entry', $this->pluginManager->getFilterSearchEntryHooks()[0]);
static::assertFalse($this->pluginManager->filterSearchEntry(new Bookmark(), ['_result' => false]));
}
}

View file

@ -3,6 +3,7 @@
use Shaarli\Config\ConfigManager;
use Shaarli\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;
@ -229,4 +236,52 @@ public function testPostLinkDuplicate()
\DateTime::createFromFormat(\DateTime::ATOM, $data['updated'])
);
}
/**
* Test link creation with a tag string provided
*/
public function testPostLinkWithTagString(): void
{
$link = [
'tags' => 'one two',
];
$env = Environment::mock([
'REQUEST_METHOD' => 'POST',
'CONTENT_TYPE' => 'application/json'
]);
$request = Request::createFromEnvironment($env);
$request = $request->withParsedBody($link);
$response = $this->controller->postLink($request, new Response());
$this->assertEquals(201, $response->getStatusCode());
$this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]);
$data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(['one', 'two'], $data['tags']);
}
/**
* Test link creation with a tag string provided
*/
public function testPostLinkWithTagString2(): void
{
$link = [
'tags' => ['one two'],
];
$env = Environment::mock([
'REQUEST_METHOD' => 'POST',
'CONTENT_TYPE' => 'application/json'
]);
$request = Request::createFromEnvironment($env);
$request = $request->withParsedBody($link);
$response = $this->controller->postLink($request, new Response());
$this->assertEquals(201, $response->getStatusCode());
$this->assertEquals('/api/v1/bookmarks/1', $response->getHeader('Location')[0]);
$data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(['one', 'two'], $data['tags']);
}
}

View file

@ -8,6 +8,7 @@
use Shaarli\Bookmark\BookmarkFileService;
use Shaarli\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;
@ -233,4 +240,52 @@ public function testGetLink404()
$this->controller->putLink($request, new Response(), ['id' => -1]);
}
/**
* Test link creation with a tag string provided
*/
public function testPutLinkWithTagString(): void
{
$link = [
'tags' => 'one two',
];
$id = '41';
$env = Environment::mock([
'REQUEST_METHOD' => 'PUT',
'CONTENT_TYPE' => 'application/json'
]);
$request = Request::createFromEnvironment($env);
$request = $request->withParsedBody($link);
$response = $this->controller->putLink($request, new Response(), ['id' => $id]);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(['one', 'two'], $data['tags']);
}
/**
* Test link creation with a tag string provided
*/
public function testPutLinkWithTagString2(): void
{
$link = [
'tags' => ['one two'],
];
$id = '41';
$env = Environment::mock([
'REQUEST_METHOD' => 'PUT',
'CONTENT_TYPE' => 'application/json'
]);
$request = Request::createFromEnvironment($env);
$request = $request->withParsedBody($link);
$response = $this->controller->putLink($request, new Response(), ['id' => $id]);
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode((string) $response->getBody(), true);
$this->assertEquals(self::NB_FIELDS_LINK, count($data));
$this->assertEquals(['one', 'two'], $data['tags']);
}
}

View file

@ -8,6 +8,7 @@
use Shaarli\Bookmark\LinkDB;
use Shaarli\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

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

View file

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

View file

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

View file

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

View file

@ -11,6 +11,7 @@
use Shaarli\Config\ConfigManager;
use Shaarli\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

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

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

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

View file

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

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

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@
use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\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

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

View file

@ -56,11 +56,11 @@ <h3 class="window-subtitle">{'General'|t}</h3>
{include="server.requirements"}
<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=
@ -3052,9 +3052,9 @@ inherits@2.0.3:
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
ini@^1.3.4, ini@^1.3.5:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
version "1.3.7"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.7.tgz#a09363e1911972ea16d7a8851005d84cf09a9a84"
integrity sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==
interpret@^1.4.0:
version "1.4.0"
@ -3714,7 +3714,7 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
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=