diff --git a/Dockerfile b/Dockerfile index f6120b71..79d33130 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 \ diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 5bbf6680..471f2397 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -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 \ diff --git a/application/Utils.php b/application/Utils.php index 952378ab..c5cd884b 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -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); } diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php index 9fb88358..cc7af18e 100644 --- a/application/api/ApiMiddleware.php +++ b/application/api/ApiMiddleware.php @@ -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 diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php index 05a2840a..9228bb2d 100644 --- a/application/api/ApiUtils.php +++ b/application/api/ApiUtils.php @@ -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); diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php index c379b962..fe4bdc9f 100644 --- a/application/api/controllers/Links.php +++ b/application/api/controllers/Links.php @@ -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()) diff --git a/application/api/controllers/Tags.php b/application/api/controllers/Tags.php index e60e00a7..5a23f6db 100644 --- a/application/api/controllers/Tags.php +++ b/application/api/controllers/Tags.php @@ -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); diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php index 6666a251..e64eeafb 100644 --- a/application/bookmark/BookmarkFileService.php +++ b/application/bookmark/BookmarkFileService.php @@ -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; } diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php index db83c51c..8b41dbb8 100644 --- a/application/bookmark/BookmarkFilter.php +++ b/application/bookmark/BookmarkFilter.php @@ -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( '/(?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, diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php index c78dbe41..8439d470 100644 --- a/application/bookmark/BookmarkIO.php +++ b/application/bookmark/BookmarkIO.php @@ -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(); + } + } } diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php index 08cdbb4e..4b1f0daa 100644 --- a/application/bookmark/BookmarkServiceInterface.php +++ b/application/bookmark/BookmarkServiceInterface.php @@ -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. diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php index d65e97ed..8fa2953a 100644 --- a/application/bookmark/LinkUtils.php +++ b/application/bookmark/LinkUtils.php @@ -1,6 +1,7 @@ ]+(?:' . $properties . ')=(?:' . $orCondition . ')[^>]*content=(["\'])([^\1]*?)\1.*?>#'; + $ogRegex = '#]+(?:' . $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 = '#]+content=(["\'])([^\1]*?)\1[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#'; + $ogRegexReverse = '#]+' . $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, '$1', $text); + $format = function (array $match): string { + return '' . $match[1] . '' + ; + }; + + 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#$2'; - 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] . '' . + '#' . $match[2] . + ''; + }; + + return preg_replace_callback($regex, $format, $description); } /** diff --git a/application/bookmark/SearchResult.php b/application/bookmark/SearchResult.php new file mode 100644 index 00000000..c0bce311 --- /dev/null +++ b/application/bookmark/SearchResult.php @@ -0,0 +1,136 @@ +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; + } +} diff --git a/application/container/ContainerBuilder.php b/application/container/ContainerBuilder.php index f0234eca..f66d75bd 100644 --- a/application/container/ContainerBuilder.php +++ b/application/container/ContainerBuilder.php @@ -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, diff --git a/application/feed/CachedPage.php b/application/feed/CachedPage.php index d809bdd9..c23c200f 100644 --- a/application/feed/CachedPage.php +++ b/application/feed/CachedPage.php @@ -1,34 +1,43 @@ 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); } /** diff --git a/application/feed/FeedBuilder.php b/application/feed/FeedBuilder.php index ed62af26..d5d74fd1 100644 --- a/application/feed/FeedBuilder.php +++ b/application/feed/FeedBuilder.php @@ -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']); diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php index 7e0afafc..7e93bf71 100644 --- a/application/formatter/BookmarkDefaultFormatter.php +++ b/application/formatter/BookmarkDefaultFormatter.php @@ -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 diff --git a/application/formatter/BookmarkMarkdownExtraFormatter.php b/application/formatter/BookmarkMarkdownExtraFormatter.php index 0694b23f..da539bfd 100644 --- a/application/formatter/BookmarkMarkdownExtraFormatter.php +++ b/application/formatter/BookmarkMarkdownExtraFormatter.php @@ -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(); } } diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php index ee4e8dca..d4dccee6 100644 --- a/application/formatter/BookmarkMarkdownFormatter.php +++ b/application/formatter/BookmarkMarkdownFormatter.php @@ -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; diff --git a/application/formatter/Parsedown/ShaarliParsedown.php b/application/formatter/Parsedown/ShaarliParsedown.php new file mode 100644 index 00000000..8eb48fda --- /dev/null +++ b/application/formatter/Parsedown/ShaarliParsedown.php @@ -0,0 +1,15 @@ +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; + } +} diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php index 8675a0c5..1333cce7 100644 --- a/application/front/controller/admin/ManageTagController.php +++ b/application/front/controller/admin/ManageTagController.php @@ -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), diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php index fabeaf2f..4b74f4a9 100644 --- a/application/front/controller/admin/ServerController.php +++ b/application/front/controller/admin/ServerController.php @@ -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); diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php index 35837baa..9633cd51 100644 --- a/application/front/controller/admin/ShaareManageController.php +++ b/application/front/controller/admin/ShaareManageController.php @@ -66,6 +66,10 @@ public function deleteBookmark(Request $request, Response $response): Response return $response->write(''); } + if ($request->getParam('source') === 'batch') { + return $response->withStatus(204); + } + // Don't redirect to permalink after deletion. return $this->redirectFromReferer($request, $response, ['shaare/']); } diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php index 4cbfcdc5..fb9cacc2 100644 --- a/application/front/controller/admin/ShaarePublishController.php +++ b/application/front/controller/admin/ShaarePublishController.php @@ -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'] ; diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php index 94d97d4b..5dfea096 100644 --- a/application/front/controller/admin/ThumbnailsController.php +++ b/application/front/controller/admin/ThumbnailsController.php @@ -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; diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php index fe8231be..4aae2652 100644 --- a/application/front/controller/visitor/BookmarkListController.php +++ b/application/front/controller/visitor/BookmarkListController.php @@ -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(), diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php index 846cfe22..3739ec16 100644 --- a/application/front/controller/visitor/DailyController.php +++ b/application/front/controller/visitor/DailyController.php @@ -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' => [], ]; diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php index bf965929..418d4a49 100644 --- a/application/front/controller/visitor/InstallController.php +++ b/application/front/controller/visitor/InstallController.php @@ -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')); diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php index 23553ee6..9c8f07d7 100644 --- a/application/front/controller/visitor/PictureWallController.php +++ b/application/front/controller/visitor/PictureWallController.php @@ -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) { diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php index ae946c59..d3f28f2f 100644 --- a/application/front/controller/visitor/ShaarliVisitorController.php +++ b/application/front/controller/visitor/ShaarliVisitorController.php @@ -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)); diff --git a/application/helper/ApplicationUtils.php b/application/helper/ApplicationUtils.php index 212dd8e2..f79998b5 100644 --- a/application/helper/ApplicationUtils.php +++ b/application/helper/ApplicationUtils.php @@ -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. * diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php index 5fabc907..05f95812 100644 --- a/application/helper/DailyPageHelper.php +++ b/application/helper/DailyPageHelper.php @@ -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) + ); + } } diff --git a/application/http/MetadataRetriever.php b/application/http/MetadataRetriever.php index 2e1401ec..cfc72583 100644 --- a/application/http/MetadataRetriever.php +++ b/application/http/MetadataRetriever.php @@ -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); } } diff --git a/application/netscape/NetscapeBookmarkUtils.php b/application/netscape/NetscapeBookmarkUtils.php index 2d97b4c8..20715bd0 100644 --- a/application/netscape/NetscapeBookmarkUtils.php +++ b/application/netscape/NetscapeBookmarkUtils.php @@ -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) { diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php index 3ea55728..939db1ea 100644 --- a/application/plugin/PluginManager.php +++ b/application/plugin/PluginManager.php @@ -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//`. + * - `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; + } } diff --git a/application/plugin/exception/PluginInvalidRouteException.php b/application/plugin/exception/PluginInvalidRouteException.php new file mode 100644 index 00000000..6ba9bc43 --- /dev/null +++ b/application/plugin/exception/PluginInvalidRouteException.php @@ -0,0 +1,26 @@ +message = 'trying to register invalid route.'; + } +} diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php index 97805c35..fe74bf27 100644 --- a/application/render/PageCacheManager.php +++ b/application/render/PageCacheManager.php @@ -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 ); } } diff --git a/application/updater/Updater.php b/application/updater/Updater.php index 4f557d0f..11b6c051 100644 --- a/application/updater/Updater.php +++ b/application/updater/Updater.php @@ -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(), '?') diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js index 557325ee..6fc16faf 100644 --- a/assets/common/js/shaare-batch.js +++ b/assets/common/js/shaare-batch.js @@ -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}/`; }); }); }); diff --git a/composer.lock b/composer.lock index 0023df88..f2f9c7f1 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/doc/md/Docker.md b/doc/md/Docker.md index fc406c00..64c42d77 100644 --- a/doc/md/Docker.md +++ b/doc/md/Docker.md @@ -194,7 +194,7 @@ $ docker logs -f # 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 ``` diff --git a/doc/md/REST-API.md b/doc/md/REST-API.md index 01071d8e..2a36ea29 100644 --- a/doc/md/REST-API.md +++ b/doc/md/REST-API.md @@ -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 `. +- This token must be included as an HTTP header called `Authorization: Bearer `. - JWT tokens are composed by three parts, separated by a dot `.` and encoded in base64: ``` diff --git a/doc/md/Server-configuration.md b/doc/md/Server-configuration.md index a49b6033..e4086dca 100644 --- a/doc/md/Server-configuration.md +++ b/doc/md/Server-configuration.md @@ -199,6 +199,8 @@ sudo nano /etc/apache2/sites-available/shaarli.mydomain.org.conf Require all denied + DirectoryIndex index.php + Require all granted diff --git a/doc/md/dev/Plugin-system.md b/doc/md/dev/Plugin-system.md index f09fadc2..0ada57ea 100644 --- a/doc/md/dev/Plugin-system.md +++ b/doc/md/dev/Plugin-system.md @@ -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 .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 _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//`. + - `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 /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 diff --git a/inc/languages/de/LC_MESSAGES/shaarli.po b/inc/languages/de/LC_MESSAGES/shaarli.po index 34d29ce8..26249b72 100644 --- a/inc/languages/de/LC_MESSAGES/shaarli.po +++ b/inc/languages/de/LC_MESSAGES/shaarli.po @@ -2,15 +2,15 @@ msgid "" msgstr "" "Project-Id-Version: Shaarli\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-03-31 09:09+0200\n" -"PO-Revision-Date: 2018-03-31 09:12+0200\n" +"POT-Creation-Date: 2021-01-23 23:57+0100\n" +"PO-Revision-Date: 2021-01-24 00:37+0100\n" "Last-Translator: \n" "Language-Team: Shaarli\n" "Language: de\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"X-Generator: Poedit 2.0.6\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" @@ -19,7 +19,614 @@ msgstr "" "X-Poedit-SearchPathExcluded-0: node_modules\n" "X-Poedit-SearchPathExcluded-1: vendor\n" -#: application/ApplicationUtils.php:153 +#: application/History.php:181 +msgid "History file isn't readable or writable" +msgstr "Protokolldatei nicht lesbar oder beschreibbar" + +#: application/History.php:192 +msgid "Could not parse history file" +msgstr "Protokolldatei konnte nicht analysiert werden" + +#: application/Languages.php:184 +msgid "Automatic" +msgstr "Automatisch" + +#: application/Languages.php:185 +msgid "German" +msgstr "Deutsch" + +#: application/Languages.php:186 +msgid "English" +msgstr "Englisch" + +#: application/Languages.php:187 +msgid "French" +msgstr "Französisch" + +#: application/Languages.php:188 +msgid "Japanese" +msgstr "Japanisch" + +#: application/Languages.php:189 +msgid "Russian" +msgstr "Russisch" + +#: application/Thumbnailer.php:62 +msgid "" +"php-gd extension must be loaded to use thumbnails. Thumbnails are now " +"disabled. Please reload the page." +msgstr "" +"Die Erweiterung php-gd muss geladen werden, um Miniaturansichten " +"(Thumbnails) verwenden zu können. Thumbnails sind jetzt deaktiviert. Bitte " +"lade die Seite neu." + +#: application/Utils.php:405 tests/UtilsTest.php:327 +msgid "Setting not set" +msgstr "Einstellung nicht gesetzt" + +#: application/Utils.php:412 tests/UtilsTest.php:325 tests/UtilsTest.php:326 +msgid "Unlimited" +msgstr "Unbegrenzt" + +#: application/Utils.php:415 tests/UtilsTest.php:322 tests/UtilsTest.php:323 +#: tests/UtilsTest.php:337 +msgid "B" +msgstr "B" + +#: application/Utils.php:415 tests/UtilsTest.php:316 tests/UtilsTest.php:317 +#: tests/UtilsTest.php:324 +msgid "kiB" +msgstr "kiB" + +#: application/Utils.php:415 tests/UtilsTest.php:318 tests/UtilsTest.php:319 +#: tests/UtilsTest.php:335 tests/UtilsTest.php:336 +msgid "MiB" +msgstr "MiB" + +#: application/Utils.php:415 tests/UtilsTest.php:320 tests/UtilsTest.php:321 +msgid "GiB" +msgstr "GiB" + +#: 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 "Du bist nicht berechtigt, den Datenspeicher zu ändern" + +#: application/bookmark/BookmarkFileService.php:210 +msgid "This bookmarks already exists" +msgstr "Diese Lesezeichen sind bereits vorhanden" + +#: application/bookmark/BookmarkInitializer.php:42 +msgid "(private bookmark with thumbnail demo)" +msgstr "(privates Lesezeichen mit Thumbnail-Demo)" + +#: application/bookmark/BookmarkInitializer.php:45 +msgid "" +"Shaarli will automatically pick up the thumbnail for links to a variety of " +"websites.\n" +"\n" +"Explore your new Shaarli instance by trying out controls and menus.\n" +"Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the " +"documentation](https://shaarli.readthedocs.io/en/master/) to learn more " +"about Shaarli.\n" +"\n" +"Now you can edit or delete the default shaares.\n" +msgstr "" +"Shaarli holt sich automatisch das Miniaturbild (Thumbnail) für Links zu " +"verschiedenen Websites.\n" +"\n" +"Erkunde Deine neue Shaarli-Instanz, indem Du Steuerelemente und Menüs " +"ausprobierst.\n" +"Besuche das Projekt auf [Github](https://github.com/shaarli/Shaarli) oder " +"[die Dokumentation](https://shaarli.readthedocs.io/en/master/), um mehr über " +"Shaarli zu erfahren.\n" +"\n" +"Jetzt kannst Du die Standard-Shaares bearbeiten oder löschen.\n" + +#: application/bookmark/BookmarkInitializer.php:58 +msgid "Note: Shaare descriptions" +msgstr "Hinweis: Shaare Beschreibungen" + +#: application/bookmark/BookmarkInitializer.php:60 +msgid "" +"Adding a shaare without entering a URL creates a text-only \"note\" post " +"such as this one.\n" +"This note is private, so you are the only one able to see it while logged " +"in.\n" +"\n" +"You can use this to keep notes, post articles, code snippets, and much " +"more.\n" +"\n" +"The Markdown formatting setting allows you to format your notes and bookmark " +"description:\n" +"\n" +"### Title headings\n" +"\n" +"#### Multiple headings levels\n" +" * bullet lists\n" +" * _italic_ text\n" +" * **bold** text\n" +" * ~~strike through~~ text\n" +" * `code` blocks\n" +" * images\n" +" * [links](https://en.wikipedia.org/wiki/Markdown)\n" +"\n" +"Markdown also supports tables:\n" +"\n" +"| Name | Type | Color | Qty |\n" +"| ------- | --------- | ------ | ----- |\n" +"| Orange | Fruit | Orange | 126 |\n" +"| Apple | Fruit | Any | 62 |\n" +"| Lemon | Fruit | Yellow | 30 |\n" +"| Carrot | Vegetable | Red | 14 |\n" +msgstr "" +"Durch Hinzufügen eines Shaare ohne Eingabe einer URL wird ein Nur-Text-" +"Notizbeitrag wie dieser erstellt.\n" +"Diese Notiz ist privat, sodass Du sie als einziger sehen kannst, während Du " +"angemeldet bist.\n" +"\n" +"Du kannst dies nutzen, um Notizen zu machen, Artikel, Codefragmente und " +"vieles mehr zu veröffentlichen.\n" +"\n" +"Mit der Markdown-Formatierungseinstellung kannst Du Deine Notizen und die " +"Lesezeichenbeschreibung formatieren:\n" +"\n" +"### Titel-Überschrift\n" +"\n" +"#### Mehrere Überschriftenebenen\n" +" * bullet lists\n" +" * _kursiver_ Text\n" +" * **fetter** Text\n" +" * ~~durchgestrichener~~ Text\n" +" * `Code` Blöcke\n" +" * Bilder\n" +" * [Links](https://en.wikipedia.org/wiki/Markdown)\n" +"\n" +"Markdown unterstützt auch Tabellen:\n" +"\n" +"| Name | Typ | Farbe | Menge |\n" +"| ------- | --------- | ------ | ----- |\n" +"| Orange | Frucht | orange | 126 |\n" +"| Apfel | Frucht | verschiedene | 62 |\n" +"| Lemon | Frucht | gelb | 30 |\n" +"| Karotte | Gemüse | rot | 14 |\n" + +#: application/bookmark/BookmarkInitializer.php:94 +#: application/legacy/LegacyLinkDB.php:246 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "" +"The personal, minimalist, super-fast, database free, bookmarking service" +msgstr "" +"Der persönliche, minimalistische, superschnelle, datenbankfreie " +"Lesezeichenservice" + +#: application/bookmark/BookmarkInitializer.php:97 +msgid "" +"Welcome to Shaarli!\n" +"\n" +"Shaarli allows you to bookmark your favorite pages, and share them with " +"others or store them privately.\n" +"You can add a description to your bookmarks, such as this one, and tag " +"them.\n" +"\n" +"Create a new shaare by clicking the `+Shaare` button, or using any of the " +"recommended tools (browser extension, mobile app, bookmarklet, REST API, " +"etc.).\n" +"\n" +"You can easily retrieve your links, even with thousands of them, using the " +"internal search engine, or search through tags (e.g. this Shaare is tagged " +"with `shaarli` and `help`).\n" +"Hashtags such as #shaarli #help are also supported.\n" +"You can also filter the available [RSS feed](/feed/atom) and picture wall by " +"tag or plaintext search.\n" +"\n" +"We hope that you will enjoy using Shaarli, maintained with ❤️ by the " +"community!\n" +"Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if " +"you have a suggestion or encounter an issue.\n" +msgstr "" +"Willkommen bei Shaarli!\n" +"\n" +"Mit Shaarli kannst Du Lesezeichen für Deine Lieblingsseiten anlegen und mit " +"anderen teilen oder privat speichern.\n" +"Du kannst Lesezeichen wie diesem eine Beschreibung hinzufügen und sie mit " +"Tags versehen.\n" +"\n" +"Erstelle eine neue Shaare, indem Du auf die Schaltfläche \"+ Shaare\" " +"klickst oder eines der empfohlenen Tools (Browsererweiterung, mobile App, " +"Lesezeichen, REST-API usw.) verwendest.\n" +"\n" +"Du kannst Deine Links - auch mit Tausenden von ihnen- einfach über die " +"interne Suchmaschine abrufen oder Tags durchsuchen (z. B. ist diese Shaare " +"mit \"shaarli\" und \"help\" gekennzeichnet).\n" +"Hashtags wie #shaarli #help werden ebenfalls unterstützt.\n" +"Du kannst den verfügbaren [RSS-Feed] (/feed/atom) und die Bilderwand auch " +"nach Tag- oder Klartextsuche filtern.\n" +"\n" +"Wir hoffen, dass Du Shaarli schätzen wirst, das von der Community mit ❤️ " +"gepflegt wird!\n" +"Du kannst gerne [ein Problem] (https://github.com/shaarli/Shaarli/issues) " +"öffnen, wenn Du einen Vorschlag hast oder auf ein Problem stößt.\n" + +#: application/bookmark/exception/BookmarkNotFoundException.php:14 +msgid "The link you are trying to reach does not exist or has been deleted." +msgstr "" +"Den Link, den du versucht zu erreichen, existiert nicht oder wurde gelöscht." + +#: 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." +msgstr "" +"Shaarli konnte die Konfigurationsdatei nicht erstellen. Bitte stelle sicher, " +"dass Shaarli das Recht hat, in den Ordner zu schreiben, in dem es " +"installiert ist." + +#: application/config/ConfigManager.php:137 +#: application/config/ConfigManager.php:164 +msgid "Invalid setting key parameter. String expected, got: " +msgstr "" +"Ungültiger Parameter für den Einstellungsschlüssel. Zeichenfolge erwartet, " +"erhalten: " + +#: application/config/exception/MissingFieldConfigException.php:20 +#, php-format +msgid "Configuration value is required for %s" +msgstr "Konfigurationswert erforderlich für %s" + +#: application/config/exception/PluginConfigOrderException.php:15 +msgid "An error occurred while trying to save plugins loading order." +msgstr "" +"Beim Versuch, die Ladereihenfolge der Plugins zu speichern, ist ein Fehler " +"aufgetreten." + +#: application/config/exception/UnauthorizedConfigException.php:15 +msgid "You are not authorized to alter config." +msgstr "Du bist nicht berechtigt, die Konfiguration zu ändern." + +#: application/exceptions/IOException.php:23 +msgid "Error accessing" +msgstr "Fehler beim Zugriff" + +#: application/feed/FeedBuilder.php:180 +msgid "Direct link" +msgstr "Direct Link" + +#: 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 "Permalink" + +#: application/front/controller/admin/ConfigureController.php:56 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +msgid "Configure" +msgstr "Konfigurieren" + +#: application/front/controller/admin/ConfigureController.php:106 +#: application/legacy/LegacyUpdater.php:539 +msgid "You have enabled or changed thumbnails mode." +msgstr "Du hast den Miniaturansichten-Modus aktiviert oder geändert." + +#: application/front/controller/admin/ConfigureController.php:108 +#: application/front/controller/admin/ServerController.php:81 +#: application/legacy/LegacyUpdater.php:540 +msgid "Please synchronize them." +msgstr "Bitte synchronisiere sie." + +#: application/front/controller/admin/ConfigureController.php:119 +#: application/front/controller/visitor/InstallController.php:154 +msgid "Error while writing config file after configuration update." +msgstr "" +"Fehler beim Schreiben der Einstellungsdatei nach der " +"Konfigurationsaktualisierung." + +#: application/front/controller/admin/ConfigureController.php:128 +msgid "Configuration was saved." +msgstr "Konfiguration wurde gespeichert." + +#: application/front/controller/admin/ExportController.php:26 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 +msgid "Export" +msgstr "Exportieren" + +#: application/front/controller/admin/ExportController.php:42 +msgid "Please select an export mode." +msgstr "Bitte wähle einen Export-Modus." + +#: application/front/controller/admin/ImportController.php:41 +#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +msgid "Import" +msgstr "Importieren" + +#: application/front/controller/admin/ImportController.php:55 +msgid "No import file provided." +msgstr "Keine Import-Datei übergeben." + +#: application/front/controller/admin/ImportController.php:66 +#, php-format +msgid "" +"The file you are trying to upload is probably bigger than what this " +"webserver can accept (%s). Please upload in smaller chunks." +msgstr "" +"Die Datei, die du hochladen möchtest, ist wahrscheinlich größer als das, was " +"dieser Webserver akzeptieren kann (%s). Bitte lade in kleineren Blöcken hoch." + +#: application/front/controller/admin/ManageTagController.php:30 +msgid "whitespace" +msgstr "Leerzeichen" + +#: application/front/controller/admin/ManageTagController.php:35 +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +msgid "Manage tags" +msgstr "Tags verwalten" + +#: application/front/controller/admin/ManageTagController.php:54 +msgid "Invalid tags provided." +msgstr "Ungültige Tags übergeben." + +#: application/front/controller/admin/ManageTagController.php:78 +#, php-format +msgid "The tag was removed from %d bookmark." +msgid_plural "The tag was removed from %d bookmarks." +msgstr[0] "Der Tag wurde aus dem Lesezeichen %d entfernt." +msgstr[1] "Der Tag wurde aus den Lesezeichen %d entfernt." + +#: application/front/controller/admin/ManageTagController.php:83 +#, php-format +msgid "The tag was renamed in %d bookmark." +msgid_plural "The tag was renamed in %d bookmarks." +msgstr[0] "Der Tag wurde im Lesezeichen %d umbenannt." +msgstr[1] "Der Tag wurde in den Lesezeichen %d umbenannt." + +#: application/front/controller/admin/ManageTagController.php:105 +msgid "Tags separator must be a single character." +msgstr "Tags müssen durch ein einzelnen Zeichen getrennt werden." + +#: application/front/controller/admin/ManageTagController.php:111 +msgid "These characters are reserved and can't be used as tags separator: " +msgstr "" +"Diese Zeichen sind reserviert und können nicht als Tag-Trennzeichen genutzt " +"werden: " + +#: application/front/controller/admin/PasswordController.php:28 +#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +msgid "Change password" +msgstr "Passwort ändern" + +#: application/front/controller/admin/PasswordController.php:55 +msgid "You must provide the current and new password to change it." +msgstr "Du musst das aktuelle und das neue Passwort angeben zur Änderung." + +#: application/front/controller/admin/PasswordController.php:71 +msgid "The old password is not correct." +msgstr "Das alte Passwort ist nicht korrekt." + +#: application/front/controller/admin/PasswordController.php:97 +msgid "Your password has been changed" +msgstr "Dein Passwort wurde geändert" + +#: application/front/controller/admin/PluginsController.php:45 +msgid "Plugin Administration" +msgstr "Plugin Administration" + +#: application/front/controller/admin/PluginsController.php:76 +msgid "Setting successfully saved." +msgstr "Einstellung wurde erfolgreich gespeichert." + +#: application/front/controller/admin/PluginsController.php:79 +msgid "Error while saving plugin configuration: " +msgstr "Fehler beim Speichern der Plugin-Konfiguration: " + +#: application/front/controller/admin/ServerController.php:35 +msgid "Check disabled" +msgstr "Prüfung deaktiviert" + +#: application/front/controller/admin/ServerController.php:62 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Server administration" +msgstr "Server-Administration" + +#: application/front/controller/admin/ServerController.php:79 +msgid "Thumbnails cache has been cleared." +msgstr "Zwischenspeicher der Miniaturansichten wurde geleert." + +#: application/front/controller/admin/ServerController.php:90 +msgid "Shaarli's cache folder has been cleared!" +msgstr "Der Zwischenspeicher-Ordner von Shaarli wurde geleert!" + +#: application/front/controller/admin/ShaareAddController.php:26 +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +msgid "Shaare a new link" +msgstr "Teile einen neuen Link" + +#: application/front/controller/admin/ShaareManageController.php:35 +#: application/front/controller/admin/ShaareManageController.php:93 +msgid "Invalid bookmark ID provided." +msgstr "Ungültige Lesezeichen-ID bereitgestellt." + +#: application/front/controller/admin/ShaareManageController.php:47 +#: application/front/controller/admin/ShaareManageController.php:116 +#: application/front/controller/admin/ShaareManageController.php:156 +#: application/front/controller/admin/ShaarePublishController.php:82 +#, php-format +msgid "Bookmark with identifier %s could not be found." +msgstr "Lesezeichen mit der ID %s konnte nicht gefunden werden." + +#: application/front/controller/admin/ShaareManageController.php:101 +msgid "Invalid visibility provided." +msgstr "Ungültige Sichtbarkeit angegeben." + +#: application/front/controller/admin/ShaarePublishController.php:173 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 +msgid "Edit" +msgstr "Bearbeiten" + +#: application/front/controller/admin/ShaarePublishController.php:176 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28 +msgid "Shaare" +msgstr "Teilen" + +#: application/front/controller/admin/ShaarePublishController.php:208 +msgid "Note: " +msgstr "Notiz: " + +#: application/front/controller/admin/ThumbnailsController.php:37 +#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Thumbnails update" +msgstr "Thumbnail-Aktualisierung" + +#: application/front/controller/admin/ToolsController.php:31 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33 +msgid "Tools" +msgstr "Tools" + +#: application/front/controller/visitor/BookmarkListController.php:121 +msgid "Search: " +msgstr "Suche: " + +#: application/front/controller/visitor/DailyController.php:201 +msgid "day" +msgstr "Tag" + +#: application/front/controller/visitor/DailyController.php:201 +#: application/front/controller/visitor/DailyController.php:204 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "Daily" +msgstr "Täglich" + +#: application/front/controller/visitor/DailyController.php:202 +msgid "week" +msgstr "Woche" + +#: application/front/controller/visitor/DailyController.php:202 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +msgid "Weekly" +msgstr "Wöchentlich" + +#: application/front/controller/visitor/DailyController.php:203 +msgid "month" +msgstr "Monat" + +#: application/front/controller/visitor/DailyController.php:203 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "Monthly" +msgstr "Monatlich" + +#: application/front/controller/visitor/ErrorController.php:30 +msgid "Error: " +msgstr "Fehler: " + +#: application/front/controller/visitor/ErrorController.php:34 +msgid "Please report it on Github." +msgstr "Bitte berichte es bei Github." + +#: application/front/controller/visitor/ErrorController.php:39 +msgid "An unexpected error occurred." +msgstr "Ein unerwarteter Fehler ist aufgetreten." + +#: application/front/controller/visitor/ErrorNotFoundController.php:25 +msgid "Requested page could not be found." +msgstr "Angefragte Seite kann nicht gefunden werden." + +#: application/front/controller/visitor/InstallController.php:70 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Install Shaarli" +msgstr "Installiere Shaarli" + +#: application/front/controller/visitor/InstallController.php:90 +#, php-format +msgid "" +"
Sessions do not seem to work correctly on your server.
Make sure the " +"variable \"session.save_path\" is set correctly in your PHP config, and that " +"you have write access to it.
It currently points to %s.
On some " +"browsers, accessing your server via a hostname like 'localhost' or any " +"custom hostname without a dot causes cookie storage to fail. We recommend " +"accessing your server via it's IP address or Fully Qualified Domain Name.
" +msgstr "" +"
Sessions scheinen auf deinem Server nicht korrekt zu funktionieren. "
+"
Stelle sicher, dass die Variable \"session.save_path\" in deiner PHP-" +"Konfiguration richtig eingestellt ist und dass du Schreibzugriff darauf hast." +"
Es verweist aktuell auf %s.
Bei einigen Browsern führt der Zugriff " +"auf deinen Server über einen Hostnamen wie \"localhost\" oder einen " +"beliebigen benutzerdefinierten Hostnamen ohne Punkt dazu, dass der Cookie-" +"Speicher fehlschlägt. Wir empfehlen den Zugriff auf deinen Server über die " +"IP-Adresse oder den Fully Qualified Domain Namen.
" + +#: application/front/controller/visitor/InstallController.php:162 +msgid "" +"Shaarli is now configured. Please login and start shaaring your bookmarks!" +msgstr "" +"Shaarli ist nun konfiguriert. Bitte melden Dich an und teile Deine " +"Lesezeichen!" + +#: application/front/controller/visitor/InstallController.php:176 +msgid "Insufficient permissions:" +msgstr "Unzureichende Berechtigungen:" + +#: application/front/controller/visitor/LoginController.php:46 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:77 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:101 +msgid "Login" +msgstr "Einloggen" + +#: application/front/controller/visitor/LoginController.php:78 +msgid "Wrong login/password." +msgstr "Falscher Loging/Passwort." + +#: application/front/controller/visitor/PictureWallController.php:29 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:43 +msgid "Picture wall" +msgstr "Bildwand" + +#: application/front/controller/visitor/TagCloudController.php:90 +msgid "Tag " +msgstr "Tag Liste " + +#: application/front/exceptions/AlreadyInstalledException.php:11 +msgid "Shaarli has already been installed. Login to edit the configuration." +msgstr "" +"Shaarlie wurde bereits installiert. Melde Dich an zum Ändern der " +"Konfiguration." + +#: application/front/exceptions/LoginBannedException.php:11 +msgid "" +"You have been banned after too many failed login attempts. Try again later." +msgstr "" +"Du wurdest nach zu vielen fehlgeschlagenen Anmeldeversuchen gesperrt. " +"Versuche es später noch einmal." + +#: application/front/exceptions/OpenShaarliPasswordException.php:16 +msgid "You are not supposed to change a password on an Open Shaarli." +msgstr "Du darfst kein Passwort für ein offenes Shaarli ändern." + +#: application/front/exceptions/ThumbnailsDisabledException.php:11 +msgid "Picture wall unavailable (thumbnails are disabled)." +msgstr "Bildwand ist nicht verfügbar (Miniaturansichten sind deaktiviert)." + +#: application/front/exceptions/WrongTokenException.php:16 +msgid "Wrong token." +msgstr "Falsches Zeichen." + +#: application/helper/ApplicationUtils.php:165 #, php-format msgid "" "Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus " @@ -30,89 +637,100 @@ msgstr "" "daher nicht laufen. Deine PHP-Version hat bekannte Sicherheitslücken und " "sollte so bald wie möglich aktualisiert werden." -#: application/ApplicationUtils.php:183 application/ApplicationUtils.php:195 +#: application/helper/ApplicationUtils.php:200 +#: application/helper/ApplicationUtils.php:220 msgid "directory is not readable" msgstr "Verzeichnis ist nicht lesbar" -#: application/ApplicationUtils.php:198 +#: application/helper/ApplicationUtils.php:223 msgid "directory is not writable" msgstr "Verzeichnis ist nicht beschreibbar" -#: application/ApplicationUtils.php:216 +#: application/helper/ApplicationUtils.php:247 msgid "file is not readable" msgstr "Datei ist nicht lesbar" -#: application/ApplicationUtils.php:219 +#: application/helper/ApplicationUtils.php:250 msgid "file is not writable" msgstr "Datei ist nicht beschreibbar" -#: application/Cache.php:16 -#, php-format -msgid "Cannot purge %s: no directory" -msgstr "Kann nicht löschen, %s ist kein Verzeichnis" +#: application/helper/ApplicationUtils.php:265 +msgid "" +"Lock can not be acquired on the datastore. You might encounter concurrent " +"access issues." +msgstr "" +"Der Datenspeicher kann nicht gesperrt werden. Möglicherweise treten Probleme " +"beim gleichzeitigen Zugriff auf." -#: application/FeedBuilder.php:151 -msgid "Direct link" -msgstr "Direct Link" +#: application/helper/ApplicationUtils.php:298 +msgid "Configuration parsing" +msgstr "Konfigurationsanalyse" -#: application/FeedBuilder.php:153 -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88 -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178 -msgid "Permalink" -msgstr "Permalink" +#: application/helper/ApplicationUtils.php:299 +msgid "Slim Framework (routing, etc.)" +msgstr "Slim Framework (Routing usw.)" -#: application/History.php:174 -msgid "History file isn't readable or writable" -msgstr "Protokolldatei nicht lesbar oder beschreibbar" +#: application/helper/ApplicationUtils.php:300 +msgid "Multibyte (Unicode) string support" +msgstr "Unterstützung für Multibyte-Zeichenfolgen (Unicode)" -#: application/History.php:185 -msgid "Could not parse history file" -msgstr "Protokolldatei konnte nicht analysiert werden" +#: application/helper/ApplicationUtils.php:301 +msgid "Required to use thumbnails" +msgstr "Erforderlich, um Miniaturansichten (Thumbnails) zu verwenden" -#: application/Languages.php:177 -msgid "Automatic" -msgstr "Automatisch" +#: application/helper/ApplicationUtils.php:302 +msgid "Localized text sorting (e.g. e->è->f)" +msgstr "Lokalisierte Textsortierung (z. B. e->è->f)" -#: application/Languages.php:178 -msgid "English" -msgstr "Englisch" +#: application/helper/ApplicationUtils.php:303 +msgid "Better retrieval of bookmark metadata and thumbnail" +msgstr "Besserer Abruf von Lesezeichen-Metadaten und Miniaturansichten" -#: application/Languages.php:179 -msgid "French" -msgstr "Französisch" +#: application/helper/ApplicationUtils.php:304 +msgid "Use the translation system in gettext mode" +msgstr "Verwende das Übersetzungssystem im gettext-Modus" -#: application/Languages.php:180 -msgid "German" -msgstr "Deutsch" +#: application/helper/ApplicationUtils.php:305 +msgid "Login using LDAP server" +msgstr "Anmeldung mittels LDAP-Server" -#: application/LinkDB.php:136 +#: application/helper/DailyPageHelper.php:179 +msgid "Week" +msgstr "Woche" + +#: application/helper/DailyPageHelper.php:183 +msgid "Today" +msgstr "Heute" + +#: application/helper/DailyPageHelper.php:185 +msgid "Yesterday" +msgstr "Gestern" + +#: application/helper/FileUtils.php:100 +msgid "Provided path is not a directory." +msgstr "Der angegebene Pfad ist kein Verzeichnis." + +#: application/helper/FileUtils.php:104 +msgid "Trying to delete a folder outside of Shaarli path." +msgstr "Versuch, einen Ordner außerhalb des Shaarli-Pfads zu löschen." + +#: application/legacy/LegacyLinkDB.php:131 msgid "You are not authorized to add a link." msgstr "Du bist nicht berechtigt einen Link hinzuzufügen." -#: application/LinkDB.php:139 +#: application/legacy/LegacyLinkDB.php:134 msgid "Internal Error: A link should always have an id and URL." msgstr "Interner Fehler: Ein Link sollte immer eine ID und URL haben." -#: application/LinkDB.php:142 +#: application/legacy/LegacyLinkDB.php:137 msgid "You must specify an integer as a key." msgstr "Du musst eine Ganzzahl als Schlüssel angeben." -#: application/LinkDB.php:145 +#: application/legacy/LegacyLinkDB.php:140 msgid "Array offset and link ID must be equal." msgstr "Array-Offset und Link-ID müssen gleich sein." -#: application/LinkDB.php:251 -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 -msgid "" -"The personal, minimalist, super-fast, database free, bookmarking service" -msgstr "" -"Der persönliche, minimalistische, superschnelle, datenbankfreie " -"Lesezeichenservice" - -#: application/LinkDB.php:253 +#: application/legacy/LegacyLinkDB.php:249 msgid "" "Welcome to Shaarli! This is your first public bookmark. To edit or delete " "me, you must first login.\n" @@ -132,326 +750,115 @@ msgstr "" "Du verwendest die von der Community unterstützte Version des ursprünglichen " "Shaarli-Projekts von Sebastien Sauvage." -#: application/LinkDB.php:267 +#: application/legacy/LegacyLinkDB.php:266 msgid "My secret stuff... - Pastebin.com" msgstr "Meine geheimen Sachen... - Pastebin.com" -#: application/LinkDB.php:269 +#: application/legacy/LegacyLinkDB.php:268 msgid "Shhhh! I'm a private link only YOU can see. You can delete me too." msgstr "" "Pssst Ich bin ein privater Link, den nur du sehen kannst. Du kannst mich " "auch löschen." -#: application/LinkFilter.php:452 -msgid "The link you are trying to reach does not exist or has been deleted." -msgstr "" -"Den Link, den du versucht zu erreichen, existiert nicht oder wurde gelöscht." +#: application/legacy/LegacyUpdater.php:104 +msgid "Couldn't retrieve updater class methods." +msgstr "Die Updater-Klassenmethoden konnten nicht abgerufen werden." -#: application/NetscapeBookmarkUtils.php:35 +#: application/legacy/LegacyUpdater.php:540 +msgid "" +msgstr "" + +#: application/netscape/NetscapeBookmarkUtils.php:63 msgid "Invalid export selection:" msgstr "Ungültige Exportauswahl:" -#: application/NetscapeBookmarkUtils.php:81 +#: application/netscape/NetscapeBookmarkUtils.php:215 #, php-format msgid "File %s (%d bytes) " msgstr "Datei %s (%d bytes) " -#: application/NetscapeBookmarkUtils.php:83 +#: application/netscape/NetscapeBookmarkUtils.php:217 msgid "has an unknown file format. Nothing was imported." msgstr "hat ein unbekanntes Dateiformat. Es wurde nichts importiert." -#: application/NetscapeBookmarkUtils.php:86 +#: application/netscape/NetscapeBookmarkUtils.php:221 #, php-format msgid "" -"was successfully processed in %d seconds: %d links imported, %d links " -"overwritten, %d links skipped." +"was successfully processed in %d seconds: %d bookmarks imported, %d " +"bookmarks overwritten, %d bookmarks skipped." msgstr "" -"wurde erfolgreich in %d Sekunden verarbeitet: %d Links importiert, %d Links " -"überschrieben, %d Links übersprungen." +"wurde erfolgreich in %d Sekunden verarbeitet: %d Lesezeichen importiert, %d " +"Lesezeichen überschrieben, %d Lesezeichen übersprungen." -#: application/PageBuilder.php:168 -msgid "The page you are trying to reach does not exist or has been deleted." -msgstr "" -"Die Seite, die du erreichen möchtest, existiert nicht oder wurde gelöscht." +#: application/plugin/PluginManager.php:99 +#: application/plugin/PluginManager.php:137 +msgid " [plugin incompatibility]: " +msgstr " [Plugin-Inkompatibiliät]: " -#: application/PageBuilder.php:170 -msgid "404 Not Found" -msgstr "404 Nicht gefunden" - -#: application/PluginManager.php:243 +#: application/plugin/exception/PluginFileNotFoundException.php:22 #, php-format msgid "Plugin \"%s\" files not found." msgstr "Plugin \"%s\" Dateien nicht gefunden." -#: application/Updater.php:76 -msgid "Couldn't retrieve Updater class methods." -msgstr "Die Updater-Klassenmethoden konnten nicht abgerufen werden." +#: application/render/PageCacheManager.php:33 +#, php-format +msgid "Cannot purge %s: no directory" +msgstr "Kann nicht löschen, %s ist kein Verzeichnis" -#: application/Updater.php:532 +#: application/updater/exception/UpdaterException.php:51 msgid "An error occurred while running the update " msgstr "Beim Ausführen des Updates ist ein Fehler aufgetreten " -#: application/Updater.php:572 -msgid "Updates file path is not set, can't write updates." -msgstr "" -"Der Update-Dateipfad ist nicht festgelegt, es können keine Updates " -"geschrieben werden." +#: index.php:82 +msgid "Shared bookmarks on " +msgstr "Geteilte Lesezeichen auf " -#: application/Updater.php:577 -msgid "Unable to write updates in " -msgstr "Es ist nicht möglich Updates zu schreiben in " - -#: application/Utils.php:376 tests/UtilsTest.php:340 -msgid "Setting not set" -msgstr "Einstellung nicht gesetzt" - -#: application/Utils.php:383 tests/UtilsTest.php:338 tests/UtilsTest.php:339 -msgid "Unlimited" -msgstr "Unbegrenzt" - -#: application/Utils.php:386 tests/UtilsTest.php:335 tests/UtilsTest.php:336 -#: tests/UtilsTest.php:350 -msgid "B" -msgstr "B" - -#: application/Utils.php:386 tests/UtilsTest.php:329 tests/UtilsTest.php:330 -#: tests/UtilsTest.php:337 -msgid "kiB" -msgstr "kiB" - -#: application/Utils.php:386 tests/UtilsTest.php:331 tests/UtilsTest.php:332 -#: tests/UtilsTest.php:348 tests/UtilsTest.php:349 -msgid "MiB" -msgstr "MiB" - -#: application/Utils.php:386 tests/UtilsTest.php:333 tests/UtilsTest.php:334 -msgid "GiB" -msgstr "GiB" - -#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:121 -msgid "" -"Shaarli could not create the config file. Please make sure Shaarli has the " -"right to write in the folder is it installed in." -msgstr "" -"Shaarli konnte die Konfigurationsdatei nicht erstellen. Bitte stelle sicher, " -"dass Shaarli das Recht hat, in den Ordner zu schreiben, in dem es " -"installiert ist." - -#: application/config/ConfigManager.php:135 -msgid "Invalid setting key parameter. String expected, got: " -msgstr "" -"Ungültiger Parameter für den Einstellungsschlüssel. Zeichenfolge erwartet, " -"erhalten: " - -#: application/config/exception/MissingFieldConfigException.php:21 -#, php-format -msgid "Configuration value is required for %s" -msgstr "Konfigurationswert erforderlich für %s" - -#: application/config/exception/PluginConfigOrderException.php:15 -msgid "An error occurred while trying to save plugins loading order." -msgstr "" -"Beim Versuch, die Ladereihenfolge der Plugins zu speichern, ist ein Fehler " -"aufgetreten." - -#: application/config/exception/UnauthorizedConfigException.php:16 -msgid "You are not authorized to alter config." -msgstr "Du bist nicht berechtigt, die Konfiguration zu ändern." - -#: application/exceptions/IOException.php:19 -msgid "Error accessing" -msgstr "Fehler beim Zugriff" - -#: index.php:142 -msgid "Shared links on " -msgstr "Geteilte Links auf " - -#: index.php:164 -msgid "Insufficient permissions:" -msgstr "Unzureichende Berechtigungen:" - -#: index.php:303 -msgid "I said: NO. You are banned for the moment. Go away." -msgstr "Ich sagte NEIN. Du bist für den Moment gesperrt. Verschwinde." - -#: index.php:368 -msgid "Wrong login/password." -msgstr "Falscher Loging/Passwort." - -#: index.php:576 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42 -msgid "Daily" -msgstr "Täglich" - -#: index.php:681 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 -#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:95 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:71 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:95 -msgid "Login" -msgstr "Einloggen" - -#: index.php:722 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39 -msgid "Picture wall" -msgstr "Bildwand" - -#: index.php:770 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:36 -#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 -msgid "Tag cloud" -msgstr "Tag Cloud" - -#: index.php:803 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 -msgid "Tag list" -msgstr "Tag Liste" - -#: index.php:1028 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31 -msgid "Tools" -msgstr "Tools" - -#: index.php:1037 -msgid "You are not supposed to change a password on an Open Shaarli." -msgstr "Du darfst kein Passwort für ein offenes Shaarli ändern." - -#: index.php:1042 index.php:1084 index.php:1160 index.php:1191 index.php:1291 -msgid "Wrong token." -msgstr "Falsches Zeichen." - -#: index.php:1047 -msgid "The old password is not correct." -msgstr "Das alte Passwort ist nicht korrekt." - -#: index.php:1067 -msgid "Your password has been changed" -msgstr "Dein Passwort wurde geändert" - -#: index.php:1072 -#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 -msgid "Change password" -msgstr "Passwort ändern" - -#: index.php:1120 -msgid "Configuration was saved." -msgstr "Konfiguration wurde gespeichert." - -#: index.php:1143 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 -msgid "Configure" -msgstr "Konfigurieren" - -#: index.php:1154 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 -msgid "Manage tags" -msgstr "Tags verwalten" - -#: index.php:1172 -#, php-format -msgid "The tag was removed from %d link." -msgid_plural "The tag was removed from %d links." -msgstr[0] "Der Tag wurde aus dem Link %d entfernt." -msgstr[1] "Der Tag wurde aus den Links %d entfernt." - -#: index.php:1173 -#, php-format -msgid "The tag was renamed in %d link." -msgid_plural "The tag was renamed in %d links." -msgstr[0] "Der Tag wurde im Link %d umbenannt." -msgstr[1] "Der Tag wurde in den Links %d umbenannt." - -#: index.php:1181 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 -msgid "Shaare a new link" -msgstr "Teile einen neuen Link" - -#: index.php:1351 tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 -msgid "Edit" -msgstr "Bearbeiten" - -#: index.php:1351 index.php:1421 -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26 -msgid "Shaare" -msgstr "Teilen" - -#: index.php:1390 -msgid "Note: " -msgstr "Notiz: " - -#: index.php:1430 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 -msgid "Export" -msgstr "Exportieren" - -#: index.php:1492 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 -msgid "Import" -msgstr "Importieren" - -#: index.php:1502 -#, php-format -msgid "" -"The file you are trying to upload is probably bigger than what this " -"webserver can accept (%s). Please upload in smaller chunks." -msgstr "" -"Die Datei, die du hochladen möchtest, ist wahrscheinlich größer als das, was " -"dieser Webserver akzeptieren kann (%s). Bitte lade in kleineren Blöcken hoch." - -#: index.php:1541 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 -msgid "Plugin administration" -msgstr "Plugin Adminstration" - -#: index.php:1706 -msgid "Search: " -msgstr "Suche: " - -#: index.php:1933 -#, php-format -msgid "" -"
Sessions do not seem to work correctly on your server.
Make sure the " -"variable \"session.save_path\" is set correctly in your PHP config, and that " -"you have write access to it.
It currently points to %s.
On some " -"browsers, accessing your server via a hostname like 'localhost' or any " -"custom hostname without a dot causes cookie storage to fail. We recommend " -"accessing your server via it's IP address or Fully Qualified Domain Name.
" -msgstr "" -"
Sessions scheinen auf deinem Server nicht korrekt zu funktionieren. "
-"
Stelle sicher, dass die Variable \"session.save_path\" in deiner PHP-" -"Konfiguration richtig eingestellt ist und dass du Schreibzugriff darauf hast." -"
Es verweist aktuell auf %s.
Bei einigen Browsern führt der Zugriff " -"auf deinen Server über einen Hostnamen wie \"localhost\" oder einen " -"beliebigen benutzerdefinierten Hostnamen ohne Punkt dazu, dass der Cookie-" -"Speicher fehlschlägt. Wir empfehlen den Zugriff auf deinen Server über die " -"IP-Adresse oder den Fully Qualified Domain Namen.
" - -#: index.php:1943 -msgid "Click to try again." -msgstr "Klicke um es erneut zu versuchen." - -#: plugins/addlink_toolbar/addlink_toolbar.php:29 +#: plugins/addlink_toolbar/addlink_toolbar.php:31 msgid "URI" msgstr "URI" -#: plugins/addlink_toolbar/addlink_toolbar.php:33 -#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +#: plugins/addlink_toolbar/addlink_toolbar.php:35 +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 msgid "Add link" msgstr "Link hinzufügen" -#: plugins/addlink_toolbar/addlink_toolbar.php:50 +#: plugins/addlink_toolbar/addlink_toolbar.php:52 msgid "Adds the addlink input on the linklist page." msgstr "Fügt die Link-hinzufügen-Eingabe auf der Linkliste hinzu." -#: plugins/archiveorg/archiveorg.php:23 +#: plugins/archiveorg/archiveorg.php:29 msgid "View on archive.org" msgstr "Auf archive.org ansehen" -#: plugins/archiveorg/archiveorg.php:36 +#: plugins/archiveorg/archiveorg.php:42 msgid "For each link, add an Archive.org icon." msgstr "Füge für jeden Link ein Archive.org Symbol hinzu." -#: plugins/demo_plugin/demo_plugin.php:465 +#: plugins/default_colors/default_colors.php:38 +msgid "" +"Default colors plugin error: This plugin is active and no custom color is " +"configured." +msgstr "" +"Fehler beim Plugin für Standardfarben: Dieses Plugin ist aktiv und es ist " +"keine benutzerdefinierte Farbe konfiguriert." + +#: plugins/default_colors/default_colors.php:127 +msgid "Override default theme colors. Use any CSS valid color." +msgstr "Überschreibe Standard-Thema-Farben. Benutze jede gültige CSS Farbe." + +#: plugins/default_colors/default_colors.php:128 +msgid "Main color (navbar green)" +msgstr "Haupt-Farbe (navbar grün)" + +#: plugins/default_colors/default_colors.php:129 +msgid "Background color (light grey)" +msgstr "Hintergrund-Farbe (hellgrau)" + +#: plugins/default_colors/default_colors.php:130 +msgid "Dark main color (e.g. visited links)" +msgstr "Dunkle Haupt-Farbe (z. B. besuchte Links)" + +#: plugins/demo_plugin/demo_plugin.php:495 msgid "" "A demo plugin covering all use cases for template designers and plugin " "developers." @@ -459,7 +866,16 @@ msgstr "" "Ein Demo-Plugin, das alle Anwendungsfälle für Template-Designer und Plugin-" "Entwickler abdeckt." -#: plugins/isso/isso.php:20 +#: plugins/demo_plugin/demo_plugin.php:496 +msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed." +msgstr "" +"Dies ist ein Parameter, der dem Demo-Plugin gewidmet ist. Es wird angehängt." + +#: plugins/demo_plugin/demo_plugin.php:497 +msgid "Other demo parameter" +msgstr "Andere Demo-Parameter" + +#: plugins/isso/isso.php:22 msgid "" "Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin " "administration page." @@ -467,47 +883,17 @@ msgstr "" "Isso Plugin Fehler: Bitte definiere die Einstellung \"ISSO_SERVER\" auf der " "Plugin-Administrationsseite." -#: plugins/isso/isso.php:63 +#: plugins/isso/isso.php:92 msgid "Let visitor comment your shaares on permalinks with Isso." msgstr "" "Lassen Sie Besucher ihre geteilten Links auf Permalinks mit Isso " "kommentieren." -#: plugins/isso/isso.php:64 +#: plugins/isso/isso.php:93 msgid "Isso server URL (without 'http://')" msgstr "Isso Server URL (ohne 'http://')" -#: plugins/markdown/markdown.php:158 -msgid "Description will be rendered with" -msgstr "Die Beschreibung wird dargestellt mit" - -#: plugins/markdown/markdown.php:159 -msgid "Markdown syntax documentation" -msgstr "Markdown Syntax Dokumentation" - -#: plugins/markdown/markdown.php:160 -msgid "Markdown syntax" -msgstr "Markdown Syntax" - -#: plugins/markdown/markdown.php:339 -msgid "" -"Render shaare description with Markdown syntax.
Warning:\n" -"If your shaared descriptions contained HTML tags before enabling the " -"markdown plugin,\n" -"enabling it might break your page.\n" -"See the
README." -msgstr "" -"Übertrage Teilen Beschreibung mit Markdown-Syntax.
Warnung:\n" -"Wenn deine Teilen Beschreibungen HTML-Tags enthielten, bevor das Markdown-" -"Plugin aktiviert wurde,\n" -"kann es deine Seite beschädigen, solltest du es aktivieren.\n" -"Weitere Informationen findest du in der README." - -#: plugins/piwik/piwik.php:21 +#: plugins/piwik/piwik.php:24 msgid "" "Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin " "administration page." @@ -515,28 +901,28 @@ msgstr "" "Piwik-Plugin-Fehler: Bitte definiere die PIWIK_URL und PIWIK_SITEID auf der " "Plugin-Administrationsseite." -#: plugins/piwik/piwik.php:70 +#: plugins/piwik/piwik.php:73 msgid "A plugin that adds Piwik tracking code to Shaarli pages." msgstr "" "Ein Plugin, das einen Piwik-Tracking-Code auf Shaarli-Seiten hinzufügt." -#: plugins/piwik/piwik.php:71 +#: plugins/piwik/piwik.php:74 msgid "Piwik URL" msgstr "Piwik URL" -#: plugins/piwik/piwik.php:72 +#: plugins/piwik/piwik.php:75 msgid "Piwik site ID" msgstr "Piwik site ID" -#: plugins/playvideos/playvideos.php:22 +#: plugins/playvideos/playvideos.php:26 msgid "Video player" msgstr "Videoplayer" -#: plugins/playvideos/playvideos.php:25 +#: plugins/playvideos/playvideos.php:29 msgid "Play Videos" msgstr "Videos abspielen" -#: plugins/playvideos/playvideos.php:56 +#: plugins/playvideos/playvideos.php:60 msgid "Add a button in the toolbar allowing to watch all videos." msgstr "" "Fügt eine Schaltfläche in der Symbolleiste hinzu, mit der man alle Videos " @@ -546,30 +932,30 @@ msgstr "" msgid "plugins/playvideos/jquery-1.11.2.min.js" msgstr "plugins/playvideos/jquery-1.11.2.min.js" -#: plugins/pubsubhubbub/pubsubhubbub.php:69 +#: plugins/pubsubhubbub/pubsubhubbub.php:72 #, php-format msgid "Could not publish to PubSubHubbub: %s" msgstr "Veröffentlichung auf PubSubHubbub nicht möglich: %s" -#: plugins/pubsubhubbub/pubsubhubbub.php:95 +#: plugins/pubsubhubbub/pubsubhubbub.php:99 #, php-format msgid "Could not post to %s" msgstr "Kann nicht posten auf %s" -#: plugins/pubsubhubbub/pubsubhubbub.php:99 +#: plugins/pubsubhubbub/pubsubhubbub.php:103 #, php-format msgid "Bad response from the hub %s" msgstr "Ungültige Antwort vom Hub %s" -#: plugins/pubsubhubbub/pubsubhubbub.php:110 +#: plugins/pubsubhubbub/pubsubhubbub.php:114 msgid "Enable PubSubHubbub feed publishing." msgstr "Aktiviere PubSubHubbub Feed Veröffentlichung." -#: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68 +#: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72 msgid "For each link, add a QRCode icon." msgstr "Für jeden Link, füge eine QRCode Icon hinzu." -#: 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." @@ -577,23 +963,28 @@ msgstr "" "Wallabag Plugin Fehler: Bitte definiere die Einstellung \"WALLABAG_URL\" auf " "der Plugin Administrationsseite." -#: plugins/wallabag/wallabag.php:47 +#: plugins/wallabag/wallabag.php:49 msgid "Save to wallabag" msgstr "Auf Wallabag speichern" -#: plugins/wallabag/wallabag.php:69 +#: plugins/wallabag/wallabag.php:73 msgid "Wallabag API URL" msgstr "Wallabag API URL" -#: plugins/wallabag/wallabag.php:70 +#: plugins/wallabag/wallabag.php:74 msgid "Wallabag API version (1 or 2)" msgstr "Wallabag API version (1 oder 2)" #: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227 -#: tests/languages/fr/LanguagesFrTest.php:160 -#: tests/languages/fr/LanguagesFrTest.php:173 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:81 +#: tests/languages/fr/LanguagesFrTest.php:159 +#: tests/languages/fr/LanguagesFrTest.php:172 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:87 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:139 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 msgid "Search" msgid_plural "Search" msgstr[0] "Suche" @@ -607,6 +998,48 @@ msgstr "Entschuldige, hier gibt es nichts zu sehen." msgid "URL or leave empty to post a note" msgstr "URL oder leer lassen um eine Notiz hinzuzufügen" +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +msgid "BULK CREATION" +msgstr "Mehrfach-Erstellung" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +msgid "Metadata asynchronous retrieval is disabled." +msgstr "Der asynchrone Metadatenabruf ist deaktiviert." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +msgid "" +"We recommend that you enable the setting general > " +"enable_async_metadata in your configuration file to use bulk link " +"creation." +msgstr "" +"Es wird empfohlen, dass Du die Einstellung \"allgemein > " +"enable_async_metadata in Deiner Konfigurationsdatei aktivierst, um die " +"Massen-Linkerstellung verwenden zu können." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +msgid "Shaare multiple new links" +msgstr "Shaare mehrere neuen Links" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 +msgid "Add one URL per line to create multiple bookmarks." +msgstr "Füge eine URL pro Zeile hinzu, um mehrere Lesezeichen zu erstellen." + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +msgid "Tags" +msgstr "Tags" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +msgid "Private" +msgstr "Privat" + +#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 +msgid "Add links" +msgstr "Links hinzufügen" + #: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 msgid "Current password" msgstr "Aktuelles Passwort" @@ -633,23 +1066,48 @@ msgid "Case sensitive" msgstr "Groß- / Kleinschreibung-unterscheidend" #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 -msgid "Rename" -msgstr "Umbenennen" +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 +msgid "Rename tag" +msgstr "Tag umbenennen" #: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172 -msgid "Delete" -msgstr "Löschen" +msgid "Delete tag" +msgstr "Lösche Tag" -#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 msgid "You can also edit tags in the" msgstr "Du kannst auch Tags bearbeiten in der" -#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39 +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 msgid "tag list" msgstr "Tag Liste" +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +msgid "Change tags separator" +msgstr "Tags-Trennzeichen ändern" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 +msgid "Your current tag separator is" +msgstr "Ihr aktuelles Tag-Trennzeichen ist" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 +msgid "New separator" +msgstr "Neues Trennzeichen" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 +msgid "Save" +msgstr "Speichern" + +#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 +msgid "Note that hashtags won't fully work with a non-whitespace separator." +msgstr "" +"Beachten Sie, dass Hashtags nicht vollständig mit einem Nicht-" +"Leerraumtrennzeichen funktionieren." + #: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 msgid "title" msgstr "Titel" @@ -666,128 +1124,177 @@ msgstr "Standardwert" msgid "Theme" msgstr "Thema" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87 -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85 +msgid "Description formatter" +msgstr "Beschreibungsformatierer" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 msgid "Language" msgstr "Sprache" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116 -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 msgid "Timezone" msgstr "Zeitzone" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 msgid "Continent" msgstr "Kontinent" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 msgid "City" msgstr "Stadt" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191 msgid "Disable session cookie hijacking protection" msgstr "Deaktiviere Session Cookie Hijacking Schutz" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:193 msgid "Check this if you get disconnected or if your IP address changes often" msgstr "" "Überprüfe dies, wenn die Verbindung getrennt wird oder wenn sich deine IP-" "Adresse häufig ändert" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210 msgid "Private links by default" msgstr "Standardmäßig Private Links" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211 msgid "All new links are private by default" msgstr "Alle neuen Links sind standardmäßig privat" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226 msgid "RSS direct links" msgstr "RSS Direkt Links" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:200 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:227 msgid "Check this to use direct URL instead of permalink in feeds" msgstr "" "Aktivieren diese Option, um direkte URLs anstelle von Permalinks in Feeds zu " "verwenden" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242 msgid "Hide public links" msgstr "Verstecke öffentliche Links" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:216 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:243 msgid "Do not show any links if the user is not logged in" msgstr "Zeige keine Links, wenn der Benutzer nicht angemeldet ist" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231 -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:258 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:149 msgid "Check updates" msgstr "Auf Updates prüfen" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:232 -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:259 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151 msgid "Notify me when a new release is ready" msgstr "Benachrichtige mich, wenn eine neue Version zur Verfügung steht" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247 -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274 +msgid "Automatically retrieve description for new bookmarks" +msgstr "Automatisches Abrufen der Beschreibung für neue Lesezeichen" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275 +msgid "Shaarli will try to retrieve the description from meta HTML headers" +msgstr "Shaarli versucht, die Beschreibung aus Meta-HTML-Headern abzurufen" + +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 msgid "Enable REST API" msgstr "Aktiviere REST API" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248 -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:291 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 msgid "Allow third party software to use Shaarli such as mobile application" msgstr "" "Erlaube Software von Drittanbietern für Shaarli, wie z.B. die mobile " "Anwendung" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263 +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306 msgid "API secret" -msgstr "API secret" +msgstr "API-Geheimnis" -#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274 -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 -#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139 -#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199 -msgid "Save" -msgstr "Speichern" +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320 +msgid "Enable thumbnails" +msgstr "Aktivierte Thunbnails" -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 -msgid "The Daily Shaarli" -msgstr "Der tägliche Shaarli" +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324 +msgid "You need to enable the extension php-gd to use thumbnails." +msgstr "" +"Sie müssen die Erweiterung php-gd aktivieren, um " +"Miniaturansichten zu verwenden." -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 -msgid "1 RSS entry per day" -msgstr "1 RSS Eintrag pro Tag" +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 +msgid "Synchronize thumbnails" +msgstr "Thumbnails synchronisieren" -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 -msgid "Previous day" -msgstr "Vorheriger Tag" +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 +msgid "All" +msgstr "Alle" -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 -msgid "All links of one day in a single page." -msgstr "Alle Links eines Tages auf einer Seite." +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 +msgid "Only common media hosts" +msgstr "Nur gängige Medienhosts" -#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 -msgid "Next day" -msgstr "Nächster Tag" +#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347 +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 +msgid "None" +msgstr "Keine" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +msgid "1 RSS entry per :type" +msgid_plural "" +msgstr[0] "1 RSS Eintrag pro :type" +msgstr[1] "1 RSS Eintrag pro :type" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +msgid "Previous :type" +msgid_plural "" +msgstr[0] "Vorheriger :type" +msgstr[1] "Vorherige :type" + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56 +#: tmp/dailyrss.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 +msgid "All links of one :type in a single page." +msgid_plural "" +msgstr[0] "Alle Links eines :type auf einer Seite." +msgstr[1] "Alle Links aller :type auf einer Seite." + +#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +msgid "Next :type" +msgid_plural "" +msgstr[0] "Nächster :type" +msgstr[1] "Nächste :type" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +msgid "Edit Shaare" +msgstr "Bearbeite Shaare" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +msgid "New Shaare" +msgstr "Neue Shaare" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 msgid "Created:" msgstr "Erstellt:" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 msgid "URL" msgstr "URL" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 msgid "Title" msgstr "Titel" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75 #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99 @@ -795,41 +1302,56 @@ msgstr "Titel" msgid "Description" msgstr "Beschreibung" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 -msgid "Tags" -msgstr "Tags" +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 +msgid "Description will be rendered with" +msgstr "Beschreibung wird dargestellt mit" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59 -#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 -msgid "Private" -msgstr "Privat" +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91 +msgid "Markdown syntax documentation" +msgstr "Dokumentation der Markdown-Syntax" -#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +msgid "Markdown syntax" +msgstr "Markdown-Syntax" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115 +msgid "Cancel" +msgstr "Abbruch" + +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 msgid "Apply Changes" msgstr "Änderungen übernehmen" +#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:126 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +msgid "Delete" +msgstr "Löschen" + +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21 +#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +msgid "Save all" +msgstr "Speichere alles" + #: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 msgid "Export Database" msgstr "Exportiere Datenbank" -#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 msgid "Selection" msgstr "Beschreibung" -#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 -msgid "All" -msgstr "Alle" - -#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40 msgid "Public" msgstr "Öffentlich" -#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 msgid "Prepend note permalinks with this Shaarli instance's URL" msgstr "Voranstellen von Notizen-Permalinks mit der URL dieser Shaarli-Instanz" -#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 +#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52 msgid "Useful to import bookmarks in a web browser" msgstr "Sinnvoll Lesezeichen im Browser zu importieren" @@ -869,224 +1391,269 @@ msgstr "Duplikate basierend auf URL" msgid "Add default tags" msgstr "Standard-Tag hinzufügen" -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 -msgid "Install Shaarli" -msgstr "Installiere Shaarli" - #: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 msgid "It looks like it's the first time you run Shaarli. Please configure it." msgstr "" "Es sieht so aus, als ob du Shaarli das erste mal verwendest. Bitte " "konfiguriere es." -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33 -#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:167 msgid "Username" msgstr "Benutzername" -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 -#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:148 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:168 msgid "Password" msgstr "Passwort" -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:62 msgid "Shaarli title" msgstr "Shaarli Titel" -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68 msgid "My links" msgstr "Meine Links" -#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 msgid "Install" msgstr "Installiere" +#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190 +msgid "Server requirements" +msgstr "Server-Anforderungen" + #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79 msgid "shaare" msgid_plural "shaares" msgstr[0] "Teile" msgstr[1] "Teilen" #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 msgid "private link" msgid_plural "private links" msgstr[0] "Privater Link" msgstr[1] "Private Links" -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123 msgid "Search text" msgstr "Text durchsuchen" -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:130 #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 -#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65 #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74 msgid "Filter by tag" msgstr "Nach Tag filtern" -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110 msgid "Nothing found." msgstr "Nichts gefunden." -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118 #, php-format msgid "%s result" msgid_plural "%s results" msgstr[0] "%s Ergebnis" msgstr[1] "%s Ergebnisse" -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122 msgid "for" msgstr "für" -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 msgid "tagged" msgstr "markiert" +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133 #: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 msgid "Remove tag" msgstr "Tag entfernen" -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144 msgid "with status" msgstr "mit Status" -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155 msgid "without any tag" msgstr "ohne irgendeinen Tag" -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174 -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41 msgid "Fold" -msgstr "Ablegen" +msgstr "Einklappen" -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177 msgid "Edited: " msgstr "Bearbeitet: " -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181 msgid "permalink" msgstr "Permalink" -#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183 msgid "Add tag" msgstr "Tag hinzufügen" -#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7 -#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7 +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185 +msgid "Toggle sticky" +msgstr "Anheften umschalten" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187 +msgid "Sticky" +msgstr "Angeheftet" + +#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 +msgid "Share a private link" +msgstr "Teile einen privaten Link" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5 msgid "Filters" msgstr "Filter" -#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12 -#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12 +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:10 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:10 msgid "Only display private links" msgstr "Zeige nur private Links" -#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 -#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:15 +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:13 msgid "Only display public links" msgstr "Zeige nur öffentliche Links" -#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 -#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:20 +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18 msgid "Filter untagged links" msgstr "Unmarkierte Tags filtern" #: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24 -#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 #: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24 -#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76 -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 -msgid "Fold all" -msgstr "Alles ablegen" +msgid "Select all" +msgstr "Alle selektieren" -#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69 -#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:69 +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42 +msgid "Fold all" +msgstr "Alles einklappen" + +#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 +#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76 msgid "Links per page" msgstr "Links pro Seite" -#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 -msgid "" -"You have been banned after too many failed login attempts. Try again later." -msgstr "" -"Du wurdest nach zu vielen fehlgeschlagenen Anmeldeversuchen gesperrt. " -"Versuche es später noch einmal." - -#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151 +#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171 msgid "Remember me" msgstr "Erinnere dich an mich" -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 #: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:14 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 #: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48 msgid "by the Shaarli community" msgstr "von der Shaarli Community" -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:16 msgid "Documentation" msgstr "Dokumentation" -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43 msgid "Expand" msgstr "Erweitern" -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44 msgid "Expand all" msgstr "Alles erweitern" -#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 -#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45 msgid "Are you sure you want to delete this link?" msgstr "Bist du sicher das du diesen Link löschen möchtest?" -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:61 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:86 +#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46 +#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46 +msgid "Are you sure you want to delete this tag?" +msgstr "Bist du sicher das du diesen Tag löschen möchtest?" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11 +msgid "Menu" +msgstr "Menü" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:38 +#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Tag cloud" +msgstr "Tag-Cloud" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:67 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:92 msgid "RSS Feed" msgstr "RSS Feed" -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:66 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:102 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:72 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:108 msgid "Logout" msgstr "Ausloggen" -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152 +msgid "Set public" +msgstr "Setze Status auf Öffentlich" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157 +msgid "Set private" +msgstr "Setze Status auf Privat" + +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189 msgid "is available" msgstr "ist verfügbar" -#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176 -#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176 +#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:196 +#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196 msgid "Error" msgstr "Fehler" -#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15 +msgid "There is no cached thumbnail." +msgstr "Es gibt keine zwischengespeicherte Miniaturansicht / Thumbnail." + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17 +msgid "Try to synchronize them." +msgstr "Versuche sie zu synchronisieren." + +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 msgid "Picture Wall" msgstr "Bildwand" -#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 msgid "pics" msgstr "Bilder" @@ -1095,6 +1662,11 @@ msgid "You need to enable Javascript to change plugin loading order." msgstr "" "Du musst Javascript aktivieren um die Ladereihenfolge der Plugins zu ändern." +#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22 +msgid "Plugin administration" +msgstr "Plugin-Administration" + #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29 msgid "Enabled Plugins" msgstr "Aktivierte Plugins" @@ -1144,12 +1716,138 @@ msgstr "In der Dokumentation" #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150 msgid "Plugin configuration" -msgstr "Plugin Konfiguration" +msgstr "Plugin-Konfiguration" #: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195 msgid "No parameter available." msgstr "Kein Parameter verfügbar." +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16 +msgid "General" +msgstr "Allgemein" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20 +msgid "Index URL" +msgstr "Index-URL" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +msgid "Base path" +msgstr "Basispfad" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +msgid "Client IP" +msgstr "Client-IP" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44 +msgid "Trusted reverse proxies" +msgstr "Vertrauenswürdige Reverse-Proxies" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58 +msgid "N/A" +msgstr "n. a." + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67 +msgid "Version" +msgstr "Version" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 +msgid "Current version" +msgstr "Aktuelle Version" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80 +msgid "Latest release" +msgstr "Letzte Veröffentlichung" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84 +msgid "Visit releases page on Github" +msgstr "Besuche die Releases-/Veröffentlichungs-Seite bei Github" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +msgid "Thumbnails" +msgstr "Thumbnails / Miniaturbilder" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 +msgid "Thumbnails status" +msgstr "Thumbnails-Status" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121 +msgid "Synchronize all link thumbnails" +msgstr "Synchronisiere alle Link-Thumbnails" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:128 +msgid "Cache" +msgstr "Zwischenspeicher" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132 +msgid "Clear main cache" +msgstr "Haupt-Zwischenspeicher leeren" + +#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:138 +msgid "Clear thumbnails cache" +msgstr "Leere Thumbnail-Zwischenspeicher" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2 +msgid "Permissions" +msgstr "Berechtigungen" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8 +msgid "There are permissions that need to be fixed." +msgstr "Es gibt Berechtigungen, die korrigiert werden müssen." + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23 +msgid "All read/write permissions are properly set." +msgstr "Alle Lese-/Schreib-Berechtigungen sind richtig gesetzt." + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32 +msgid "Running PHP" +msgstr "Laufendes PHP" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36 +msgid "End of life: " +msgstr "Abgekündigt: " + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48 +msgid "Extension" +msgstr "Erweiterung" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49 +msgid "Usage" +msgstr "Benutzung" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50 +msgid "Status" +msgstr "Status" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51 +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:66 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66 +msgid "Loaded" +msgstr "Geladen" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 +msgid "Required" +msgstr "Erforderlich" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60 +msgid "Optional" +msgstr "optional" + +#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70 +#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70 +msgid "Not loaded" +msgstr "Nicht geladen" + #: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 #: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 msgid "tags" @@ -1160,6 +1858,10 @@ msgstr "Tags" msgid "List all links with those tags" msgstr "Zeige alle Links mit diesen Tags" +#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19 +msgid "Tag list" +msgstr "Tag Liste" + #: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3 #: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3 msgid "Sort by:" @@ -1196,15 +1898,19 @@ msgstr "Shaarli konfigurieren" msgid "Enable, disable and configure plugins" msgstr "Plugins aktivieren, deaktivieren und konfigurieren" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27 +msgid "Check instance's server configuration" +msgstr "Überprüfe die Server-Konfiguration dieser Instanz" + +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34 msgid "Change your password" msgstr "Ändere dein Passwort" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 msgid "Rename or delete a tag in all links" msgstr "Umbenennen oder löschen eines Tags in allen Links" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 msgid "" "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, " "delicious...)" @@ -1212,11 +1918,11 @@ msgstr "" "Importiere Netscape Lesezeichen (wie aus Firefox exportiert, Chrome, Opera, " "delicious...)" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 msgid "Import links" msgstr "Importiere Links" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53 msgid "" "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, " "Opera, delicious...)" @@ -1224,11 +1930,11 @@ msgstr "" "Exportiere Netscape HTML Lesezeichen (welche in Firefox importiert werden " "können, Chrome, Opera, delicious...)" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54 msgid "Export database" msgstr "Exportiere Datenbank" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 msgid "" "Drag one of these button to your bookmarks toolbar or right-click it and " "\"Bookmark This Link\"" @@ -1237,13 +1943,13 @@ msgstr "" "klicke mit der rechten Maustaste darauf und \"Speichere diesen Link als " "Lesezeichen\"" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78 msgid "then click on the bookmarklet in any page you want to share." msgstr "" "Klicke dann auf das Bookmarklet auf jeder Seite, welches du teilen möchtest." -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106 msgid "" "Drag this link to your bookmarks toolbar or right-click it and Bookmark This " "Link" @@ -1251,22 +1957,22 @@ msgstr "" "Ziehe diese Link in deine Lesezeichen-Symbolleiste oder klicke mit der " "rechten Maustaste darauf und \"Speichere diesen Link als Lesezeichen\"" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83 msgid "then click ✚Shaare link button in any page you want to share" msgstr "" "klicke dann auf die Schaltfläche ✚Teilen auf jeder Seite, die du teilen " "möchtest" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114 msgid "The selected text is too long, it will be truncated." msgstr "Der ausgewählte Text ist zu lang, er wird gekürzt." -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102 msgid "Shaare link" msgstr "Teile Link" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107 msgid "" "Then click ✚Add Note button anytime to start composing a private Note (text " "post) to your Shaarli" @@ -1274,40 +1980,42 @@ msgstr "" "Klicke auf ✚Notiz hinzufügen um eine private Notiz (Textnachricht) zu " "Shaarli hinzuzufügen" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123 msgid "Add Note" msgstr "Notiz hinzufügen" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129 -msgid "" -"You need to browse your Shaarli over HTTPS to use this " -"functionality." -msgstr "" -"Um diese Funktion nutzen zu können, musst du Shaarli über HTTPS aufrufen." - -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134 -msgid "Add to" -msgstr "Hinzufügen zu" - -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132 msgid "3rd party" msgstr "Von Dritten" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153 -msgid "Plugin" -msgstr "Plugin" - -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148 -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140 msgid "plugin" msgstr "Plugin" -#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175 +#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165 msgid "" "Drag this link to your bookmarks toolbar, or right-click it and choose " "Bookmark This Link" msgstr "" "Ziehe diesen Link in deine Lesezeichen-Symbolleiste oder klicke mit der " "rechten Maustaste darauf und wähle \"Speichere diesen Link als Lesezeichen\"" + +#~ msgid "Rename" +#~ msgstr "Umbenennen" + +#~ msgid "The Daily Shaarli" +#~ msgstr "Der tägliche Shaarli" + +#~ msgid "" +#~ "You need to browse your Shaarli over HTTPS to use this " +#~ "functionality." +#~ msgstr "" +#~ "Um diese Funktion nutzen zu können, musst du Shaarli über HTTPS aufrufen." + +#~ msgid "Add to" +#~ msgstr "Hinzufügen zu" + +#~ msgid "Plugin" +#~ msgstr "Plugin" diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po index 26dede4e..01492af4 100644 --- a/inc/languages/fr/LC_MESSAGES/shaarli.po +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -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 "" "
Sessions do not seem to work correctly on your server.
Make sure the " @@ -559,14 +559,14 @@ msgstr "" "des cookies. Nous vous recommandons d'accéder à votre serveur depuis son " "adresse IP ou un Fully Qualified Domain Name.
" -#: 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 "" msgstr "" @@ -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)" diff --git a/inc/languages/jp/LC_MESSAGES/shaarli.po b/inc/languages/jp/LC_MESSAGES/shaarli.po index 57f42fc2..d5a83341 100644 --- a/inc/languages/jp/LC_MESSAGES/shaarli.po +++ b/inc/languages/jp/LC_MESSAGES/shaarli.po @@ -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 \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 diff --git a/inc/languages/ru/LC_MESSAGES/shaarli.po b/inc/languages/ru/LC_MESSAGES/shaarli.po old mode 100755 new mode 100644 diff --git a/index.php b/index.php index 1eb7659a..862c53ef 100644 --- a/index.php +++ b/index.php @@ -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 () { diff --git a/phpcs.xml b/phpcs.xml index c559e35d..9bdc8720 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -18,5 +18,6 @@ index.php + plugins/* diff --git a/plugins/default_colors/default_colors.php b/plugins/default_colors/default_colors.php index 574a0bd4..d3e1fa76 100644 --- a/plugins/default_colors/default_colors.php +++ b/plugins/default_colors/default_colors.php @@ -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. * diff --git a/plugins/demo_plugin/DemoPluginController.php b/plugins/demo_plugin/DemoPluginController.php new file mode 100644 index 00000000..b8ace9c8 --- /dev/null +++ b/plugins/demo_plugin/DemoPluginController.php @@ -0,0 +1,24 @@ +assignView( + 'content', + '
' . + 'This is a demo page. I have access to Shaarli container, so I\'m free to do whatever I want here.' . + '
' + ); + + return $response->write($this->render('pluginscontent')); + } +} diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php index 22d27b68..d89765cf 100644 --- a/plugins/demo_plugin/demo_plugin.php +++ b/plugins/demo_plugin/demo_plugin.php @@ -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'][] = '
'; 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. */ diff --git a/tests/PluginManagerTest.php b/tests/PluginManagerTest.php index efef5e87..75b3ae00 100644 --- a/tests/PluginManagerTest.php +++ b/tests/PluginManagerTest.php @@ -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])); + } } diff --git a/tests/api/ApiMiddlewareTest.php b/tests/api/ApiMiddlewareTest.php index 86700840..2afac28b 100644 --- a/tests/api/ApiMiddlewareTest.php +++ b/tests/api/ApiMiddlewareTest.php @@ -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); } /** diff --git a/tests/api/controllers/info/InfoTest.php b/tests/api/controllers/info/InfoTest.php index 10b29ab2..2428ca43 100644 --- a/tests/api/controllers/info/InfoTest.php +++ b/tests/api/controllers/info/InfoTest.php @@ -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); diff --git a/tests/api/controllers/links/DeleteLinkTest.php b/tests/api/controllers/links/DeleteLinkTest.php index 805c9be3..dc2cf917 100644 --- a/tests/api/controllers/links/DeleteLinkTest.php +++ b/tests/api/controllers/links/DeleteLinkTest.php @@ -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]; diff --git a/tests/api/controllers/links/GetLinkIdTest.php b/tests/api/controllers/links/GetLinkIdTest.php index 1ec56ef3..c93a3b4b 100644 --- a/tests/api/controllers/links/GetLinkIdTest.php +++ b/tests/api/controllers/links/GetLinkIdTest.php @@ -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); diff --git a/tests/api/controllers/links/GetLinksTest.php b/tests/api/controllers/links/GetLinksTest.php index b1c46ee2..3c966732 100644 --- a/tests/api/controllers/links/GetLinksTest.php +++ b/tests/api/controllers/links/GetLinksTest.php @@ -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); diff --git a/tests/api/controllers/links/PostLinkTest.php b/tests/api/controllers/links/PostLinkTest.php index e12f803b..a54e4a16 100644 --- a/tests/api/controllers/links/PostLinkTest.php +++ b/tests/api/controllers/links/PostLinkTest.php @@ -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']); + } } diff --git a/tests/api/controllers/links/PutLinkTest.php b/tests/api/controllers/links/PutLinkTest.php index 240ee323..ed14d5f8 100644 --- a/tests/api/controllers/links/PutLinkTest.php +++ b/tests/api/controllers/links/PutLinkTest.php @@ -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']); + } } diff --git a/tests/api/controllers/tags/DeleteTagTest.php b/tests/api/controllers/tags/DeleteTagTest.php index 37f07229..c0f8a6a9 100644 --- a/tests/api/controllers/tags/DeleteTagTest.php +++ b/tests/api/controllers/tags/DeleteTagTest.php @@ -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); diff --git a/tests/api/controllers/tags/GetTagNameTest.php b/tests/api/controllers/tags/GetTagNameTest.php index 878de5a4..0ad71495 100644 --- a/tests/api/controllers/tags/GetTagNameTest.php +++ b/tests/api/controllers/tags/GetTagNameTest.php @@ -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); diff --git a/tests/api/controllers/tags/GetTagsTest.php b/tests/api/controllers/tags/GetTagsTest.php index b565a8c4..a4b62c51 100644 --- a/tests/api/controllers/tags/GetTagsTest.php +++ b/tests/api/controllers/tags/GetTagsTest.php @@ -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; diff --git a/tests/api/controllers/tags/PutTagTest.php b/tests/api/controllers/tags/PutTagTest.php index c73f6d3b..045473e6 100644 --- a/tests/api/controllers/tags/PutTagTest.php +++ b/tests/api/controllers/tags/PutTagTest.php @@ -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; diff --git a/tests/bookmark/BookmarkFileServiceTest.php b/tests/bookmark/BookmarkFileServiceTest.php index f619aff3..1d250719 100644 --- a/tests/bookmark/BookmarkFileServiceTest.php +++ b/tests/bookmark/BookmarkFileServiceTest.php @@ -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(); diff --git a/tests/bookmark/BookmarkFilterTest.php b/tests/bookmark/BookmarkFilterTest.php index 835674f2..79be807d 100644 --- a/tests/bookmark/BookmarkFilterTest.php +++ b/tests/bookmark/BookmarkFilterTest.php @@ -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 */ diff --git a/tests/bookmark/BookmarkInitializerTest.php b/tests/bookmark/BookmarkInitializerTest.php index 0c8420ce..351807c1 100644 --- a/tests/bookmark/BookmarkInitializerTest.php +++ b/tests/bookmark/BookmarkInitializerTest.php @@ -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(); diff --git a/tests/bookmark/LinkUtilsTest.php b/tests/bookmark/LinkUtilsTest.php index ddab4e3c..46a7f1fe 100644 --- a/tests/bookmark/LinkUtilsTest.php +++ b/tests/bookmark/LinkUtilsTest.php @@ -245,6 +245,16 @@ public function testHtmlExtractNonExistentOgTag() $this->assertFalse(html_extract_tag('description', $html)); } + public function testHtmlExtractDescriptionFromGoogleRealCase(): void + { + $html = 'id="gsr">'. + ''. + 'assertSame('Bonnes fêtes de fin d\'année ! #GoogleDoodle', html_extract_tag('description', $html)); + } + /** * Test the header callback with valid value */ diff --git a/tests/bookmark/SearchResultTest.php b/tests/bookmark/SearchResultTest.php new file mode 100644 index 00000000..12854c1f --- /dev/null +++ b/tests/bookmark/SearchResultTest.php @@ -0,0 +1,125 @@ +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()); + } +} diff --git a/tests/container/ContainerBuilderTest.php b/tests/container/ContainerBuilderTest.php index 3d43c344..04d4ef01 100644 --- a/tests/container/ContainerBuilderTest.php +++ b/tests/container/ContainerBuilderTest.php @@ -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) ); } diff --git a/tests/feed/CachedPageTest.php b/tests/feed/CachedPageTest.php index 904db9dc..1decfaf3 100644 --- a/tests/feed/CachedPageTest.php +++ b/tests/feed/CachedPageTest.php @@ -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('

Some content

'); @@ -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('

Some content

'); @@ -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('

Some content

'); @@ -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('

Some content

'); + $this->assertFileExists(self::$filename); + $this->assertEquals( + '

Some content

', + $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('

Some content

'); + $this->assertFileExists(self::$filename); + $this->assertNull($page->cachedVersion()); + } } diff --git a/tests/feed/FeedBuilderTest.php b/tests/feed/FeedBuilderTest.php index 6b9204eb..fe092f78 100644 --- a/tests/feed/FeedBuilderTest.php +++ b/tests/feed/FeedBuilderTest.php @@ -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', diff --git a/tests/formatter/BookmarkDefaultFormatterTest.php b/tests/formatter/BookmarkDefaultFormatterTest.php index 4fcc5dd1..983960b6 100644 --- a/tests/formatter/BookmarkDefaultFormatterTest.php +++ b/tests/formatter/BookmarkDefaultFormatterTest.php @@ -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( 'This guide extends and expands on PSR-1, the ' . 'basic coding ' . - 'standard.', + 'standard.
' . PHP_EOL . + '' . + 'https://www.php-fig.org/psr/psr-1/' . + '', $link['description'] ); } diff --git a/tests/formatter/BookmarkMarkdownFormatterTest.php b/tests/formatter/BookmarkMarkdownFormatterTest.php index ab6b4080..32f7b444 100644 --- a/tests/formatter/BookmarkMarkdownFormatterTest.php +++ b/tests/formatter/BookmarkMarkdownFormatterTest.php @@ -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 description'. 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 = '

'; + $description .= 'This a <strong>description</strong>
' . + PHP_EOL; + $url = 'https://sub.domain.tld?query=here&for=real#hash'; + $highlighted = 'https://sub.domain.tld'; + $highlighted .= '?query=here&for=real#hash'; + $description .= 'text '. $highlighted .' more text
'. PHP_EOL; + $description .= 'Also, there is an #hasht' . + 'ag added
'. PHP_EOL; + $description .= 'A N D KEEP SPACES !
' . PHP_EOL; + $description .= 'And ' . + 'yet another link'; + $description .= '

'; + + $this->assertEquals($description, $link['description']); + } + /** * Test formatting URL with an index_url set * It should prepend relative links. diff --git a/tests/front/controller/admin/ConfigureControllerTest.php b/tests/front/controller/admin/ConfigureControllerTest.php index d82db0a7..13644df9 100644 --- a/tests/front/controller/admin/ConfigureControllerTest.php +++ b/tests/front/controller/admin/ConfigureControllerTest.php @@ -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']); } diff --git a/tests/front/controller/admin/ManageTagControllerTest.php b/tests/front/controller/admin/ManageTagControllerTest.php index af6f273f..56a64cbb 100644 --- a/tests/front/controller/admin/ManageTagControllerTest.php +++ b/tests/front/controller/admin/ManageTagControllerTest.php @@ -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 diff --git a/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php index a276d988..42d0c0d6 100644 --- a/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php +++ b/tests/front/controller/admin/ShaareManageControllerTest/DeleteBookmarkTest.php @@ -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('', (string) $result->getBody('location')); + static::assertSame('', (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()); } } diff --git a/tests/front/controller/admin/ThumbnailsControllerTest.php b/tests/front/controller/admin/ThumbnailsControllerTest.php index e5749654..0c9b63c3 100644 --- a/tests/front/controller/admin/ThumbnailsControllerTest.php +++ b/tests/front/controller/admin/ThumbnailsControllerTest.php @@ -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); diff --git a/tests/front/controller/visitor/BookmarkListControllerTest.php b/tests/front/controller/visitor/BookmarkListControllerTest.php index dec938f2..0fbab9d4 100644 --- a/tests/front/controller/visitor/BookmarkListControllerTest.php +++ b/tests/front/controller/visitor/BookmarkListControllerTest.php @@ -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)) diff --git a/tests/front/controller/visitor/DailyControllerTest.php b/tests/front/controller/visitor/DailyControllerTest.php index 70fbce54..821ba321 100644 --- a/tests/front/controller/visitor/DailyControllerTest.php +++ b/tests/front/controller/visitor/DailyControllerTest.php @@ -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 = []; diff --git a/tests/front/controller/visitor/PictureWallControllerTest.php b/tests/front/controller/visitor/PictureWallControllerTest.php index b868231d..429e99a2 100644 --- a/tests/front/controller/visitor/PictureWallControllerTest.php +++ b/tests/front/controller/visitor/PictureWallControllerTest.php @@ -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'), - ]; + ]); } }) ; diff --git a/tests/front/controller/visitor/ShaarliVisitorControllerTest.php b/tests/front/controller/visitor/ShaarliVisitorControllerTest.php index 935ec24e..7676f14d 100644 --- a/tests/front/controller/visitor/ShaarliVisitorControllerTest.php +++ b/tests/front/controller/visitor/ShaarliVisitorControllerTest.php @@ -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']); diff --git a/tests/helper/DailyPageHelperTest.php b/tests/helper/DailyPageHelperTest.php index 5255b7b1..2d745800 100644 --- a/tests/helper/DailyPageHelperTest.php +++ b/tests/helper/DailyPageHelperTest.php @@ -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'), + ], + ]; + } } diff --git a/tests/http/MetadataRetrieverTest.php b/tests/http/MetadataRetrieverTest.php index 3c9eaa0e..cae65091 100644 --- a/tests/http/MetadataRetrieverTest.php +++ b/tests/http/MetadataRetrieverTest.php @@ -41,7 +41,7 @@ public function testFullRetrieval(): void $remoteCharset = 'utf-8'; $expectedResult = [ - 'title' => $remoteTitle, + 'title' => trim($remoteTitle), 'description' => $remoteDesc, 'tags' => $remoteTags, ]; diff --git a/tests/netscape/BookmarkExportTest.php b/tests/netscape/BookmarkExportTest.php index ad288f78..b8a88cd8 100644 --- a/tests/netscape/BookmarkExportTest.php +++ b/tests/netscape/BookmarkExportTest.php @@ -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'); } diff --git a/tests/netscape/BookmarkImportTest.php b/tests/netscape/BookmarkImportTest.php index 6856ebca..ecd33ea1 100644 --- a/tests/netscape/BookmarkImportTest.php +++ b/tests/netscape/BookmarkImportTest.php @@ -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); } diff --git a/tests/plugins/PluginDefaultColorsTest.php b/tests/plugins/PluginDefaultColorsTest.php index cc844c60..54e97612 100644 --- a/tests/plugins/PluginDefaultColorsTest.php +++ b/tests/plugins/PluginDefaultColorsTest.php @@ -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); + } } diff --git a/tests/plugins/test/test.php b/tests/plugins/test/test.php index 03be4f4e..8dbb3f94 100644 --- a/tests/plugins/test/test.php +++ b/tests/plugins/test/test.php @@ -1,5 +1,7 @@ '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']; +} diff --git a/tests/plugins/test_route_invalid/test_route_invalid.php b/tests/plugins/test_route_invalid/test_route_invalid.php new file mode 100644 index 00000000..0c5a5101 --- /dev/null +++ b/tests/plugins/test_route_invalid/test_route_invalid.php @@ -0,0 +1,12 @@ + 'GET', + 'route' => 'not a route', + 'callable' => 'getFunction', + ], + ]; +} diff --git a/tests/updater/UpdaterTest.php b/tests/updater/UpdaterTest.php index cadd8265..a8539d63 100644 --- a/tests/updater/UpdaterTest.php +++ b/tests/updater/UpdaterTest.php @@ -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); } diff --git a/tpl/default/editlink.batch.html b/tpl/default/editlink.batch.html index b1f8e5bd..973a5ccc 100644 --- a/tpl/default/editlink.batch.html +++ b/tpl/default/editlink.batch.html @@ -20,6 +20,7 @@ {loop="$links"} + {$batchId=$key} {include="editlink"} {/loop} diff --git a/tpl/default/editlink.html b/tpl/default/editlink.html index 83e541fd..a5828c75 100644 --- a/tpl/default/editlink.html +++ b/tpl/default/editlink.html @@ -1,3 +1,4 @@ +{$batchId=isset($batchId) ? $batchId : ''} {if="empty($batch_mode)"} @@ -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} -