4 Alexandre Alapetite
+ 4 yude
4 David Sferruzza
- 4 Immánuel Fodor
- 3 Agurato
3 Teromene
- 2 Alexandre G.-Raymond
- 2 Chris Kuethe
+ 3 yudete
+ 3 Agurato
+ 3 Olivier
+ 3 Christoph Stoettner
2 Felix Bartels
- 2 Knah Tsaeb
- 2 Luce Carević
2 Mathieu Chabanon
2 Miloš Jovanović
+ 2 Neros
+ 2 Alexandre G.-Raymond
2 Qwerty
+ 2 Guillaume Virlet
+ 2 Sebastien Wains
2 Stephen Muth
2 Timo Van Neerden
+ 2 Alexander Railean
+ 2 Doug Breaux <25640850+dougbreaux@users.noreply.github.com>
+ 2 flow.gunso
+ 2 Chris Kuethe
+ 2 Ganesh Kandu
2 julienCXX
+ 2 Knah Tsaeb
2 philipp-r
2 pips
+ 2 prog-it
2 trailjeep
- 1 Adrien Oliva
+ 1 leyrer
+ 1 locness3 <37651007+locness3@users.noreply.github.com>
+ 1 owen bell <66233223+xfnw@users.noreply.github.com>
+ 1 philipp
+ 1 rfolo9li <50079896+rfolo9li@users.noreply.github.com>
+ 1 sprak3000
+ 1 yudejp
+ 1 Rajat Hans
1 Adrien le Maire
+ 1 Ajabep
1 Alexis J
1 Angristan
1 Bish Erbas <42714627+bisherbas@users.noreply.github.com>
1 BoboTiG
+ 1 Brendan M. Sleight
1 Bronco
1 Buster One <37770318+buster-one@users.noreply.github.com>
1 D Low
1 Daniel Jakots
+ 1 David Foucher
+ 1 Denis Renning
1 Dennis Verspuij
1 Dimtion
1 Fanch
@@ -49,19 +75,31 @@
1 Florian Voigt
1 Franck Kerbiriou
1 Gary Marigliano
- 1 Guillaume Virlet
+ 1 Gregory
+ 1 Hazhar Galeh <78073762+hazhargaleh@users.noreply.github.com>
+ 1 Hg
+ 1 Jens Kubieziel
1 Jonathan Amiez
1 Jonathan Druart
1 Julien Pivotto
1 Kevin Canévet
+ 1 Kevin Masson
1 Knah Tsaeb
1 Lionel Martin
+ 1 Loïc Carr
1 Mark Gerarts
1 Marsup
- 1 Neros
- 1 Rajat Hans
+ 1 Nicolas Friedli
+ 1 Paul van den Burg
+ 1 Adrien Oliva
1 Sbgodin
+ 1 ToM
1 TsT
+ 1 agentcobra
+ 1 aguy
+ 1 bschwede
1 dimtion
1 durcheinandr
+ 1 heimpogo
+ 1 jalr
1 lapineige
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7398ca19..334ee849 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,44 +4,234 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
-<<<<<<< HEAD
-## [v0.10.4](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4) - 2019-04-16
+## [v0.12.2](https://github.com/shaarli/Shaarli/releases/tag/v0.12.2) - 2023-03-18
-### Fixed
-- Fix thumbnails disabling if PHP GD is not installed
-- Fix a warning if links sticky status isn't set
+> Docker: use `ghcr.io/shaarli/shaarli` as Docker image instead of `shaarli/shaarli`.
+> The `:master` Docker image has been removed, please use `:latest` instead.
+> The `:stable` Docker image has been removed, please use `:release` instead.
+
+## Added
+
+- Bulk action: add or delete tag to multiple bookmarks
+- New Core Plugin: ReadItLater
+- Plugin system: allow plugins to provide custom routes
+- Support search highlights when matching URL content
+- Support for OR (~) and optional AND (+) operators for tag search
+- Russian translation
+- Chinese translation
+- Export:
+ - Export: set a bookmark's LAST_MODIFIED attribute to its update timestamp
+ - Export: set a bookmark's PRIVATE attribute using an integer value
+- Add an additional free disk space check before saving the datastore
+- curl: support HTTP/2 response code header
+- CI:
+ - Build and push Docker images through Github Actions
+ - push container images to github registry in addition to dockerhub
+- Documentation:
+ - Add '206 not acceptable' to the Troubleshooting section
+ - Add mention to Shaarli Archiver
+ - doc: add note to adjust proxy timeouts or PHP max execution time
+ - doc: shaarli configuration: mention file:/// URIs
+ - add "formatter" key to example config.json.php
+
+## Changed
+
+- docker latest: replace dev in shaarli_version.php with the latest commit
+- Daily RSS Cache: invalidate cache base on the date
+- Update Japanese translations
+- Update German translations
+- Templates: Inject current template name
+- format_date: include timezone in IntlDateFormatter object
+- Handle pagination through BookmarkService
+- autocapitalize off for username input
+- More intuitive label for plugin checkboxes
+- Simple and uniform localized website title
+- Use rewrited version of Netscape Bookmark Parser
+- tests/makefile: rewrite translate target to be compatible with busybox
+- PubSubHub Plugin: make 1 external call per request
+- Docker:
+ - newer alpine (for newer PHP) and apk upgrade
+ - Dockerfile.armhf: upgrade python2 -> python3
+ - Dockerfile: add php8-gettext package
+ - update s6 service definition to use php-fpm8
+ - install php8-ldap in Docker images
+- CI:
+ - use Github Action instead of Travis CI
+ - use the yarnpkg command instead of yarn
+ - tools: github actions: fix PHP 8.0 tests
+ - github actions: add tests for PHP 8.2
+- Documentation:
+ - apache: explicitely ste index.php as DirectoryIndex
+ - bookmarklet is now working on github.com
+ - LDAP login support, update php requirements list
+ - installation/tests: clarify build tools installation procedure
+ - doc: PHP extensions are also required for development
+ - doc: move OCI images hosting to ghcr.io
+
+## Fixed
+
+- Error handling if the datastore mutex is not working
+- Synchronous metadata retrieval is failing in strict mode
+- Improve metadata extraction
+- Typo: 'Authentication' ->
+- default_colors plugin: update CSS file on color change
+- API: POST/PUT Link - properly parse tags string
+- Error when using bulk shaare with a single URL
+- Bulk Shaare:
+ - use unique HTML ID
+ - error with a single URL
+ - redirection with ending slash
+- Bug when trying to access ATOM feed without bookmarks
+- Documentation build
+- pubsubhubbub hub link in RSS / Atom.
+- Monthly views previous/next month links during month
+- Resolve PHP 8.1 deprecation warnings
+- Fix PHP 8 incompatibility with debug mode enabled
+- Fixed Roboto-Regular and Roboto-Bold font declarations
+- template/vintage: fix typo in visibility selection link
+- Do not display deprecated warnings by default
+- Fix a bug when using '/' as a tag separator
+- Fix Logger exception: gracefully handle permission issue
+- Documentation:
+ - plugins.md: fix link casing
+
+## Removed
+
+- Daily RSS: Remove relative description (today, yesterday)
+- Documentation:
+ - remove the markdown plugin from the plugins list
+ - remove duplicate "general" key in example config.php.json
+
+## [v0.12.1](https://github.com/shaarli/Shaarli/releases/tag/v0.12.1) - 2020-11-12
+
+> nginx ([#1628](https://github.com/shaarli/Shaarli/pull/1628)) and Apache ([#1630](https://github.com/shaarli/Shaarli/pull/1630)) configurations have been reviewed. It is recommended that you
+> update yours using [the documentation](https://shaarli.readthedocs.io/en/master/Server-configuration/).
+> Users using official Docker image will receive updated configuration automatically.
-## [v0.10.3](https://github.com/shaarli/Shaarli/releases/tag/v0.10.3) - 2019-02-23
### Added
-- Add OpenGraph metadata tags on permalink page
-- Add CORS headers to REST API reponses
-- Add a button to toggle checkboxes of displayed links
-- Add an icon to the link list when the Isso plugin is enabled
-- Add noindex, nofollow to documentation pages
-- Document usage of robots.txt
-- Add a button to set links as sticky
+- Bulk creation of bookmarks
+- Server administration tool page (and install page requirements)
+- Support any tag separator, not just whitespaces
+- Share a private bookmark using a URL with a token
+- Add a setting to retrieve bookmark metadata asynchronously (enabled by default)
+- Highlight fulltext search results
+- Weekly and monthly view/RSS feed for daily page
+- MarkdownExtra formatter
+- Default formatter: add a setting to disable auto-linkification
+- Add mutex on datastore I/O operations to prevent data loss
+- PHP 8.0 support
+- REST API: allow override of creation and update dates
+- Add strict types for bookmarks management
### Changed
-- Update French translation
-- Refactor the documentation homepage
-- Bump netscape-bookmark-parser
-- Update session_start condition
-- Improve accessibility
-- Cleanup and refactor lint tooling
+- Improve regex and performances to extract HTML metadata (title, description, etc.)
+- Support using Shaarli without URL rewriting (prefix URL with `/index.php/`)
+- Improve the "Manage tags" tools page
+- Use PSR-3 logger for login attempts
+- Move utils classes to Shaarli\Helper namespace and folder
+- Include php-simplexml in Docker image
+- Raise 404 error instead of 500 if permalink access is denied
+- Display error details even with dev.debug set to false
+- Reviewed nginx configuration
+- Reviewed Apache configuration
+- Replace vimeo link in demo bookmarks due to IP ban on the demo instance
+- Apply PSR-12 on code base, and add CI check using PHPCS
### Fixed
-- Fix input size for dropdown search form
-- Fix history for bulk link deletion
-- Fix thumbnail requests
-- Fix hashtag rendering when markdown escaping is enabled
-- Fix AJAX tag deletion
-- Fix lint errors and improve PSR-1 and PSR-2 compliance
+- Compatiliby issue on login with PHP 7.1
+- Japanese translations update
+- Redirect to referrer after bookmark deletion
+- Inject ROOT_PATH in plugin instead of regenerating it everywhere
+- Wallabag plugin: minor improvements
+- REST API postLink: change relative path to absolute path
+- Webpack: fix vintage theme images include
+- Docker-compose: fix SSL certificate + add parameter for Docker tag
### Removed
-- Remove Firefox Share documentation
+- `config.json.php` new lines in prefix/suffix to prevent issues with Windows PHP
+
+## [v0.12.0](https://github.com/shaarli/Shaarli/releases/tag/v0.12.0) - 2020-10-13
+
+**Save you `data/` folder before updating!**
+
+### Added
+- Thumbnailer: add soundcloud.com to list of common media domains
+- Markdown rendering is now integrated into Shaarli core
+- Add autofocus on tag cloud filter input
+- Japanese translations
+- Japanese translation: add language to admin configuration page
+- Support for PHP 8.0
+- Support for local anchor URL (starting with `#`)
+- LDAP authentication
+- Encapsulated PageCacheManager
+- Docs:
+ - add screenshots of all pages
+ - section about mkdocs
+ - Ulauncher extension
+- CI: run against PHP 7.4
+- Added $links_per_page variable to template and display on default
+- Inject BookmarkServiceInterface in plugins data
+- Add manual configuration for root URL
+- Added PATCH to the allowed Apache request methods.
+- REST API: compatibility with ionos Apache's headers
+
+### Changed
+- Introduce Bookmark object and Service layer
+ - Save bookmark as objects in the datastore
+ - Handle bookmark as objects across the whole codebase (except templates and plugins)
+- Process all Shaarli page through Slim controller, with proper URL rewriting (see #1516)
+- Docs: the entire documentation has been reviewed, updated and improved, thanks to @nodiscc!
+- ATOM feed: use instance name as author name instead of URL
+- Updated French translation
+- Default colors plugin: generate CSS file during initialization
+- Improve default bookmarks after install
+- Upgrade all front end dependencies and webpack build
+- Default theme: Make tag cloud/list views buttons more obvious
+
+### Fixed
+- Undefined index: thumbnail in daily page
+- Undefined index: thumbnail on OpenGraph headers
+- Undefined index: updated on linklist
+- Make sure that bookmark sort is consistent, even with equal timestamps
+- Code PHP version check as requirement bumped to PHP 7.1
+- Thumbnail images lazy loading
+- Markdown plugin: fix RSS feed direct link reverse
+- Fix RSS permalink included in Markdown bloc
+- Demo plugin: multiple typos
+- Makefile target for releases
+- Makefile target for html documentation
+- Session cookie setting being set while session is active
+- Deprecated use of implode
+- Division by zero in tag cloud
+- CI: deprecated linux distribution and sudo directive
+- Docker build: gcc is no longer included in python alpine image
+- Default template: display pin button in mobile view
+- Pinned bookmarks are not longer displayed first in ATOM/RSS feeds
+- Docs:
+ - Outdated Docker documentation for stable branch
+ - Outdated links
+ - Plugin description in meta files
+- docker-compose.yml: pin traefik image to 1.7-alpine
+
+### Removed
+- Markdown plugin
+- Docs:
+ - emojione & twemoji removed
+- Makefile: remove static_analysis_summary from all: target
+- doc/Makefile: remove references to composer update
+
+## [v0.11.1](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1) - 2019-08-03
+
+Release to fix broken Docker build on the latest version.
+
+### Fixed
+- Fixed Docker build
+- Fixed a few documentation broken links
+- Fixed broken label in configuration page
+
+### Added
+- More accessibility improvements
-||||||| merged common ancestors
-=======
## [v0.11.0](https://github.com/shaarli/Shaarli/releases/tag/v0.11.0) - 2019-07-27
**Shaarli no longer officially support PHP 5.6 and PHP 7.0 as they've reached end of life.**
@@ -122,7 +312,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Removed
- Remove Firefox Share documentation
->>>>>>> v0.11.0
## [v0.10.2](https://github.com/shaarli/Shaarli/releases/tag/v0.10.2) - 2018-08-11
### Fixed
@@ -366,7 +555,7 @@ configuration to enable URL rewriting, see:
- `/api/v1/info`: get general information on the Shaarli instance
- `/api/v1/links`: get a list of shaared links
- `/api/v1/history`: get a list of latest actions
-Theming:
+- Theming:
- Introduce a new theme
- Allow selecting themes/templates from the configuration page
- New/Edit link form can be submitted using CTRL+Enter in the textarea
@@ -425,22 +614,6 @@ Theming:
### Security
- Markdown plugin: escape HTML entities by default
-## [v0.8.4](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4) - 2017-03-04
-### Security
-- Markdown plugin: escape HTML entities by default
-
-
-## [v0.8.3](https://github.com/shaarli/Shaarli/releases/tag/v0.8.3) - 2017-01-20
-
-### Fixed
-
-- PHP 7.1 compatibility: add ConfigManager parameter to anti-bruteforce function call in login template.
-
-## [v0.8.2](https://github.com/shaarli/Shaarli/releases/tag/v0.8.2) - 2016-12-15
-
-### Fixed
-
-- Editing a link created before the new ID system would change its permalink.
## [v0.8.7](https://github.com/shaarli/Shaarli/releases/tag/v0.8.7) - 2018-06-20
### Changed
diff --git a/README.md b/README.md
index 21f2eae7..b1746ed6 100644
--- a/README.md
+++ b/README.md
@@ -6,18 +6,13 @@ _Do you want to share the links you discover?_
_Shaarli is a minimalist link sharing service that you can install on your own server._
_It is designed to be personal (single-user), fast and handy._
-[![](https://img.shields.io/badge/stable-v0.9.7-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.7)
-[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
-•
-[![](https://img.shields.io/badge/latest-v0.10.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.10.4)
-[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
-•
-[![](https://img.shields.io/badge/master-v0.11.x-blue.svg)](https://github.com/shaarli/Shaarli)
-[![](https://img.shields.io/travis/shaarli/Shaarli.svg?label=master)](https://travis-ci.org/shaarli/Shaarli)
-
+[![](https://img.shields.io/badge/stable-v0.11.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.11.1)
+[![](https://img.shields.io/badge/latest-v0.12.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.12.1)
+[![](https://img.shields.io/badge/master-v0.12.x-blue.svg)](https://github.com/shaarli/Shaarli)
+[![](https://github.com/shaarli/Shaarli/actions/workflows/ci.yml/badge.svg)](https://github.com/shaarli/Shaarli/actions)
[![Join the chat at https://gitter.im/shaarli/Shaarli](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/shaarli/Shaarli)
[![Bountysource](https://www.bountysource.com/badge/team?team_id=19583&style=bounties_received)](https://www.bountysource.com/teams/shaarli/issues)
-[![Docker repository](https://img.shields.io/docker/pulls/shaarli/shaarli.svg)](https://hub.docker.com/r/shaarli/shaarli/)
+[![Docker repository](https://img.shields.io/docker/pulls/shaarli/shaarli.svg)](https://github.com/shaarli/Shaarli/pkgs/container/shaarli)
## Quickstart
diff --git a/application/History.php b/application/History.php
index a5846652..d230f39d 100644
--- a/application/History.php
+++ b/application/History.php
@@ -1,8 +1,11 @@
addEvent(self::CREATED, $link['id']);
+ $this->addEvent(self::CREATED, $link->getId());
}
/**
* Add Event: update existing link.
*
- * @param array $link Link data.
+ * @param Bookmark $link Link data.
*/
public function updateLink($link)
{
- $this->addEvent(self::UPDATED, $link['id']);
+ $this->addEvent(self::UPDATED, $link->getId());
}
/**
* Add Event: delete existing link.
*
- * @param array $link Link data.
+ * @param Bookmark $link Link data.
*/
public function deleteLink($link)
{
- $this->addEvent(self::DELETED, $link['id']);
+ $this->addEvent(self::DELETED, $link->getId());
}
/**
@@ -134,7 +137,7 @@ public function updateSettings()
/**
* Add Event: bulk import.
*
- * Note: we don't store links add/update one by one since it can have a huge impact on performances.
+ * Note: we don't store bookmarks add/update one by one since it can have a huge impact on performances.
*/
public function importLinks()
{
diff --git a/application/Languages.php b/application/Languages.php
index 5cda802e..3ea434e2 100644
--- a/application/Languages.php
+++ b/application/Languages.php
@@ -41,7 +41,7 @@ class Languages
/**
* Core translations domain
*/
- const DEFAULT_DOMAIN = 'shaarli';
+ public const DEFAULT_DOMAIN = 'shaarli';
/**
* @var TranslatorInterface
@@ -76,7 +76,8 @@ public function __construct($language, $conf)
$this->language = $confLanguage;
}
- if (! extension_loaded('gettext')
+ if (
+ ! extension_loaded('gettext')
|| in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
) {
$this->initPhpTranslator();
@@ -98,7 +99,7 @@ protected function initGettextTranslator()
$this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
// Default extension translation from the current theme
- $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $this->conf->get('theme') .'/language';
+ $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $this->conf->get('theme') . '/language';
if (is_dir($themeTransFolder)) {
$this->translator->loadDomain($this->conf->get('theme'), $themeTransFolder, false);
}
@@ -121,7 +122,9 @@ protected function initPhpTranslator()
$translations = new Translations();
// Core translations
try {
- $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
+ $translations = $translations->addFromPoFile(
+ 'inc/languages/' . $this->language . '/LC_MESSAGES/shaarli.po'
+ );
$translations->setDomain('shaarli');
$this->translator->loadTranslations($translations);
} catch (\InvalidArgumentException $e) {
@@ -129,11 +132,11 @@ protected function initPhpTranslator()
// Default extension translation from the current theme
$theme = $this->conf->get('theme');
- $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') .'/'. $theme .'/language';
+ $themeTransFolder = rtrim($this->conf->get('raintpl_tpl'), '/') . '/' . $theme . '/language';
if (is_dir($themeTransFolder)) {
try {
$translations = Translations::fromPoFile(
- $themeTransFolder .'/'. $this->language .'/LC_MESSAGES/'. $theme .'.po'
+ $themeTransFolder . '/' . $this->language . '/LC_MESSAGES/' . $theme . '.po'
);
$translations->setDomain($theme);
$this->translator->loadTranslations($translations);
@@ -149,7 +152,7 @@ protected function initPhpTranslator()
try {
$extension = Translations::fromPoFile(
- $translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po'
+ $translationPath . $this->language . '/LC_MESSAGES/' . $domain . '.po'
);
$extension->setDomain($domain);
$this->translator->loadTranslations($extension);
@@ -179,9 +182,12 @@ public static function getAvailableLanguages()
{
return [
'auto' => t('Automatic'),
+ 'de' => t('German'),
'en' => t('English'),
'fr' => t('French'),
- 'de' => t('German'),
+ 'jp' => t('Japanese'),
+ 'ru' => t('Russian'),
+ 'zh_CN' => t('Chinese (Simplified)'),
];
}
}
diff --git a/application/Thumbnailer.php b/application/Thumbnailer.php
index d5f5ac28..c4ff8d7a 100644
--- a/application/Thumbnailer.php
+++ b/application/Thumbnailer.php
@@ -4,7 +4,6 @@
use Shaarli\Config\ConfigManager;
use WebThumbnailer\Application\ConfigManager as WTConfigManager;
-use WebThumbnailer\Exception\WebThumbnailerException;
use WebThumbnailer\WebThumbnailer;
/**
@@ -14,7 +13,7 @@
*/
class Thumbnailer
{
- const COMMON_MEDIA_DOMAINS = [
+ protected const COMMON_MEDIA_DOMAINS = [
'imgur.com',
'flickr.com',
'youtube.com',
@@ -27,13 +26,14 @@ class Thumbnailer
'instagram.com',
'pinterest.com',
'pinterest.fr',
+ 'soundcloud.com',
'tumblr.com',
'deviantart.com',
];
- const MODE_ALL = 'all';
- const MODE_COMMON = 'common';
- const MODE_NONE = 'none';
+ public const MODE_ALL = 'all';
+ public const MODE_COMMON = 'common';
+ public const MODE_NONE = 'none';
/**
* @var WebThumbnailer instance.
@@ -60,7 +60,7 @@ public function __construct($conf)
// TODO: create a proper error handling system able to catch exceptions...
die(t(
'php-gd extension must be loaded to use thumbnails. '
- .'Thumbnails are now disabled. Please reload the page.'
+ . 'Thumbnails are now disabled. Please reload the page.'
));
}
@@ -81,7 +81,8 @@ public function __construct($conf)
*/
public function get($url)
{
- if ($this->conf->get('thumbnails.mode') === self::MODE_COMMON
+ if (
+ $this->conf->get('thumbnails.mode') === self::MODE_COMMON
&& ! $this->isCommonMediaOrImage($url)
) {
return false;
@@ -89,7 +90,7 @@ public function get($url)
try {
return $this->wt->thumbnail($url);
- } catch (WebThumbnailerException $e) {
+ } catch (\Throwable $e) {
// Exceptions are only thrown in debug mode.
error_log(get_class($e) . ': ' . $e->getMessage());
}
diff --git a/application/TimeZone.php b/application/TimeZone.php
index c1869ef8..a420eb96 100644
--- a/application/TimeZone.php
+++ b/application/TimeZone.php
@@ -1,4 +1,5 @@
$continent, 'city' => $city];
$continents[$continent] = true;
}
@@ -85,7 +86,7 @@ function generateTimeZoneData($installedTimeZones, $preselectedTimezone = '')
function isTimeZoneValid($continent, $city)
{
return in_array(
- $continent.'/'.$city,
+ $continent . '/' . $city,
timezone_identifiers_list()
);
}
diff --git a/application/Utils.php b/application/Utils.php
index 925e1a22..48d47415 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -1,24 +1,27 @@
$value) {
- $out[$key] = escape($value);
+ $out[escape($key)] = escape($value);
}
return $out;
}
@@ -157,12 +165,12 @@ function checkDateFormat($format, $string)
*
* @return string $referer - final referer.
*/
-function generateLocation($referer, $host, $loopTerms = array())
+function generateLocation($referer, $host, $loopTerms = [])
{
- $finalReferer = '?';
+ $finalReferer = './?';
// No referer if it contains any value in $loopCriteria.
- foreach ($loopTerms as $value) {
+ foreach (array_filter($loopTerms) as $value) {
if (strpos($referer, $value) !== false) {
return $finalReferer;
}
@@ -173,7 +181,7 @@ function generateLocation($referer, $host, $loopTerms = array())
$host = substr($host, 0, $pos);
}
- $refererHost = parse_url($referer, PHP_URL_HOST);
+ $refererHost = parse_url($referer, PHP_URL_HOST) ?? '';
if (!empty($referer) && (strpos($refererHost, $host) !== false || startsWith('?', $refererHost))) {
$finalReferer = $referer;
}
@@ -190,7 +198,7 @@ function generateLocation($referer, $host, $loopTerms = array())
function autoLocale($headerLocale)
{
// Default if browser does not send HTTP_ACCEPT_LANGUAGE
- $locales = array('en_US', 'en_US.utf8', 'en_US.UTF-8');
+ $locales = ['en_US.UTF-8', 'en_US.utf8', 'en_US'];
if (! empty($headerLocale)) {
if (preg_match_all('/([a-z]{2,3})[-_]?([a-z]{2})?,?/i', $headerLocale, $matches, PREG_SET_ORDER)) {
$attempts = [];
@@ -285,7 +293,7 @@ function generate_api_secret($username, $salt)
*/
function normalize_spaces($string)
{
- return preg_replace('/\s{2,}/', ' ', trim($string));
+ return preg_replace('/\s{2,}/', ' ', trim($string ?? ''));
}
/**
@@ -294,32 +302,52 @@ function normalize_spaces($string)
* Requires php-intl to display international datetimes,
* otherwise default format '%c' will be returned.
*
- * @param DateTime $date to format.
- * @param bool $time Displays time if true.
- * @param bool $intl Use international format if true.
+ * @param DateTimeInterface $date to format.
+ * @param bool $time Displays time if true.
+ * @param bool $intl Use international format if true.
*
* @return bool|string Formatted date, or false if the input is invalid.
*/
function format_date($date, $time = true, $intl = true)
{
- if (! $date instanceof DateTime) {
+ if (! $date instanceof DateTimeInterface) {
return false;
}
if (! $intl || ! class_exists('IntlDateFormatter')) {
- $format = $time ? '%c' : '%x';
- return strftime($format, $date->getTimestamp());
+ $format = 'F j, Y';
+ if ($time) {
+ $format .= ' h:i:s A \G\M\TP';
+ }
+ return $date->format($format);
}
-
$formatter = new IntlDateFormatter(
setlocale(LC_TIME, 0),
IntlDateFormatter::LONG,
$time ? IntlDateFormatter::LONG : IntlDateFormatter::NONE
);
+ $formatter->setTimeZone($date->getTimezone());
return $formatter->format($date);
}
+/**
+ * Format the date month according to the locale.
+ *
+ * @param DateTimeInterface $date to format.
+ *
+ * @return bool|string Formatted date, or false if the input is invalid.
+ */
+function format_month(DateTimeInterface $date)
+{
+ if (! $date instanceof DateTimeInterface) {
+ return false;
+ }
+
+ return strftime('%B', $date->getTimestamp());
+}
+
+
/**
* Check if the input is an integer, no matter its real type.
*
@@ -353,13 +381,15 @@ function return_bytes($val)
return $val;
}
$val = trim($val);
- $last = strtolower($val[strlen($val)-1]);
+ $last = strtolower($val[strlen($val) - 1]);
$val = intval(substr($val, 0, -1));
switch ($last) {
case 'g':
$val *= 1024;
+ // do no break in order 1024^2 for each unit
case 'm':
$val *= 1024;
+ // do no break in order 1024^2 for each unit
case 'k':
$val *= 1024;
}
@@ -448,14 +478,28 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
* Wrapper function for translation which match the API
* of gettext()/_() and ngettext().
*
- * @param string $text Text to translate.
- * @param string $nText The plural message ID.
- * @param int $nb The number of items for plural forms.
- * @param string $domain The domain where the translation is stored (default: shaarli).
+ * @param string $text Text to translate.
+ * @param string $nText The plural message ID.
+ * @param int $nb The number of items for plural forms.
+ * @param string $domain The domain where the translation is stored (default: shaarli).
+ * @param array $variables Associative array of variables to replace in translated text.
+ * @param bool $fixCase Apply `ucfirst` on the translated string, might be useful for strings with variables.
*
* @return string Text translated.
*/
-function t($text, $nText = '', $nb = 1, $domain = 'shaarli')
+function t($text, $nText = '', $nb = 1, $domain = 'shaarli', $variables = [], $fixCase = false)
{
- return dn__($domain, $text, $nText, $nb);
+ $postFunction = $fixCase ? 'ucfirst' : function ($input) {
+ return $input;
+ };
+
+ return $postFunction(dn__($domain, $text, $nText, $nb, $variables));
+}
+
+/**
+ * Converts an exception into a printable stack trace string.
+ */
+function exception2text(Throwable $e): string
+{
+ return $e->getMessage() . PHP_EOL . $e->getFile() . $e->getLine() . PHP_EOL . $e->getTraceAsString();
}
diff --git a/application/api/ApiMiddleware.php b/application/api/ApiMiddleware.php
index 2d55bda6..cc7af18e 100644
--- a/application/api/ApiMiddleware.php
+++ b/application/api/ApiMiddleware.php
@@ -1,8 +1,11 @@
getApiResponse();
}
- return $response;
+ return $response
+ ->withHeader('Access-Control-Allow-Origin', '*')
+ ->withHeader(
+ 'Access-Control-Allow-Headers',
+ 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
+ )
+ ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
+ ;
}
/**
@@ -99,7 +109,10 @@ protected function checkRequest($request)
*/
protected function checkToken($request)
{
- if (! $request->hasHeader('Authorization')) {
+ if (
+ !$request->hasHeader('Authorization')
+ && !isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])
+ ) {
throw new ApiAuthorizationException('JWT token not provided');
}
@@ -107,7 +120,11 @@ protected function checkToken($request)
throw new ApiAuthorizationException('Token secret must be set in Shaarli\'s administration');
}
- $authorization = $request->getHeaderLine('Authorization');
+ if (isset($this->container->environment['REDIRECT_HTTP_AUTHORIZATION'])) {
+ $authorization = $this->container->environment['REDIRECT_HTTP_AUTHORIZATION'];
+ } else {
+ $authorization = $request->getHeaderLine('Authorization');
+ }
if (! preg_match('/^Bearer (.*)/i', $authorization, $matches)) {
throw new ApiAuthorizationException('Invalid JWT header');
@@ -117,7 +134,7 @@ protected function checkToken($request)
}
/**
- * Instantiate a new LinkDB including private links,
+ * Instantiate a new LinkDB including private bookmarks,
* and load in the Slim container.
*
* FIXME! LinkDB could use a refactoring to avoid this trick.
@@ -126,10 +143,12 @@ protected function checkToken($request)
*/
protected function setLinkDb($conf)
{
- $linkDb = new \Shaarli\Bookmark\LinkDB(
- $conf->get('resource.datastore'),
- true,
- $conf->get('privacy.hide_public_links')
+ $linkDb = new BookmarkFileService(
+ $conf,
+ $this->container->get('pluginManager'),
+ $this->container->get('history'),
+ new FlockMutex(fopen(SHAARLI_MUTEX_FILE, 'r'), 2),
+ true
);
$this->container['db'] = $linkDb;
}
diff --git a/application/api/ApiUtils.php b/application/api/ApiUtils.php
index 1e3ac02e..9228bb2d 100644
--- a/application/api/ApiUtils.php
+++ b/application/api/ApiUtils.php
@@ -1,7 +1,9 @@
iat)
+ if (
+ empty($payload->iat)
|| $payload->iat > time()
|| time() - $payload->iat > ApiMiddleware::$TOKEN_DURATION
) {
throw new ApiAuthorizationException('Invalid JWT issued time');
}
+
+ return true;
}
/**
* Format a Link for the REST API.
*
- * @param array $link Link data read from the datastore.
- * @param string $indexUrl Shaarli's index URL (used for relative URL).
+ * @param Bookmark $bookmark Bookmark data read from the datastore.
+ * @param string $indexUrl Shaarli's index URL (used for relative URL).
*
* @return array Link data formatted for the REST API.
*/
- public static function formatLink($link, $indexUrl)
+ public static function formatLink($bookmark, $indexUrl)
{
- $out['id'] = $link['id'];
+ $out['id'] = $bookmark->getId();
// Not an internal link
- if (! is_note($link['url'])) {
- $out['url'] = $link['url'];
+ if (! $bookmark->isNote()) {
+ $out['url'] = $bookmark->getUrl();
} else {
- $out['url'] = $indexUrl . $link['url'];
+ $out['url'] = rtrim($indexUrl, '/') . '/' . ltrim($bookmark->getUrl(), '/');
}
- $out['shorturl'] = $link['shorturl'];
- $out['title'] = $link['title'];
- $out['description'] = $link['description'];
- $out['tags'] = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
- $out['private'] = $link['private'] == true;
- $out['created'] = $link['created']->format(\DateTime::ATOM);
- if (! empty($link['updated'])) {
- $out['updated'] = $link['updated']->format(\DateTime::ATOM);
+ $out['shorturl'] = $bookmark->getShortUrl();
+ $out['title'] = $bookmark->getTitle();
+ $out['description'] = $bookmark->getDescription();
+ $out['tags'] = $bookmark->getTags();
+ $out['private'] = $bookmark->isPrivate();
+ $out['created'] = $bookmark->getCreated()->format(\DateTime::ATOM);
+ if (! empty($bookmark->getUpdated())) {
+ $out['updated'] = $bookmark->getUpdated()->format(\DateTime::ATOM);
} else {
$out['updated'] = '';
}
@@ -79,58 +86,72 @@ public static function formatLink($link, $indexUrl)
}
/**
- * Convert a link given through a request, to a valid link for LinkDB.
+ * Convert a link given through a request, to a valid Bookmark for the datastore.
*
* 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 $input Request Link.
- * @param bool $defaultPrivate Request Link.
+ * @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 array Formatted link.
+ * @return Bookmark instance.
*/
- public static function buildLinkFromRequest($input, $defaultPrivate)
- {
- $input['url'] = ! empty($input['url']) ? cleanup_url($input['url']) : '';
+ 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'])) {
$private = filter_var($input['private'], FILTER_VALIDATE_BOOLEAN);
} else {
$private = $defaultPrivate;
}
- $link = [
- 'title' => ! empty($input['title']) ? $input['title'] : $input['url'],
- 'url' => $input['url'],
- 'description' => ! empty($input['description']) ? $input['description'] : '',
- 'tags' => ! empty($input['tags']) ? implode(' ', $input['tags']) : '',
- 'private' => $private,
- 'created' => new \DateTime(),
- ];
- return $link;
+ $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);
+
+ $created = \DateTime::createFromFormat(\DateTime::ATOM, $input['created'] ?? '');
+ if ($created instanceof \DateTimeInterface) {
+ $bookmark->setCreated($created);
+ }
+ $updated = \DateTime::createFromFormat(\DateTime::ATOM, $input['updated'] ?? '');
+ if ($updated instanceof \DateTimeInterface) {
+ $bookmark->setUpdated($updated);
+ }
+
+ return $bookmark;
}
/**
* Update link fields using an updated link object.
*
- * @param array $oldLink data
- * @param array $newLink data
+ * @param Bookmark $oldLink data
+ * @param Bookmark $newLink data
*
- * @return array $oldLink updated with $newLink values
+ * @return Bookmark $oldLink updated with $newLink values
*/
public static function updateLink($oldLink, $newLink)
{
- foreach (['title', 'url', 'description', 'tags', 'private'] as $field) {
- $oldLink[$field] = $newLink[$field];
- }
- $oldLink['updated'] = new \DateTime();
-
- if (empty($oldLink['url'])) {
- $oldLink['url'] = '?' . $oldLink['shorturl'];
- }
-
- if (empty($oldLink['title'])) {
- $oldLink['title'] = $oldLink['url'];
- }
+ $oldLink->setTitle($newLink->getTitle());
+ $oldLink->setUrl($newLink->getUrl());
+ $oldLink->setDescription($newLink->getDescription());
+ $oldLink->setTags($newLink->getTags());
+ $oldLink->setPrivate($newLink->isPrivate());
return $oldLink;
}
@@ -139,7 +160,7 @@ public static function updateLink($oldLink, $newLink)
* Format a Tag for the REST API.
*
* @param string $tag Tag name
- * @param int $occurrences Number of links using this tag
+ * @param int $occurrences Number of bookmarks using this tag
*
* @return array Link data formatted for the REST API.
*/
diff --git a/application/api/controllers/ApiController.php b/application/api/controllers/ApiController.php
index a6e7cbab..88a845eb 100644
--- a/application/api/controllers/ApiController.php
+++ b/application/api/controllers/ApiController.php
@@ -2,8 +2,9 @@
namespace Shaarli\Api\Controllers;
-use Shaarli\Bookmark\LinkDB;
+use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
+use Shaarli\History;
use Slim\Container;
/**
@@ -26,12 +27,12 @@ abstract class ApiController
protected $conf;
/**
- * @var LinkDB
+ * @var BookmarkServiceInterface
*/
- protected $linkDb;
+ protected $bookmarkService;
/**
- * @var HistoryController
+ * @var History
*/
protected $history;
@@ -51,7 +52,7 @@ public function __construct(Container $ci)
{
$this->ci = $ci;
$this->conf = $ci->get('conf');
- $this->linkDb = $ci->get('db');
+ $this->bookmarkService = $ci->get('db');
$this->history = $ci->get('history');
if ($this->conf->get('dev.debug', false)) {
$this->jsonStyle = JSON_PRETTY_PRINT;
diff --git a/application/api/controllers/HistoryController.php b/application/api/controllers/HistoryController.php
index 9afcfa26..e16036f6 100644
--- a/application/api/controllers/HistoryController.php
+++ b/application/api/controllers/HistoryController.php
@@ -1,6 +1,5 @@
history->getHistory();
// Return history operations from the {offset}th, starting from {since}.
- $since = \DateTime::createFromFormat(\DateTime::ATOM, $request->getParam('since'));
+ $since = \DateTime::createFromFormat(\DateTime::ATOM, $request->getParam('since', ''));
$offset = $request->getParam('offset');
if (empty($offset)) {
$offset = 0;
@@ -41,7 +40,7 @@ public function getHistory($request, $response)
throw new ApiBadParametersException('Invalid offset');
}
- // limit parameter is either a number of links or 'all' for everything.
+ // limit parameter is either a number of bookmarks or 'all' for everything.
$limit = $request->getParam('limit');
if (empty($limit)) {
$limit = count($history);
diff --git a/application/api/controllers/Info.php b/application/api/controllers/Info.php
index f37dcae5..ae7db93e 100644
--- a/application/api/controllers/Info.php
+++ b/application/api/controllers/Info.php
@@ -2,6 +2,7 @@
namespace Shaarli\Api\Controllers;
+use Shaarli\Bookmark\BookmarkFilter;
use Slim\Http\Request;
use Slim\Http\Response;
@@ -26,15 +27,15 @@ class Info extends ApiController
public function getInfo($request, $response)
{
$info = [
- 'global_counter' => count($this->linkDb),
- 'private_counter' => count_private($this->linkDb),
- 'settings' => array(
+ 'global_counter' => $this->bookmarkService->count(),
+ 'private_counter' => $this->bookmarkService->count(BookmarkFilter::$PRIVATE),
+ 'settings' => [
'title' => $this->conf->get('general.title', 'Shaarli'),
'header_link' => $this->conf->get('general.header_link', '?'),
'timezone' => $this->conf->get('general.timezone', 'UTC'),
'enabled_plugins' => $this->conf->get('general.enabled_plugins', []),
'default_private_links' => $this->conf->get('privacy.default_private_links', false),
- ),
+ ],
];
return $response->withJson($info, 200, $this->jsonStyle);
diff --git a/application/api/controllers/Links.php b/application/api/controllers/Links.php
index ffcfd4c7..fe4bdc9f 100644
--- a/application/api/controllers/Links.php
+++ b/application/api/controllers/Links.php
@@ -11,7 +11,7 @@
/**
* Class Links
*
- * REST API Controller: all services related to links collection.
+ * REST API Controller: all services related to bookmarks collection.
*
* @package Api\Controllers
* @see http://shaarli.github.io/api-documentation/#links-links-collection
@@ -19,12 +19,12 @@
class Links extends ApiController
{
/**
- * @var int Number of links returned if no limit is provided.
+ * @var int Number of bookmarks returned if no limit is provided.
*/
public static $DEFAULT_LIMIT = 20;
/**
- * Retrieve a list of links, allowing different filters.
+ * Retrieve a list of bookmarks, allowing different filters.
*
* @param Request $request Slim request.
* @param Response $response Slim response.
@@ -36,49 +36,48 @@ class Links extends ApiController
public function getLinks($request, $response)
{
$private = $request->getParam('visibility');
- $links = $this->linkDb->filterSearch(
- [
- 'searchtags' => $request->getParam('searchtags', ''),
- 'searchterm' => $request->getParam('searchterm', ''),
- ],
- false,
- $private
- );
- // Return links from the {offset}th link, starting from 0.
+ // Return bookmarks from the {offset}th link, starting from 0.
$offset = $request->getParam('offset');
if (! empty($offset) && ! ctype_digit($offset)) {
throw new ApiBadParametersException('Invalid offset');
}
$offset = ! empty($offset) ? intval($offset) : 0;
- if ($offset > count($links)) {
- return $response->withJson([], 200, $this->jsonStyle);
- }
- // limit parameter is either a number of links or 'all' for everything.
+ // limit parameter is either a number of bookmarks or 'all' for everything.
$limit = $request->getParam('limit');
if (empty($limit)) {
$limit = self::$DEFAULT_LIMIT;
} elseif (ctype_digit($limit)) {
$limit = intval($limit);
} elseif ($limit === 'all') {
- $limit = count($links);
+ $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 ($links as $link) {
- if (count($out) >= $limit) {
- break;
- }
- if ($index++ >= $offset) {
- $out[] = ApiUtils::formatLink($link, $indexUrl);
- }
+ foreach ($searchResult->getBookmarks() as $bookmark) {
+ $out[] = ApiUtils::formatLink($bookmark, $indexUrl);
}
return $response->withJson($out, 200, $this->jsonStyle);
@@ -97,11 +96,12 @@ public function getLinks($request, $response)
*/
public function getLink($request, $response, $args)
{
- if (!isset($this->linkDb[$args['id']])) {
+ $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null;
+ if ($id === null || ! $this->bookmarkService->exists($id)) {
throw new ApiLinkNotFoundException();
}
$index = index_url($this->ci['environment']);
- $out = ApiUtils::formatLink($this->linkDb[$args['id']], $index);
+ $out = ApiUtils::formatLink($this->bookmarkService->get($id), $index);
return $response->withJson($out, 200, $this->jsonStyle);
}
@@ -116,10 +116,17 @@ public function getLink($request, $response, $args)
*/
public function postLink($request, $response)
{
- $data = $request->getParsedBody();
- $link = ApiUtils::buildLinkFromRequest($data, $this->conf->get('privacy.default_private_links'));
+ $data = (array) ($request->getParsedBody() ?? []);
+ $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($link['url']) && ! empty($dup = $this->linkDb->getLinkFromUrl($link['url']))) {
+ if (
+ ! empty($bookmark->getUrl())
+ && ! empty($dup = $this->bookmarkService->findByUrl($bookmark->getUrl()))
+ ) {
return $response->withJson(
ApiUtils::formatLink($dup, index_url($this->ci['environment'])),
409,
@@ -127,23 +134,9 @@ public function postLink($request, $response)
);
}
- $link['id'] = $this->linkDb->getNextId();
- $link['shorturl'] = link_small_hash($link['created'], $link['id']);
-
- // note: general relative URL
- if (empty($link['url'])) {
- $link['url'] = '?' . $link['shorturl'];
- }
-
- if (empty($link['title'])) {
- $link['title'] = $link['url'];
- }
-
- $this->linkDb[$link['id']] = $link;
- $this->linkDb->save($this->conf->get('resource.page_cache'));
- $this->history->addLink($link);
- $out = ApiUtils::formatLink($link, index_url($this->ci['environment']));
- $redirect = $this->ci->router->relativePathFor('getLink', ['id' => $link['id']]);
+ $this->bookmarkService->add($bookmark);
+ $out = ApiUtils::formatLink($bookmark, index_url($this->ci['environment']));
+ $redirect = $this->ci->router->pathFor('getLink', ['id' => $bookmark->getId()]);
return $response->withAddedHeader('Location', $redirect)
->withJson($out, 201, $this->jsonStyle);
}
@@ -161,18 +154,24 @@ public function postLink($request, $response)
*/
public function putLink($request, $response, $args)
{
- if (! isset($this->linkDb[$args['id']])) {
+ $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null;
+ if ($id === null || !$this->bookmarkService->exists($id)) {
throw new ApiLinkNotFoundException();
}
$index = index_url($this->ci['environment']);
$data = $request->getParsedBody();
- $requestLink = ApiUtils::buildLinkFromRequest($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($requestLink['url'])
- && ! empty($dup = $this->linkDb->getLinkFromUrl($requestLink['url']))
- && $dup['id'] != $args['id']
+ if (
+ ! empty($requestBookmark->getUrl())
+ && ! empty($dup = $this->bookmarkService->findByUrl($requestBookmark->getUrl()))
+ && $dup->getId() != $id
) {
return $response->withJson(
ApiUtils::formatLink($dup, $index),
@@ -181,13 +180,11 @@ public function putLink($request, $response, $args)
);
}
- $responseLink = $this->linkDb[$args['id']];
- $responseLink = ApiUtils::updateLink($responseLink, $requestLink);
- $this->linkDb[$responseLink['id']] = $responseLink;
- $this->linkDb->save($this->conf->get('resource.page_cache'));
- $this->history->updateLink($responseLink);
+ $responseBookmark = $this->bookmarkService->get($id);
+ $responseBookmark = ApiUtils::updateLink($responseBookmark, $requestBookmark);
+ $this->bookmarkService->set($responseBookmark);
- $out = ApiUtils::formatLink($responseLink, $index);
+ $out = ApiUtils::formatLink($responseBookmark, $index);
return $response->withJson($out, 200, $this->jsonStyle);
}
@@ -204,13 +201,12 @@ public function putLink($request, $response, $args)
*/
public function deleteLink($request, $response, $args)
{
- if (! isset($this->linkDb[$args['id']])) {
+ $id = is_integer_mixed($args['id']) ? (int) $args['id'] : null;
+ if ($id === null || !$this->bookmarkService->exists($id)) {
throw new ApiLinkNotFoundException();
}
- $link = $this->linkDb[$args['id']];
- unset($this->linkDb[(int) $args['id']]);
- $this->linkDb->save($this->conf->get('resource.page_cache'));
- $this->history->deleteLink($link);
+ $bookmark = $this->bookmarkService->get($id);
+ $this->bookmarkService->remove($bookmark);
return $response->withStatus(204);
}
diff --git a/application/api/controllers/Tags.php b/application/api/controllers/Tags.php
index 82f3ef74..5a23f6db 100644
--- a/application/api/controllers/Tags.php
+++ b/application/api/controllers/Tags.php
@@ -5,6 +5,7 @@
use Shaarli\Api\ApiUtils;
use Shaarli\Api\Exceptions\ApiBadParametersException;
use Shaarli\Api\Exceptions\ApiTagNotFoundException;
+use Shaarli\Bookmark\BookmarkFilter;
use Slim\Http\Request;
use Slim\Http\Response;
@@ -18,7 +19,7 @@
class Tags extends ApiController
{
/**
- * @var int Number of links returned if no limit is provided.
+ * @var int Number of bookmarks returned if no limit is provided.
*/
public static $DEFAULT_LIMIT = 'all';
@@ -35,7 +36,7 @@ class Tags extends ApiController
public function getTags($request, $response)
{
$visibility = $request->getParam('visibility');
- $tags = $this->linkDb->linksCountPerTag([], $visibility);
+ $tags = $this->bookmarkService->bookmarksCountPerTag([], $visibility);
// Return tags from the {offset}th tag, starting from 0.
$offset = $request->getParam('offset');
@@ -47,7 +48,7 @@ public function getTags($request, $response)
return $response->withJson([], 200, $this->jsonStyle);
}
- // limit parameter is either a number of links or 'all' for everything.
+ // limit parameter is either a number of bookmarks or 'all' for everything.
$limit = $request->getParam('limit');
if (empty($limit)) {
$limit = self::$DEFAULT_LIMIT;
@@ -87,7 +88,7 @@ public function getTags($request, $response)
*/
public function getTag($request, $response, $args)
{
- $tags = $this->linkDb->linksCountPerTag();
+ $tags = $this->bookmarkService->bookmarksCountPerTag();
if (!isset($tags[$args['tagName']])) {
throw new ApiTagNotFoundException();
}
@@ -111,7 +112,7 @@ public function getTag($request, $response, $args)
*/
public function putTag($request, $response, $args)
{
- $tags = $this->linkDb->linksCountPerTag();
+ $tags = $this->bookmarkService->bookmarksCountPerTag();
if (! isset($tags[$args['tagName']])) {
throw new ApiTagNotFoundException();
}
@@ -121,13 +122,19 @@ public function putTag($request, $response, $args)
throw new ApiBadParametersException('New tag name is required in the request body');
}
- $updated = $this->linkDb->renameTag($args['tagName'], $data['name']);
- $this->linkDb->save($this->conf->get('resource.page_cache'));
- foreach ($updated as $link) {
- $this->history->updateLink($link);
+ $searchResult = $this->bookmarkService->search(
+ ['searchtags' => $args['tagName']],
+ BookmarkFilter::$ALL,
+ true
+ );
+ foreach ($searchResult->getBookmarks() as $bookmark) {
+ $bookmark->renameTag($args['tagName'], $data['name']);
+ $this->bookmarkService->set($bookmark, false);
+ $this->history->updateLink($bookmark);
}
+ $this->bookmarkService->save();
- $tags = $this->linkDb->linksCountPerTag();
+ $tags = $this->bookmarkService->bookmarksCountPerTag();
$out = ApiUtils::formatTag($data['name'], $tags[$data['name']]);
return $response->withJson($out, 200, $this->jsonStyle);
}
@@ -145,15 +152,22 @@ public function putTag($request, $response, $args)
*/
public function deleteTag($request, $response, $args)
{
- $tags = $this->linkDb->linksCountPerTag();
+ $tags = $this->bookmarkService->bookmarksCountPerTag();
if (! isset($tags[$args['tagName']])) {
throw new ApiTagNotFoundException();
}
- $updated = $this->linkDb->renameTag($args['tagName'], null);
- $this->linkDb->save($this->conf->get('resource.page_cache'));
- foreach ($updated as $link) {
- $this->history->updateLink($link);
+
+ $searchResult = $this->bookmarkService->search(
+ ['searchtags' => $args['tagName']],
+ BookmarkFilter::$ALL,
+ true
+ );
+ foreach ($searchResult->getBookmarks() as $bookmark) {
+ $bookmark->deleteTag($args['tagName']);
+ $this->bookmarkService->set($bookmark, false);
+ $this->history->updateLink($bookmark);
}
+ $this->bookmarkService->save();
return $response->withStatus(204);
}
diff --git a/application/api/exceptions/ApiAuthorizationException.php b/application/api/exceptions/ApiAuthorizationException.php
index 0e3f4776..c77e9eea 100644
--- a/application/api/exceptions/ApiAuthorizationException.php
+++ b/application/api/exceptions/ApiAuthorizationException.php
@@ -28,7 +28,7 @@ public function getApiResponse()
*/
public function setMessage($message)
{
- $original = $this->debug === true ? ': '. $this->getMessage() : '';
+ $original = $this->debug === true ? ': ' . $this->getMessage() : '';
$this->message = $message . $original;
}
}
diff --git a/application/api/exceptions/ApiException.php b/application/api/exceptions/ApiException.php
index d6b66323..49cfbee4 100644
--- a/application/api/exceptions/ApiException.php
+++ b/application/api/exceptions/ApiException.php
@@ -12,7 +12,6 @@
*/
abstract class ApiException extends \Exception
{
-
/**
* @var Response instance from Slim.
*/
@@ -44,7 +43,7 @@ protected function getApiResponseBody()
}
return [
'message' => $this->getMessage(),
- 'stacktrace' => get_class($this) .': '. $this->getTraceAsString()
+ 'stacktrace' => get_class($this) . ': ' . $this->getTraceAsString()
];
}
diff --git a/application/bookmark/Bookmark.php b/application/bookmark/Bookmark.php
new file mode 100644
index 00000000..56751e15
--- /dev/null
+++ b/application/bookmark/Bookmark.php
@@ -0,0 +1,542 @@
+id = $data['id'] ?? null;
+ $this->shortUrl = $data['shorturl'] ?? null;
+ $this->url = $data['url'] ?? null;
+ $this->title = $data['title'] ?? null;
+ $this->description = $data['description'] ?? null;
+ $this->thumbnail = $data['thumbnail'] ?? null;
+ $this->sticky = $data['sticky'] ?? false;
+ $this->created = $data['created'] ?? null;
+ if (is_array($data['tags'])) {
+ $this->tags = $data['tags'];
+ } else {
+ $this->tags = tags_str2array($data['tags'] ?? '', $tagsSeparator);
+ }
+ if (! empty($data['updated'])) {
+ $this->updated = $data['updated'];
+ }
+ $this->private = ($data['private'] ?? false) ? true : false;
+ $this->additionalContent = $data['additional_content'] ?? [];
+
+ return $this;
+ }
+
+ /**
+ * Make sure that the current instance of Bookmark is valid and can be saved into the data store.
+ * A valid link requires:
+ * - an integer ID
+ * - a short URL (for permalinks)
+ * - a creation date
+ *
+ * This function also initialize optional empty fields:
+ * - the URL with the permalink
+ * - the title with the URL
+ *
+ * Also make sure that we do not save search highlights in the datastore.
+ *
+ * @throws InvalidBookmarkException
+ */
+ public function validate(): void
+ {
+ if (
+ $this->id === null
+ || ! is_int($this->id)
+ || empty($this->shortUrl)
+ || empty($this->created)
+ ) {
+ throw new InvalidBookmarkException($this);
+ }
+ if (empty($this->url)) {
+ $this->url = '/shaare/' . $this->shortUrl;
+ }
+ if (empty($this->title)) {
+ $this->title = $this->url;
+ }
+ if (array_key_exists('search_highlight', $this->additionalContent)) {
+ unset($this->additionalContent['search_highlight']);
+ }
+ }
+
+ /**
+ * Set the Id.
+ * If they're not already initialized, this function also set:
+ * - created: with the current datetime
+ * - shortUrl: with a generated small hash from the date and the given ID
+ *
+ * @param int|null $id
+ *
+ * @return Bookmark
+ */
+ public function setId(?int $id): Bookmark
+ {
+ $this->id = $id;
+ if (empty($this->created)) {
+ $this->created = new DateTime();
+ }
+ if (empty($this->shortUrl)) {
+ $this->shortUrl = link_small_hash($this->created, $this->id);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Get the Id.
+ *
+ * @return int|null
+ */
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ /**
+ * Get the ShortUrl.
+ *
+ * @return string|null
+ */
+ public function getShortUrl(): ?string
+ {
+ return $this->shortUrl;
+ }
+
+ /**
+ * Get the Url.
+ *
+ * @return string|null
+ */
+ public function getUrl(): ?string
+ {
+ return $this->url;
+ }
+
+ /**
+ * Get the Title.
+ *
+ * @return string
+ */
+ public function getTitle(): ?string
+ {
+ return $this->title;
+ }
+
+ /**
+ * Get the Description.
+ *
+ * @return string
+ */
+ public function getDescription(): string
+ {
+ return ! empty($this->description) ? $this->description : '';
+ }
+
+ /**
+ * Get the Created.
+ *
+ * @return DateTimeInterface
+ */
+ public function getCreated(): ?DateTimeInterface
+ {
+ return $this->created;
+ }
+
+ /**
+ * Get the Updated.
+ *
+ * @return DateTimeInterface
+ */
+ public function getUpdated(): ?DateTimeInterface
+ {
+ return $this->updated;
+ }
+
+ /**
+ * Set the ShortUrl.
+ *
+ * @param string|null $shortUrl
+ *
+ * @return Bookmark
+ */
+ public function setShortUrl(?string $shortUrl): Bookmark
+ {
+ $this->shortUrl = $shortUrl;
+
+ return $this;
+ }
+
+ /**
+ * Set the Url.
+ *
+ * @param string|null $url
+ * @param string[] $allowedProtocols
+ *
+ * @return Bookmark
+ */
+ public function setUrl(?string $url, array $allowedProtocols = []): Bookmark
+ {
+ $url = $url !== null ? trim($url) : '';
+ if (! empty($url)) {
+ $url = whitelist_protocols($url, $allowedProtocols);
+ }
+ $this->url = $url;
+
+ return $this;
+ }
+
+ /**
+ * Set the Title.
+ *
+ * @param string|null $title
+ *
+ * @return Bookmark
+ */
+ public function setTitle(?string $title): Bookmark
+ {
+ $this->title = $title !== null ? trim($title) : '';
+
+ return $this;
+ }
+
+ /**
+ * Set the Description.
+ *
+ * @param string|null $description
+ *
+ * @return Bookmark
+ */
+ public function setDescription(?string $description): Bookmark
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * Set the Created.
+ * Note: you shouldn't set this manually except for special cases (like bookmark import)
+ *
+ * @param DateTimeInterface|null $created
+ *
+ * @return Bookmark
+ */
+ public function setCreated(?DateTimeInterface $created): Bookmark
+ {
+ $this->created = $created;
+
+ return $this;
+ }
+
+ /**
+ * Set the Updated.
+ *
+ * @param DateTimeInterface|null $updated
+ *
+ * @return Bookmark
+ */
+ public function setUpdated(?DateTimeInterface $updated): Bookmark
+ {
+ $this->updated = $updated;
+
+ return $this;
+ }
+
+ /**
+ * Get the Private.
+ *
+ * @return bool
+ */
+ public function isPrivate(): bool
+ {
+ return $this->private ? true : false;
+ }
+
+ /**
+ * Set the Private.
+ *
+ * @param bool|null $private
+ *
+ * @return Bookmark
+ */
+ public function setPrivate(?bool $private): Bookmark
+ {
+ $this->private = $private ? true : false;
+
+ return $this;
+ }
+
+ /**
+ * Get the Tags.
+ *
+ * @return string[]
+ */
+ public function getTags(): array
+ {
+ return is_array($this->tags) ? $this->tags : [];
+ }
+
+ /**
+ * Set the Tags.
+ *
+ * @param string[]|null $tags
+ *
+ * @return Bookmark
+ */
+ public function setTags(?array $tags): Bookmark
+ {
+ $this->tags = array_map(
+ function (string $tag): string {
+ return $tag[0] === '-' ? substr($tag, 1) : $tag;
+ },
+ tags_filter($tags, ' ')
+ );
+
+ return $this;
+ }
+
+ /**
+ * Get the Thumbnail.
+ *
+ * @return string|bool|null Thumbnail's URL - initialized at null, false if no thumbnail could be found
+ */
+ public function getThumbnail()
+ {
+ return !$this->isNote() ? $this->thumbnail : false;
+ }
+
+ /**
+ * Set the Thumbnail.
+ *
+ * @param string|bool|null $thumbnail Thumbnail's URL - false if no thumbnail could be found
+ *
+ * @return Bookmark
+ */
+ public function setThumbnail($thumbnail): Bookmark
+ {
+ $this->thumbnail = $thumbnail;
+
+ return $this;
+ }
+
+ /**
+ * Return true if:
+ * - the bookmark's thumbnail is not already set to false (= not found)
+ * - it's not a note
+ * - it's an HTTP(S) link
+ * - the thumbnail has not yet be retrieved (null) or its associated cache file doesn't exist anymore
+ *
+ * @return bool True if the bookmark's thumbnail needs to be retrieved.
+ */
+ public function shouldUpdateThumbnail(): bool
+ {
+ return $this->thumbnail !== false
+ && !$this->isNote()
+ && startsWith(strtolower($this->url), 'http')
+ && (null === $this->thumbnail || !is_file($this->thumbnail))
+ ;
+ }
+
+ /**
+ * Get the Sticky.
+ *
+ * @return bool
+ */
+ public function isSticky(): bool
+ {
+ return $this->sticky ? true : false;
+ }
+
+ /**
+ * Set the Sticky.
+ *
+ * @param bool|null $sticky
+ *
+ * @return Bookmark
+ */
+ public function setSticky(?bool $sticky): Bookmark
+ {
+ $this->sticky = $sticky ? true : false;
+
+ return $this;
+ }
+
+ /**
+ * @param string $separator Tags separator loaded from the config file.
+ *
+ * @return string Bookmark's tags as a string, separated by a separator
+ */
+ public function getTagsString(string $separator = ' '): string
+ {
+ return tags_array2str($this->getTags(), $separator);
+ }
+
+ /**
+ * @return bool
+ */
+ public function isNote(): bool
+ {
+ // We check empty value to get a valid result if the link has not been saved yet
+ return empty($this->url) || startsWith($this->url, '/shaare/') || $this->url[0] === '?';
+ }
+
+ /**
+ * Set tags from a string.
+ * Note:
+ * - tags must be separated whether by a space or a comma
+ * - multiple spaces will be removed
+ * - trailing dash in tags will be removed
+ *
+ * @param string|null $tags
+ * @param string $separator Tags separator loaded from the config file.
+ *
+ * @return $this
+ */
+ public function setTagsString(?string $tags, string $separator = ' '): Bookmark
+ {
+ $this->setTags(tags_str2array($tags, $separator));
+
+ return $this;
+ }
+
+ /**
+ * Get entire additionalContent array.
+ *
+ * @return mixed[]
+ */
+ public function getAdditionalContent(): array
+ {
+ return $this->additionalContent;
+ }
+
+ /**
+ * Set a single entry in additionalContent, by key.
+ *
+ * @param string $key
+ * @param mixed|null $value Any type of value can be set.
+ *
+ * @return $this
+ */
+ public function setAdditionalContentEntry(string $key, $value): self
+ {
+ $this->additionalContent[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Get a single entry in additionalContent, by key.
+ *
+ * @param string $key
+ * @param mixed|null $default
+ *
+ * @return mixed|null can be any type or even null.
+ */
+ public function getAdditionalContentEntry(string $key, $default = null)
+ {
+ return array_key_exists($key, $this->additionalContent) ? $this->additionalContent[$key] : $default;
+ }
+
+ /**
+ * Rename a tag in tags list.
+ *
+ * @param string $fromTag
+ * @param string $toTag
+ */
+ public function renameTag(string $fromTag, string $toTag): void
+ {
+ if (($pos = array_search($fromTag, $this->tags ?? [])) !== false) {
+ $this->tags[$pos] = trim($toTag);
+ }
+ }
+
+ /**
+ * Add a tag in tags list.
+ *
+ * @param string $tag
+ */
+ public function addTag(string $tag): self
+ {
+ return $this->setTags(array_unique(array_merge($this->getTags(), [$tag])));
+ }
+
+ /**
+ * Delete a tag from tags list.
+ *
+ * @param string $tag
+ */
+ public function deleteTag(string $tag): void
+ {
+ while (($pos = array_search($tag, $this->tags ?? [])) !== false) {
+ unset($this->tags[$pos]);
+ $this->tags = array_values($this->tags);
+ }
+ }
+}
diff --git a/application/bookmark/BookmarkArray.php b/application/bookmark/BookmarkArray.php
new file mode 100644
index 00000000..0c1a6eca
--- /dev/null
+++ b/application/bookmark/BookmarkArray.php
@@ -0,0 +1,264 @@
+offset.
+ */
+ protected $ids;
+
+ /**
+ * @var int Position in the $this->keys array (for the Iterator interface)
+ */
+ protected $position;
+
+ /**
+ * @var array List of offset keys (for the Iterator interface implementation)
+ */
+ protected $keys;
+
+ /**
+ * @var array List of all recorded URLs (key=url, value=bookmark offset)
+ * for fast reserve search (url-->bookmark offset)
+ */
+ protected $urls;
+
+ public function __construct()
+ {
+ $this->ids = [];
+ $this->bookmarks = [];
+ $this->keys = [];
+ $this->urls = [];
+ $this->position = 0;
+ }
+
+ /**
+ * Countable - Counts elements of an object
+ *
+ * @return int Number of bookmarks
+ */
+ public function count(): int
+ {
+ return count($this->bookmarks);
+ }
+
+ /**
+ * ArrayAccess - Assigns a value to the specified offset
+ *
+ * @param int $offset Bookmark ID
+ * @param Bookmark $value instance
+ *
+ * @throws InvalidBookmarkException
+ */
+ public function offsetSet($offset, $value): void
+ {
+ if (
+ ! $value instanceof Bookmark
+ || $value->getId() === null || empty($value->getUrl())
+ || ($offset !== null && ! is_int($offset)) || ! is_int($value->getId())
+ || $offset !== null && $offset !== $value->getId()
+ ) {
+ throw new InvalidBookmarkException($value);
+ }
+
+ // If the bookmark exists, we reuse the real offset, otherwise new entry
+ if ($offset !== null) {
+ $existing = $this->getBookmarkOffset($offset);
+ } else {
+ $existing = $this->getBookmarkOffset($value->getId());
+ }
+
+ if ($existing !== null) {
+ $offset = $existing;
+ } else {
+ $offset = count($this->bookmarks);
+ }
+
+ $this->bookmarks[$offset] = $value;
+ $this->urls[$value->getUrl()] = $offset;
+ $this->ids[$value->getId()] = $offset;
+ }
+
+ /**
+ * ArrayAccess - Whether or not an offset exists
+ *
+ * @param int $offset Bookmark ID
+ *
+ * @return bool true if it exists, false otherwise
+ */
+ public function offsetExists($offset): bool
+ {
+ return array_key_exists($this->getBookmarkOffset($offset), $this->bookmarks);
+ }
+
+ /**
+ * ArrayAccess - Unsets an offset
+ *
+ * @param int $offset Bookmark ID
+ */
+ public function offsetUnset($offset): void
+ {
+ $realOffset = $this->getBookmarkOffset($offset);
+ $url = $this->bookmarks[$realOffset]->getUrl();
+ unset($this->urls[$url]);
+ unset($this->ids[$offset]);
+ unset($this->bookmarks[$realOffset]);
+ }
+
+ /**
+ * ArrayAccess - Returns the value at specified offset
+ *
+ * @param int $offset Bookmark ID
+ *
+ * @return Bookmark|null The Bookmark if found, null otherwise
+ */
+ public function offsetGet($offset): ?Bookmark
+ {
+ $realOffset = $this->getBookmarkOffset($offset);
+ return isset($this->bookmarks[$realOffset]) ? $this->bookmarks[$realOffset] : null;
+ }
+
+ /**
+ * Iterator - Returns the current element
+ *
+ * @return Bookmark corresponding to the current position
+ */
+ public function current(): Bookmark
+ {
+ return $this[$this->keys[$this->position]];
+ }
+
+ /**
+ * Iterator - Returns the key of the current element
+ *
+ * @return int Bookmark ID corresponding to the current position
+ */
+ public function key(): int
+ {
+ return $this->keys[$this->position];
+ }
+
+ /**
+ * Iterator - Moves forward to next element
+ */
+ public function next(): void
+ {
+ ++$this->position;
+ }
+
+ /**
+ * Iterator - Rewinds the Iterator to the first element
+ *
+ * Entries are sorted by date (latest first)
+ */
+ public function rewind(): void
+ {
+ $this->keys = array_keys($this->ids);
+ $this->position = 0;
+ }
+
+ /**
+ * Iterator - Checks if current position is valid
+ *
+ * @return bool true if the current Bookmark ID exists, false otherwise
+ */
+ public function valid(): bool
+ {
+ return isset($this->keys[$this->position]);
+ }
+
+ /**
+ * Returns a bookmark offset in bookmarks array from its unique ID.
+ *
+ * @param int|null $id Persistent ID of a bookmark.
+ *
+ * @return int Real offset in local array, or null if doesn't exist.
+ */
+ protected function getBookmarkOffset(?int $id): ?int
+ {
+ if ($id !== null && isset($this->ids[$id])) {
+ return $this->ids[$id];
+ }
+ return null;
+ }
+
+ /**
+ * Return the next key for bookmark creation.
+ * E.g. If the last ID is 597, the next will be 598.
+ *
+ * @return int next ID.
+ */
+ public function getNextId(): int
+ {
+ if (!empty($this->ids)) {
+ return max(array_keys($this->ids)) + 1;
+ }
+ return 0;
+ }
+
+ /**
+ * @param string $url
+ *
+ * @return Bookmark|null
+ */
+ public function getByUrl(string $url): ?Bookmark
+ {
+ if (
+ ! empty($url)
+ && isset($this->urls[$url])
+ && isset($this->bookmarks[$this->urls[$url]])
+ ) {
+ return $this->bookmarks[$this->urls[$url]];
+ }
+ return null;
+ }
+
+ /**
+ * Reorder links by creation date (newest first).
+ *
+ * Also update the urls and ids mapping arrays.
+ *
+ * @param string $order ASC|DESC
+ * @param bool $ignoreSticky If set to true, sticky bookmarks won't be first
+ */
+ public function reorder(string $order = 'DESC', bool $ignoreSticky = false): void
+ {
+ $order = $order === 'ASC' ? -1 : 1;
+ // Reorder array by dates.
+ usort($this->bookmarks, function ($a, $b) use ($order, $ignoreSticky) {
+ /** @var $a Bookmark */
+ /** @var $b Bookmark */
+ if (false === $ignoreSticky && $a->isSticky() !== $b->isSticky()) {
+ return $a->isSticky() ? -1 : 1;
+ }
+ return $a->getCreated() < $b->getCreated() ? 1 * $order : -1 * $order;
+ });
+
+ $this->urls = [];
+ $this->ids = [];
+ foreach ($this->bookmarks as $key => $bookmark) {
+ $this->urls[$bookmark->getUrl()] = $key;
+ $this->ids[$bookmark->getId()] = $key;
+ }
+ }
+}
diff --git a/application/bookmark/BookmarkFileService.php b/application/bookmark/BookmarkFileService.php
new file mode 100644
index 00000000..9faf1c3b
--- /dev/null
+++ b/application/bookmark/BookmarkFileService.php
@@ -0,0 +1,443 @@
+conf = $conf;
+ $this->history = $history;
+ $this->mutex = $mutex;
+ $this->pageCacheManager = new PageCacheManager($this->conf->get('resource.page_cache'), $isLoggedIn);
+ $this->bookmarksIO = new BookmarkIO($this->conf, $this->mutex);
+ $this->isLoggedIn = $isLoggedIn;
+
+ if (!$this->isLoggedIn && $this->conf->get('privacy.hide_public_links', false)) {
+ $this->bookmarks = new BookmarkArray();
+ } else {
+ try {
+ $this->bookmarks = $this->bookmarksIO->read();
+ } catch (EmptyDataStoreException | DatastoreNotInitializedException $e) {
+ $this->bookmarks = new BookmarkArray();
+
+ if ($this->isLoggedIn) {
+ // Datastore file does not exists, we initialize it with default bookmarks.
+ if ($e instanceof DatastoreNotInitializedException) {
+ $this->initialize();
+ } else {
+ $this->save();
+ }
+ }
+ }
+
+ if (! $this->bookmarks instanceof BookmarkArray) {
+ $this->migrate();
+ exit(
+ 'Your data store has been migrated, please reload the page.' . PHP_EOL .
+ 'If this message keeps showing up, please delete data/updates.txt file.'
+ );
+ }
+ }
+
+ $this->pluginManager = $pluginManager;
+ $this->bookmarkFilter = new BookmarkFilter($this->bookmarks, $this->conf, $this->pluginManager);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function findByHash(string $hash, string $privateKey = null): Bookmark
+ {
+ $bookmark = $this->bookmarkFilter->filter(BookmarkFilter::$FILTER_HASH, $hash);
+ // PHP 7.3 introduced array_key_first() to avoid this hack
+ $first = reset($bookmark);
+ if (
+ !$this->isLoggedIn
+ && $first->isPrivate()
+ && (empty($privateKey) || $privateKey !== $first->getAdditionalContentEntry('private_key'))
+ ) {
+ throw new BookmarkNotFoundException();
+ }
+
+ return $first;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function findByUrl(string $url): ?Bookmark
+ {
+ return $this->bookmarks->getByUrl($url);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function search(
+ array $request = [],
+ string $visibility = null,
+ bool $caseSensitive = false,
+ bool $untaggedOnly = false,
+ bool $ignoreSticky = false,
+ array $pagination = []
+ ): SearchResult {
+ if ($visibility === null) {
+ $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
+ }
+
+ // Filter bookmark database according to parameters.
+ $searchTags = isset($request['searchtags']) ? $request['searchtags'] : '';
+ $searchTerm = isset($request['searchterm']) ? $request['searchterm'] : '';
+
+ if ($ignoreSticky) {
+ $this->bookmarks->reorder('DESC', true);
+ }
+
+ $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
+ );
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function get(int $id, string $visibility = null): Bookmark
+ {
+ if (! isset($this->bookmarks[$id])) {
+ throw new BookmarkNotFoundException();
+ }
+
+ if ($visibility === null) {
+ $visibility = $this->isLoggedIn ? BookmarkFilter::$ALL : BookmarkFilter::$PUBLIC;
+ }
+
+ $bookmark = $this->bookmarks[$id];
+ if (
+ ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
+ || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
+ ) {
+ throw new Exception('Unauthorized');
+ }
+
+ return $bookmark;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function set(Bookmark $bookmark, bool $save = true): Bookmark
+ {
+ if (true !== $this->isLoggedIn) {
+ throw new Exception(t('You\'re not authorized to alter the datastore'));
+ }
+ if (! isset($this->bookmarks[$bookmark->getId()])) {
+ throw new BookmarkNotFoundException();
+ }
+ $bookmark->validate();
+
+ $bookmark->setUpdated(new DateTime());
+ $this->bookmarks[$bookmark->getId()] = $bookmark;
+ if ($save === true) {
+ $this->save();
+ $this->history->updateLink($bookmark);
+ }
+ return $this->bookmarks[$bookmark->getId()];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function add(Bookmark $bookmark, bool $save = true): Bookmark
+ {
+ if (true !== $this->isLoggedIn) {
+ throw new Exception(t('You\'re not authorized to alter the datastore'));
+ }
+ if (!empty($bookmark->getId())) {
+ throw new Exception(t('This bookmarks already exists'));
+ }
+ $bookmark->setId($this->bookmarks->getNextId());
+ $bookmark->validate();
+
+ $this->bookmarks[$bookmark->getId()] = $bookmark;
+ if ($save === true) {
+ $this->save();
+ $this->history->addLink($bookmark);
+ }
+ return $this->bookmarks[$bookmark->getId()];
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function addOrSet(Bookmark $bookmark, bool $save = true): Bookmark
+ {
+ if (true !== $this->isLoggedIn) {
+ throw new Exception(t('You\'re not authorized to alter the datastore'));
+ }
+ if ($bookmark->getId() === null) {
+ return $this->add($bookmark, $save);
+ }
+ return $this->set($bookmark, $save);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function remove(Bookmark $bookmark, bool $save = true): void
+ {
+ if (true !== $this->isLoggedIn) {
+ throw new Exception(t('You\'re not authorized to alter the datastore'));
+ }
+ if (! isset($this->bookmarks[$bookmark->getId()])) {
+ throw new BookmarkNotFoundException();
+ }
+
+ unset($this->bookmarks[$bookmark->getId()]);
+ if ($save === true) {
+ $this->save();
+ $this->history->deleteLink($bookmark);
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function exists(int $id, string $visibility = null): bool
+ {
+ if (! isset($this->bookmarks[$id])) {
+ return false;
+ }
+
+ if ($visibility === null) {
+ $visibility = $this->isLoggedIn ? 'all' : 'public';
+ }
+
+ $bookmark = $this->bookmarks[$id];
+ if (
+ ($bookmark->isPrivate() && $visibility != 'all' && $visibility != 'private')
+ || (! $bookmark->isPrivate() && $visibility != 'all' && $visibility != 'public')
+ ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function count(string $visibility = null): int
+ {
+ return $this->search([], $visibility)->getResultCount();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save(): void
+ {
+ if (true !== $this->isLoggedIn) {
+ // TODO: raise an Exception instead
+ die('You are not authorized to change the database.');
+ }
+
+ $this->bookmarks->reorder();
+ $this->bookmarksIO->write($this->bookmarks);
+ $this->pageCacheManager->invalidateCaches();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function bookmarksCountPerTag(array $filteringTags = [], string $visibility = null): array
+ {
+ $searchResult = $this->search(['searchtags' => $filteringTags], $visibility);
+ $tags = [];
+ $caseMapping = [];
+ foreach ($searchResult->getBookmarks() as $bookmark) {
+ foreach ($bookmark->getTags() as $tag) {
+ if (
+ empty($tag)
+ || (! $this->isLoggedIn && startsWith($tag, '.'))
+ || $tag === BookmarkMarkdownFormatter::NO_MD_TAG
+ || in_array($tag, $filteringTags, true)
+ ) {
+ continue;
+ }
+
+ // The first case found will be displayed.
+ if (!isset($caseMapping[strtolower($tag)])) {
+ $caseMapping[strtolower($tag)] = $tag;
+ $tags[$caseMapping[strtolower($tag)]] = 0;
+ }
+ $tags[$caseMapping[strtolower($tag)]]++;
+ }
+ }
+
+ /*
+ * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
+ * Also, this function doesn't produce the same result between PHP 5.6 and 7.
+ *
+ * So we now use array_multisort() to sort tags by DESC occurrences,
+ * then ASC alphabetically for equal values.
+ *
+ * @see https://github.com/shaarli/Shaarli/issues/1142
+ */
+ $keys = array_keys($tags);
+ $tmpTags = array_combine($keys, $keys);
+ array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
+
+ return $tags;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function findByDate(
+ \DateTimeInterface $from,
+ \DateTimeInterface $to,
+ ?\DateTimeInterface &$previous,
+ ?\DateTimeInterface &$next
+ ): array {
+ $out = [];
+ $previous = null;
+ $next = null;
+
+ foreach ($this->search([], null, false, false, true)->getBookmarks() as $bookmark) {
+ if ($to < $bookmark->getCreated()) {
+ $next = $bookmark->getCreated();
+ } elseif ($from < $bookmark->getCreated() && $to > $bookmark->getCreated()) {
+ $out[] = $bookmark;
+ } else {
+ if ($previous !== null) {
+ break;
+ }
+ $previous = $bookmark->getCreated();
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getLatest(): ?Bookmark
+ {
+ foreach ($this->search([], null, false, false, true)->getBookmarks() as $bookmark) {
+ return $bookmark;
+ }
+
+ return null;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function initialize(): void
+ {
+ $initializer = new BookmarkInitializer($this);
+ $initializer->initialize();
+
+ if (true === $this->isLoggedIn) {
+ $this->save();
+ }
+ }
+
+ /**
+ * Handles migration to the new database format (BookmarksArray).
+ */
+ protected function migrate(): void
+ {
+ $bookmarkDb = new LegacyLinkDB(
+ $this->conf->get('resource.datastore'),
+ true,
+ false
+ );
+ $updater = new LegacyUpdater(
+ UpdaterUtils::readUpdatesFile($this->conf->get('resource.updates')),
+ $bookmarkDb,
+ $this->conf,
+ true
+ );
+ $newUpdates = $updater->update();
+ if (! empty($newUpdates)) {
+ UpdaterUtils::writeUpdatesFile(
+ $this->conf->get('resource.updates'),
+ $updater->getDoneUpdates()
+ );
+ }
+ }
+}
diff --git a/application/bookmark/BookmarkFilter.php b/application/bookmark/BookmarkFilter.php
new file mode 100644
index 00000000..00cbba06
--- /dev/null
+++ b/application/bookmark/BookmarkFilter.php
@@ -0,0 +1,635 @@
+bookmarks = $bookmarks;
+ $this->conf = $conf;
+ $this->pluginManager = $pluginManager;
+ }
+
+ /**
+ * Filter bookmarks according to parameters.
+ *
+ * @param string $type Type of filter (eg. tags, permalink, etc.).
+ * @param mixed $request Filter content.
+ * @param bool $casesensitive Optional: Perform case sensitive filter if true.
+ * @param string $visibility Optional: return only all/private/public bookmarks
+ * @param bool $untaggedonly Optional: return only untagged bookmarks. Applies only if $type includes FILTER_TAG
+ *
+ * @return Bookmark[] filtered bookmark list.
+ *
+ * @throws BookmarkNotFoundException
+ */
+ public function filter(
+ string $type,
+ $request,
+ bool $casesensitive = false,
+ string $visibility = 'all',
+ bool $untaggedonly = false
+ ) {
+ if (!in_array($visibility, ['all', 'public', 'private'])) {
+ $visibility = 'all';
+ }
+
+ switch ($type) {
+ case self::$FILTER_HASH:
+ return $this->filterSmallHash($request);
+ case self::$FILTER_TAG | self::$FILTER_TEXT: // == "vuotext"
+ $noRequest = empty($request) || (empty($request[0]) && empty($request[1]));
+ if ($noRequest) {
+ if ($untaggedonly) {
+ return $this->filterUntagged($visibility);
+ }
+ return $this->noFilter($visibility);
+ }
+ if ($untaggedonly) {
+ $filtered = $this->filterUntagged($visibility);
+ } else {
+ $filtered = $this->bookmarks;
+ }
+ if (!empty($request[0])) {
+ $filtered = (new BookmarkFilter($filtered, $this->conf, $this->pluginManager))
+ ->filterTags($request[0], $casesensitive, $visibility)
+ ;
+ }
+ if (!empty($request[1])) {
+ $filtered = (new BookmarkFilter($filtered, $this->conf, $this->pluginManager))
+ ->filterFulltext($request[1], $visibility)
+ ;
+ }
+ return $filtered;
+ case self::$FILTER_TEXT:
+ return $this->filterFulltext($request, $visibility);
+ case self::$FILTER_TAG:
+ if ($untaggedonly) {
+ return $this->filterUntagged($visibility);
+ } else {
+ return $this->filterTags($request, $casesensitive, $visibility);
+ }
+ default:
+ return $this->noFilter($visibility);
+ }
+ }
+
+ /**
+ * Unknown filter, but handle private only.
+ *
+ * @param string $visibility Optional: return only all/private/public bookmarks
+ *
+ * @return Bookmark[] filtered bookmarks.
+ */
+ private function noFilter(string $visibility = 'all')
+ {
+ $out = [];
+ foreach ($this->bookmarks as $key => $value) {
+ 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;
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Returns the shaare corresponding to a smallHash.
+ *
+ * @param string $smallHash permalink hash.
+ *
+ * @return Bookmark[] $filtered array containing permalink data.
+ *
+ * @throws BookmarkNotFoundException if the smallhash doesn't match any link.
+ */
+ private function filterSmallHash(string $smallHash)
+ {
+ foreach ($this->bookmarks as $key => $l) {
+ if ($smallHash == $l->getShortUrl()) {
+ // Yes, this is ugly and slow
+ return [$key => $l];
+ }
+ }
+
+ throw new BookmarkNotFoundException();
+ }
+
+ /**
+ * Returns the list of bookmarks corresponding to a full-text search
+ *
+ * Searches:
+ * - in the URLs, title and description;
+ * - are case-insensitive;
+ * - terms surrounded by quotes " are exact terms search.
+ * - terms starting with a dash - are excluded (except exact terms).
+ *
+ * Example:
+ * print_r($mydb->filterFulltext('hollandais'));
+ *
+ * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8')
+ * - allows to perform searches on Unicode text
+ * - see https://github.com/shaarli/Shaarli/issues/75 for examples
+ *
+ * @param string $searchterms search query.
+ * @param string $visibility Optional: return only all/private/public bookmarks.
+ *
+ * @return Bookmark[] search results.
+ */
+ private function filterFulltext(string $searchterms, string $visibility = 'all')
+ {
+ if (empty($searchterms)) {
+ return $this->noFilter($visibility);
+ }
+
+ $filtered = [];
+ $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
+ $exactRegex = '/"([^"]+)"/';
+ // Retrieve exact search terms.
+ preg_match_all($exactRegex, $search, $exactSearch);
+ $exactSearch = array_values(array_filter($exactSearch[1]));
+
+ // Remove exact search terms to get AND terms search.
+ $explodedSearchAnd = explode(' ', trim(preg_replace($exactRegex, '', $search)));
+ $explodedSearchAnd = array_values(array_filter($explodedSearchAnd));
+
+ // Filter excluding terms and update andSearch.
+ $excludeSearch = [];
+ $andSearch = [];
+ foreach ($explodedSearchAnd as $needle) {
+ if ($needle[0] == '-' && strlen($needle) > 1) {
+ $excludeSearch[] = substr($needle, 1);
+ } else {
+ $andSearch[] = $needle;
+ }
+ }
+
+ // Iterate over every stored 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 (!$bookmark->isPrivate() && $visibility === 'private') {
+ continue;
+ } elseif ($bookmark->isPrivate() && $visibility === 'public') {
+ continue;
+ }
+ }
+
+ $lengths = [];
+ $content = $this->buildFullTextSearchableLink($bookmark, $lengths);
+
+ // Be optimistic
+ $found = true;
+ $foundPositions = [];
+
+ // First, we look for exact term search
+ // Then iterate over keywords, if keyword is not found,
+ // no need to check for the others. We want all or nothing.
+ foreach ([$exactSearch, $andSearch] as $search) {
+ for ($i = 0; $i < count($search) && $found !== false; $i++) {
+ $found = mb_strpos($content, $search[$i]);
+ if ($found === false) {
+ break;
+ }
+
+ $foundPositions[] = ['start' => $found, 'end' => $found + mb_strlen($search[$i])];
+ }
+ }
+
+ // Exclude terms.
+ for ($i = 0; $i < count($excludeSearch) && $found !== false; $i++) {
+ $found = strpos($content, $excludeSearch[$i]) === false;
+ }
+
+ if ($found !== false) {
+ $bookmark->setAdditionalContentEntry(
+ 'search_highlight',
+ $this->postProcessFoundPositions($lengths, $foundPositions)
+ );
+
+ $filtered[$id] = $bookmark;
+ }
+ }
+
+ return $filtered;
+ }
+
+ /**
+ * Returns the list of bookmarks associated with a given list of tags
+ *
+ * You can specify one or more tags, separated by space or a comma, e.g.
+ * print_r($mydb->filterTags('linux programming'));
+ *
+ * @param string|array $tags list of tags, separated by commas or blank spaces if passed as string.
+ * @param bool $casesensitive ignore case if false.
+ * @param string $visibility Optional: return only all/private/public bookmarks.
+ *
+ * @return Bookmark[] filtered bookmarks.
+ */
+ public function filterTags($tags, bool $casesensitive = false, string $visibility = 'all')
+ {
+ $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
+ // get single tags (we may get passed an array, even though the docs say different)
+ $inputTags = $tags;
+ if (!is_array($tags)) {
+ // we got an input string, split tags
+ $inputTags = tags_str2array($inputTags, $tagsSeparator);
+ }
+ if (count($inputTags) === 0) {
+ // no input tags
+ return $this->noFilter($visibility);
+ }
+
+ // If we only have public visibility, we can't look for hidden tags
+ if ($visibility === self::$PUBLIC) {
+ $inputTags = array_values(array_filter($inputTags, function ($tag) {
+ return ! startsWith($tag, '.');
+ }));
+
+ if (empty($inputTags)) {
+ return [];
+ }
+ }
+
+ // build regex from all tags
+ $re_and = implode(array_map([$this, 'tag2regex'], $inputTags));
+ $re = '/^' . $re_and;
+
+ $orTags = array_filter(array_map(function ($tag) {
+ return startsWith($tag, '~') ? substr($tag, 1) : null;
+ }, $inputTags));
+
+ $re_or = implode('|', array_map([$this, 'tag2matchterm'], $orTags));
+ if ($re_or) {
+ $re_or = '(' . $re_or . ')';
+ $re .= $this->term2match($re_or, false);
+ }
+
+ $re .= '.*$/';
+ if (!$casesensitive) {
+ // make regex case insensitive
+ $re .= 'i';
+ }
+
+ // create resulting array
+ $filtered = [];
+
+ // iterate over each 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 (!$bookmark->isPrivate() && $visibility === 'private') {
+ continue;
+ } elseif ($bookmark->isPrivate() && $visibility === 'public') {
+ continue;
+ }
+ }
+ // build search string, start with tags of current link
+ $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(),
+ $descTags
+ );
+ if (count($descTags[1])) {
+ // there were some tags in the description, add them to the search string
+ $search .= $tagsSeparator . tags_array2str($descTags[1], $tagsSeparator);
+ }
+ }
+ // match regular expression with search string
+ if (!preg_match($re, $search)) {
+ // this entry does _not_ match our regex
+ continue;
+ }
+ $filtered[$key] = $bookmark;
+ }
+
+ return $filtered;
+ }
+
+ /**
+ * Return only bookmarks without any tag.
+ *
+ * @param string $visibility return only all/private/public bookmarks.
+ *
+ * @return Bookmark[] filtered bookmarks.
+ */
+ public function filterUntagged(string $visibility)
+ {
+ $filtered = [];
+ foreach ($this->bookmarks as $key => $bookmark) {
+ if (
+ !$this->pluginManager->filterSearchEntry(
+ $bookmark,
+ ['source' => 'untagged', 'visibility' => $visibility]
+ )
+ ) {
+ continue;
+ }
+
+ if ($visibility !== 'all') {
+ if (!$bookmark->isPrivate() && $visibility === 'private') {
+ continue;
+ } elseif ($bookmark->isPrivate() && $visibility === 'public') {
+ continue;
+ }
+ }
+
+ if (empty($bookmark->getTags())) {
+ $filtered[$key] = $bookmark;
+ }
+ }
+
+ return $filtered;
+ }
+
+ /**
+ * Convert a list of tags (str) to an array. Also
+ * - handle case sensitivity.
+ * - accepts spaces commas as separator.
+ *
+ * @param string $tags string containing a list of tags.
+ * @param bool $casesensitive will convert everything to lowercase if false.
+ *
+ * @return string[] filtered tags string.
+ */
+ public static function tagsStrToArray(string $tags, bool $casesensitive): array
+ {
+ // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
+ $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
+ $tagsOut = str_replace(',', ' ', $tagsOut);
+
+ return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
+ }
+
+ /**
+ * generate a regex fragment out of a tag
+ *
+ * @param string $tag to generate regexs from. may start with '-'
+ * to negate, contain '*' as wildcard. Tags starting with '~' are
+ * treated separately as an 'OR' clause.
+ *
+ * @return string generated regex fragment
+ */
+ protected function tag2regex(string $tag): string
+ {
+ $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
+ if (!$tag || $tag === "-" || $tag === "*" || $tag[0] === "~") {
+ // nothing to search, return empty regex
+ return '';
+ }
+ $negate = false;
+ if ($tag[0] === "+" && $tag[1]) {
+ $tag = substr($tag, 1); // use offset to start after '+' character
+ }
+ if ($tag[0] === "-") {
+ // query is negated
+ $tag = substr($tag, 1); // use offset to start after '-' character
+ $negate = true;
+ }
+ $term = $this->tag2matchterm($tag);
+
+ return $this->term2match($term, $negate);
+ }
+
+ /**
+ * generate a regex match term fragment out of a tag
+ *
+ * @param string $tag to to generate regexs from. This function
+ * assumes any leading flags ('-', '~') have been stripped. The
+ * wildcard flag '*' is expanded by this function and any other
+ * regex characters are escaped.
+ *
+ * @return string generated regex match term fragment
+ */
+ protected function tag2matchterm(string $tag): string
+ {
+ $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
+ $len = strlen($tag);
+ $term = '';
+ // iterate over string, separating it into placeholder and content
+ $i = 0; // start at first character
+ for (; $i < $len; $i++) {
+ if ($tag[$i] === '*') {
+ // placeholder found
+ $term .= '[^' . $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.
+ $term .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
+ // move $i on
+ $i = $offset;
+ }
+ }
+
+ return $term;
+ }
+
+ /**
+ * generate a regex fragment out of a match term
+ *
+ * @param string $term is the match term already generated by tag2matchterm
+ * @param bool $negate if true create a negative lookahead
+ *
+ * @return string generated regex fragment
+ */
+ protected function term2match(string $term, bool $negate): string
+ {
+ $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
+ $regex = $negate ? '(?!' : '(?='; // use negative or positive lookahead
+
+ // before tag may only be the separator or the beginning
+ $regex .= '.*(?:^|' . $tagsSeparator . ')';
+
+ $regex .= $term;
+
+ // 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,
+ * making sure that there is no overlapping results, etc.
+ *
+ * @param array $fieldLengths Start and end positions of every bookmark fields in the aggregated bookmark content.
+ * @param array $foundPositions Positions where the search results were found in the aggregated content.
+ *
+ * @return array Updated $foundPositions, by bookmark field.
+ */
+ protected function postProcessFoundPositions(array $fieldLengths, array $foundPositions): array
+ {
+ // Sort results by starting position ASC.
+ usort($foundPositions, function (array $entryA, array $entryB): int {
+ return $entryA['start'] > $entryB['start'] ? 1 : -1;
+ });
+
+ $out = [];
+ $currentMax = -1;
+ foreach ($foundPositions as $foundPosition) {
+ // we do not allow overlapping highlights
+ if ($foundPosition['start'] < $currentMax) {
+ continue;
+ }
+
+ $currentMax = $foundPosition['end'];
+ foreach ($fieldLengths as $part => $length) {
+ if ($foundPosition['start'] < $length['start'] || $foundPosition['start'] > $length['end']) {
+ continue;
+ }
+
+ $out[$part][] = [
+ 'start' => $foundPosition['start'] - $length['start'],
+ 'end' => $foundPosition['end'] - $length['start'],
+ ];
+ break;
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Concatenate link fields to search across fields. Adds a '\' separator for exact search terms.
+ * Also populate $length array with starting and ending positions of every bookmark field
+ * inside concatenated content.
+ *
+ * @param Bookmark $link
+ * @param array $lengths (by reference)
+ *
+ * @return string Lowercase concatenated fields content.
+ */
+ protected function buildFullTextSearchableLink(Bookmark $link, array &$lengths): string
+ {
+ $tagString = $link->getTagsString($this->conf->get('general.tags_separator', ' '));
+ $content = mb_convert_case($link->getTitle(), MB_CASE_LOWER, 'UTF-8') . '\\';
+ $content .= mb_convert_case($link->getDescription(), MB_CASE_LOWER, 'UTF-8') . '\\';
+ $content .= mb_convert_case($link->getUrl(), MB_CASE_LOWER, 'UTF-8') . '\\';
+ $content .= mb_convert_case($tagString, MB_CASE_LOWER, 'UTF-8') . '\\';
+
+ $lengths['title'] = ['start' => 0, 'end' => mb_strlen($link->getTitle())];
+ $nextField = $lengths['title']['end'] + 1;
+ $lengths['description'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getDescription())];
+ $nextField = $lengths['description']['end'] + 1;
+ $lengths['url'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($link->getUrl())];
+ $nextField = $lengths['url']['end'] + 1;
+ $lengths['tags'] = ['start' => $nextField, 'end' => $nextField + mb_strlen($tagString)];
+
+ return $content;
+ }
+}
diff --git a/application/bookmark/BookmarkIO.php b/application/bookmark/BookmarkIO.php
new file mode 100644
index 00000000..fa5cefb5
--- /dev/null
+++ b/application/bookmark/BookmarkIO.php
@@ -0,0 +1,173 @@
+';
+
+ /**
+ * LinksIO constructor.
+ *
+ * @param ConfigManager $conf instance
+ */
+ public function __construct(ConfigManager $conf, Mutex $mutex = null)
+ {
+ if ($mutex === null) {
+ // This should only happen with legacy classes
+ $mutex = new NoMutex();
+ }
+ $this->conf = $conf;
+ $this->datastore = $conf->get('resource.datastore');
+ $this->mutex = $mutex;
+ }
+
+ /**
+ * Reads database from disk to memory
+ *
+ * @return Bookmark[]
+ *
+ * @throws NotWritableDataStoreException Data couldn't be loaded
+ * @throws EmptyDataStoreException Datastore file exists but does not contain any bookmark
+ * @throws DatastoreNotInitializedException File does not exists
+ */
+ public function read()
+ {
+ if (! file_exists($this->datastore)) {
+ throw new DatastoreNotInitializedException();
+ }
+
+ if (!is_writable($this->datastore)) {
+ throw new NotWritableDataStoreException($this->datastore);
+ }
+
+ $content = null;
+ $this->synchronized(function () use (&$content) {
+ $content = file_get_contents($this->datastore);
+ });
+
+ // Note that gzinflate is faster than gzuncompress.
+ // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
+ $links = unserialize(gzinflate(base64_decode(
+ substr($content, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
+ )));
+
+ if (empty($links)) {
+ if (filesize($this->datastore) > 100) {
+ throw new NotWritableDataStoreException($this->datastore);
+ }
+ throw new EmptyDataStoreException();
+ }
+
+ return $links;
+ }
+
+ /**
+ * Saves the database from memory to disk
+ *
+ * @param Bookmark[] $links
+ *
+ * @throws NotWritableDataStoreException the datastore is not writable
+ * @throws InvalidWritableDataException
+ */
+ public function write($links)
+ {
+ if (is_file($this->datastore) && !is_writeable($this->datastore)) {
+ // The datastore exists but is not writeable
+ throw new NotWritableDataStoreException($this->datastore);
+ } elseif (!is_file($this->datastore) && !is_writeable(dirname($this->datastore))) {
+ // The datastore does not exist and its parent directory is not writeable
+ throw new NotWritableDataStoreException(dirname($this->datastore));
+ }
+
+ $data = base64_encode(gzdeflate(serialize($links)));
+
+ if (empty($data)) {
+ throw new InvalidWritableDataException();
+ }
+
+ $data = self::$phpPrefix . $data . self::$phpSuffix;
+
+ $this->synchronized(function () use ($data) {
+ if (!$this->checkDiskSpace($data)) {
+ throw new NotEnoughSpaceException();
+ }
+
+ 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();
+ }
+ }
+
+ /**
+ * Make sure that there is enough disk space available to save the current data store.
+ * We add an arbitrary margin of 500kB.
+ *
+ * @param string $data to be saved
+ *
+ * @return bool True if data can safely be saved
+ */
+ public function checkDiskSpace(string $data): bool
+ {
+ return disk_free_space(dirname($this->datastore)) > (strlen($data) + 1024 * 500);
+ }
+}
diff --git a/application/bookmark/BookmarkInitializer.php b/application/bookmark/BookmarkInitializer.php
new file mode 100644
index 00000000..8ab5c441
--- /dev/null
+++ b/application/bookmark/BookmarkInitializer.php
@@ -0,0 +1,115 @@
+bookmarkService = $bookmarkService;
+ }
+
+ /**
+ * Initialize the data store with default bookmarks
+ */
+ public function initialize(): void
+ {
+ $bookmark = new Bookmark();
+ $bookmark->setTitle('Calm Jazz Music - YouTube ' . t('(private bookmark with thumbnail demo)'));
+ $bookmark->setUrl('https://www.youtube.com/watch?v=DVEUcbPkb-c');
+ $bookmark->setDescription(t(
+ 'Shaarli will automatically pick up the thumbnail for links to a variety of websites.
+
+Explore your new Shaarli instance by trying out controls and menus.
+Visit the project on [Github](https://github.com/shaarli/Shaarli) or [the documentation](https://shaarli.readthedocs.io/en/master/) to learn more about Shaarli.
+
+Now you can edit or delete the default shaares.
+'
+ ));
+ $bookmark->setTagsString('shaarli help thumbnail');
+ $bookmark->setPrivate(true);
+ $this->bookmarkService->add($bookmark, false);
+
+ $bookmark = new Bookmark();
+ $bookmark->setTitle(t('Note: Shaare descriptions'));
+ $bookmark->setDescription(t(
+ 'Adding a shaare without entering a URL creates a text-only "note" post such as this one.
+This note is private, so you are the only one able to see it while logged in.
+
+You can use this to keep notes, post articles, code snippets, and much more.
+
+The Markdown formatting setting allows you to format your notes and bookmark description:
+
+### Title headings
+
+#### Multiple headings levels
+ * bullet lists
+ * _italic_ text
+ * **bold** text
+ * ~~strike through~~ text
+ * `code` blocks
+ * images
+ * [links](https://en.wikipedia.org/wiki/Markdown)
+
+Markdown also supports tables:
+
+| Name | Type | Color | Qty |
+| ------- | --------- | ------ | ----- |
+| Orange | Fruit | Orange | 126 |
+| Apple | Fruit | Any | 62 |
+| Lemon | Fruit | Yellow | 30 |
+| Carrot | Vegetable | Red | 14 |
+'
+ ));
+ $bookmark->setTagsString('shaarli help');
+ $bookmark->setPrivate(true);
+ $this->bookmarkService->add($bookmark, false);
+
+ $bookmark = new Bookmark();
+ $bookmark->setTitle(
+ 'Shaarli - ' . t('The personal, minimalist, super-fast, database free, bookmarking service')
+ );
+ $bookmark->setDescription(t(
+ 'Welcome to Shaarli!
+
+Shaarli allows you to bookmark your favorite pages, and share them with others or store them privately.
+You can add a description to your bookmarks, such as this one, and tag them.
+
+Create a new shaare by clicking the `+Shaare` button, or using any of the recommended tools (browser extension, mobile app, bookmarklet, REST API, etc.).
+
+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`).
+Hashtags such as #shaarli #help are also supported.
+You can also filter the available [RSS feed](/feed/atom) and picture wall by tag or plaintext search.
+
+We hope that you will enjoy using Shaarli, maintained with ❤️ by the community!
+Feel free to open [an issue](https://github.com/shaarli/Shaarli/issues) if you have a suggestion or encounter an issue.
+'
+ ));
+ $bookmark->setTagsString('shaarli help');
+ $this->bookmarkService->add($bookmark, false);
+ }
+}
diff --git a/application/bookmark/BookmarkServiceInterface.php b/application/bookmark/BookmarkServiceInterface.php
new file mode 100644
index 00000000..4b1f0daa
--- /dev/null
+++ b/application/bookmark/BookmarkServiceInterface.php
@@ -0,0 +1,189 @@
+ bookmarksCount
+ */
+ public function bookmarksCountPerTag(array $filteringTags = [], ?string $visibility = null): array;
+
+ /**
+ * Return a list of bookmark matching provided period of time.
+ * It also update directly previous and next date outside of given period found in the datastore.
+ *
+ * @param \DateTimeInterface $from Starting date.
+ * @param \DateTimeInterface $to Ending date.
+ * @param \DateTimeInterface|null $previous (by reference) updated with first created date found before $from.
+ * @param \DateTimeInterface|null $next (by reference) updated with first created date found after $to.
+ *
+ * @return array List of bookmarks matching provided period of time.
+ */
+ public function findByDate(
+ \DateTimeInterface $from,
+ \DateTimeInterface $to,
+ ?\DateTimeInterface &$previous,
+ ?\DateTimeInterface &$next
+ ): array;
+
+ /**
+ * Returns the latest bookmark by creation date.
+ *
+ * @return Bookmark|null Found Bookmark or null if the datastore is empty.
+ */
+ public function getLatest(): ?Bookmark;
+
+ /**
+ * Creates the default database after a fresh install.
+ */
+ public function initialize(): void;
+}
diff --git a/application/bookmark/LinkUtils.php b/application/bookmark/LinkUtils.php
index 77eb2d95..601e352b 100644
--- a/application/bookmark/LinkUtils.php
+++ b/application/bookmark/LinkUtils.php
@@ -1,112 +1,7 @@
]+(?:'. $properties .')=["\']?(?:og:)?'. $tag .'["\'\s][^>]*content=["\']?(.*?)["\'/>]#';
+ // We need a OR here to accept either 'property=og:noquote' or 'property="og:unrelated og:my-tag"'
+ $orCondition = '["\']?(?:og:)?' . $tag . '["\']?|["\'][^\'"]*?(?:og:)?' . $tag . '[^\'"]*?[\'"]';
+ // Support quotes in double quoted content, and the other way around
+ $content = 'content=(["\'])((?:(?!\1).)*)\1';
+ // Try to retrieve OpenGraph tag.
+ $ogRegex = '#]+(?:' . $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=["\']([^"\']+)[^>]+(?:'. $properties .')=["\']?(?:og)?:'. $tag .'["\'\s/>]#';
+ $ogRegexReverse = '#]+' . $content . '[^>]+(?:' . $properties . ')=(?:' . $orCondition . ').*?>#';
- if (preg_match($ogRegex, $html, $matches) > 0
+ if (
+ preg_match($ogRegex, $html, $matches) > 0
|| preg_match($ogRegexReverse, $html, $matches) > 0
) {
- return $matches[1];
+ return $matches[2];
}
return false;
}
/**
- * Count private links in given linklist.
- *
- * @param array|Countable $links Linklist.
- *
- * @return int Number of private links.
- */
-function count_private($links)
-{
- $cpt = 0;
- foreach ($links as $link) {
- if ($link['private']) {
- $cpt += 1;
- }
- }
-
- return $cpt;
-}
-
-/**
- * In a string, converts URLs to clickable links.
+ * In a string, converts URLs to clickable bookmarks.
*
* @param string $text input string.
*
- * @return string returns $text with all links converted to HTML links.
+ * @return string returns $text with all bookmarks converted to HTML bookmarks.
*
* @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
*/
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);
}
/**
@@ -231,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
@@ -238,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);
}
/**
@@ -261,12 +167,17 @@ function space2nbsp($text)
*
* @param string $description shaare's description.
* @param string $indexUrl URL to Shaarli's index.
-
+ * @param bool $autolink Turn on/off automatic linkifications of URLs and hashtags
+ *
* @return string formatted description.
*/
-function format_description($description, $indexUrl = '')
+function format_description($description, $indexUrl = '', $autolink = true)
{
- return nl2br(space2nbsp(hashtag_autolink(text2clickable($description), $indexUrl)));
+ if ($autolink) {
+ $description = hashtag_autolink(text2clickable($description), $indexUrl);
+ }
+
+ return nl2br(space2nbsp($description));
}
/**
@@ -279,7 +190,7 @@ function format_description($description, $indexUrl = '')
*/
function link_small_hash($date, $id)
{
- return smallHash($date->format(LinkDB::LINK_DATE_FORMAT) . $id);
+ return smallHash($date->format(Bookmark::LINK_DATE_FORMAT) . $id);
}
/**
@@ -294,3 +205,49 @@ function is_note($linkUrl)
{
return isset($linkUrl[0]) && $linkUrl[0] === '?';
}
+
+/**
+ * Extract an array of tags from a given tag string, with provided separator.
+ *
+ * @param string|null $tags String containing a list of tags separated by $separator.
+ * @param string $separator Shaarli's default: ' ' (whitespace)
+ *
+ * @return array List of tags
+ */
+function tags_str2array(?string $tags, string $separator): array
+{
+ // For whitespaces, we use the special \s regex character
+ $separator = str_replace([' ', '/'], ['\s', '\/'], $separator);
+
+ return preg_split('/\s*' . $separator . '+\s*/', trim($tags ?? ''), -1, PREG_SPLIT_NO_EMPTY) ?: [];
+}
+
+/**
+ * Return a tag string with provided separator from a list of tags.
+ * Note that given array is clean up by tags_filter().
+ *
+ * @param array|null $tags List of tags
+ * @param string $separator
+ *
+ * @return string
+ */
+function tags_array2str(?array $tags, string $separator): string
+{
+ return implode($separator, tags_filter($tags, $separator));
+}
+
+/**
+ * Clean an array of tags: trim + remove empty entries
+ *
+ * @param array|null $tags List of tags
+ * @param string $separator
+ *
+ * @return array
+ */
+function tags_filter(?array $tags, string $separator): array
+{
+ $trimDefault = " \t\n\r\0\x0B";
+ return array_values(array_filter(array_map(function (string $entry) use ($separator, $trimDefault): string {
+ return trim($entry, $trimDefault . $separator);
+ }, $tags ?? [])));
+}
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/bookmark/exception/BookmarkNotFoundException.php b/application/bookmark/exception/BookmarkNotFoundException.php
new file mode 100644
index 00000000..a91d1efa
--- /dev/null
+++ b/application/bookmark/exception/BookmarkNotFoundException.php
@@ -0,0 +1,16 @@
+message = t('The link you are trying to reach does not exist or has been deleted.');
+ }
+}
diff --git a/application/bookmark/exception/DatastoreNotInitializedException.php b/application/bookmark/exception/DatastoreNotInitializedException.php
new file mode 100644
index 00000000..d2379f88
--- /dev/null
+++ b/application/bookmark/exception/DatastoreNotInitializedException.php
@@ -0,0 +1,9 @@
+getCreated() instanceof \DateTime) {
+ $created = $bookmark->getCreated()->format(\DateTime::ATOM);
+ } elseif (empty($bookmark->getCreated())) {
+ $created = '';
+ } else {
+ $created = 'Not a DateTime object';
+ }
+ $this->message = 'This bookmark is not valid' . PHP_EOL;
+ $this->message .= ' - ID: ' . $bookmark->getId() . PHP_EOL;
+ $this->message .= ' - Title: ' . $bookmark->getTitle() . PHP_EOL;
+ $this->message .= ' - Url: ' . $bookmark->getUrl() . PHP_EOL;
+ $this->message .= ' - ShortUrl: ' . $bookmark->getShortUrl() . PHP_EOL;
+ $this->message .= ' - Created: ' . $created . PHP_EOL;
+ } else {
+ $this->message = 'The provided data is not a bookmark' . PHP_EOL;
+ $this->message .= var_export($bookmark, true);
+ }
+ }
+}
diff --git a/application/bookmark/exception/InvalidWritableDataException.php b/application/bookmark/exception/InvalidWritableDataException.php
new file mode 100644
index 00000000..bf3ae167
--- /dev/null
+++ b/application/bookmark/exception/InvalidWritableDataException.php
@@ -0,0 +1,14 @@
+message = 'Couldn\'t generate bookmark data to store in the datastore. Skipping file writing.';
+ }
+}
diff --git a/application/bookmark/exception/NotEnoughSpaceException.php b/application/bookmark/exception/NotEnoughSpaceException.php
new file mode 100644
index 00000000..e55dd22e
--- /dev/null
+++ b/application/bookmark/exception/NotEnoughSpaceException.php
@@ -0,0 +1,14 @@
+message = 'Not enough available disk space to save the datastore.';
+ }
+}
diff --git a/application/bookmark/exception/NotWritableDataStoreException.php b/application/bookmark/exception/NotWritableDataStoreException.php
new file mode 100644
index 00000000..df91f3bc
--- /dev/null
+++ b/application/bookmark/exception/NotWritableDataStoreException.php
@@ -0,0 +1,17 @@
+message = 'Couldn\'t load data from the data store file "' . $dataStore . '". ' .
+ 'Your data might be corrupted, or your file isn\'t readable.';
+ }
+}
diff --git a/application/config/ConfigIO.php b/application/config/ConfigIO.php
index 3efe5b6f..a623bc8b 100644
--- a/application/config/ConfigIO.php
+++ b/application/config/ConfigIO.php
@@ -1,4 +1,5 @@
';
+ return '*/ ?>';
}
}
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index c95e6800..717a038f 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -1,8 +1,10 @@
getConfigFileExt()) && !$isLoggedIn) {
@@ -361,14 +363,16 @@ protected function setDefaultValues()
$this->setEmpty('security.open_shaarli', false);
$this->setEmpty('security.allowed_protocols', ['ftp', 'ftps', 'magnet']);
- $this->setEmpty('general.header_link', '?');
+ $this->setEmpty('general.header_link', '/');
$this->setEmpty('general.links_per_page', 20);
$this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
$this->setEmpty('general.default_note_title', 'Note: ');
- $this->setEmpty('general.retrieve_description', false);
+ $this->setEmpty('general.retrieve_description', true);
+ $this->setEmpty('general.enable_async_metadata', true);
+ $this->setEmpty('general.tags_separator', ' ');
- $this->setEmpty('updates.check_updates', false);
- $this->setEmpty('updates.check_updates_branch', 'stable');
+ $this->setEmpty('updates.check_updates', true);
+ $this->setEmpty('updates.check_updates_branch', 'latest');
$this->setEmpty('updates.check_updates_interval', 86400);
$this->setEmpty('feed.rss_permalinks', true);
@@ -381,6 +385,7 @@ protected function setDefaultValues()
// default state of the 'remember me' checkbox of the login form
$this->setEmpty('privacy.remember_user_default', true);
+ $this->setEmpty('thumbnails.mode', Thumbnailer::MODE_ALL);
$this->setEmpty('thumbnails.width', '125');
$this->setEmpty('thumbnails.height', '90');
@@ -388,7 +393,9 @@ protected function setDefaultValues()
$this->setEmpty('translation.mode', 'php');
$this->setEmpty('translation.extensions', []);
- $this->setEmpty('plugins', array());
+ $this->setEmpty('plugins', []);
+
+ $this->setEmpty('formatter', 'markdown');
}
/**
diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php
index cad34594..53d6a7a3 100644
--- a/application/config/ConfigPhp.php
+++ b/application/config/ConfigPhp.php
@@ -1,4 +1,5 @@
legacy key.
*/
- public static $LEGACY_KEYS_MAPPING = array(
+ public static $LEGACY_KEYS_MAPPING = [
'credentials.login' => 'login',
'credentials.hash' => 'hash',
'credentials.salt' => 'salt',
@@ -68,7 +69,7 @@ class ConfigPhp implements ConfigIO
'privacy.hide_public_links' => 'config.HIDE_PUBLIC_LINKS',
'privacy.hide_timestamps' => 'config.HIDE_TIMESTAMPS',
'security.open_shaarli' => 'config.OPEN_SHAARLI',
- );
+ ];
/**
* @inheritdoc
@@ -76,12 +77,12 @@ class ConfigPhp implements ConfigIO
public function read($filepath)
{
if (! file_exists($filepath) || ! is_readable($filepath)) {
- return array();
+ return [];
}
include $filepath;
- $out = array();
+ $out = [];
foreach (self::$ROOT_KEYS as $key) {
$out[$key] = isset($GLOBALS[$key]) ? $GLOBALS[$key] : '';
}
@@ -95,7 +96,7 @@ public function read($filepath)
*/
public function write($filepath, $conf)
{
- $configStr = ' $value) {
$configStr .= '$GLOBALS[\'config\'][\''
. $key
- .'\'] = '
- .var_export($conf['config'][$key], true).';'
+ . '\'] = '
+ . var_export($conf['config'][$key], true) . ';'
. PHP_EOL;
}
@@ -115,18 +116,19 @@ public function write($filepath, $conf)
foreach ($conf['plugins'] as $key => $value) {
$configStr .= '$GLOBALS[\'plugins\'][\''
. $key
- .'\'] = '
- .var_export($conf['plugins'][$key], true).';'
+ . '\'] = '
+ . var_export($conf['plugins'][$key], true) . ';'
. PHP_EOL;
}
}
- if (!file_put_contents($filepath, $configStr)
+ if (
+ !file_put_contents($filepath, $configStr)
|| strcmp(file_get_contents($filepath), $configStr) != 0
) {
throw new \Shaarli\Exceptions\IOException(
$filepath,
- t('Shaarli could not create the config file. '.
+ t('Shaarli could not create the config file. ' .
'Please make sure Shaarli has the right to write in the folder is it installed in.')
);
}
diff --git a/application/config/ConfigPlugin.php b/application/config/ConfigPlugin.php
index dbb24937..6cadef12 100644
--- a/application/config/ConfigPlugin.php
+++ b/application/config/ConfigPlugin.php
@@ -1,6 +1,7 @@
$data) {
if (startsWith($key, 'order')) {
continue;
@@ -47,7 +62,7 @@ function save_plugin_config($formData)
throw new PluginConfigOrderException();
}
- $finalPlugins = array();
+ $finalPlugins = [];
// Make plugins order continuous.
foreach ($plugins as $plugin) {
$finalPlugins[] = $plugin;
@@ -66,10 +81,10 @@ function save_plugin_config($formData)
*/
function validate_plugin_order($formData)
{
- $orders = array();
+ $orders = [];
foreach ($formData as $key => $value) {
// No duplicate order allowed.
- if (in_array($value, $orders)) {
+ if (in_array($value, $orders, true)) {
return false;
}
diff --git a/application/config/exception/MissingFieldConfigException.php b/application/config/exception/MissingFieldConfigException.php
index 9e0a9359..a5f4356a 100644
--- a/application/config/exception/MissingFieldConfigException.php
+++ b/application/config/exception/MissingFieldConfigException.php
@@ -1,6 +1,5 @@
conf = $conf;
+ $this->session = $session;
+ $this->login = $login;
+ $this->cookieManager = $cookieManager;
+ $this->pluginManager = $pluginManager;
+ $this->logger = $logger;
+ }
+
+ public function build(): ShaarliContainer
+ {
+ $container = new ShaarliContainer();
+
+ $container['conf'] = $this->conf;
+ $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['history'] = function (ShaarliContainer $container): History {
+ return new History($container->conf->get('resource.history'));
+ };
+
+ $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()
+ );
+ };
+
+ $container['metadataRetriever'] = function (ShaarliContainer $container): MetadataRetriever {
+ return new MetadataRetriever($container->conf, $container->httpAccess);
+ };
+
+ $container['pageBuilder'] = function (ShaarliContainer $container): PageBuilder {
+ return new PageBuilder(
+ $container->conf,
+ $container->sessionManager->getSession(),
+ $container->logger,
+ $container->bookmarkService,
+ $container->sessionManager->generateToken(),
+ $container->loginManager->isLoggedIn()
+ );
+ };
+
+ $container['formatterFactory'] = function (ShaarliContainer $container): FormatterFactory {
+ return new FormatterFactory(
+ $container->conf,
+ $container->loginManager->isLoggedIn()
+ );
+ };
+
+ $container['pageCacheManager'] = function (ShaarliContainer $container): PageCacheManager {
+ return new PageCacheManager(
+ $container->conf->get('resource.page_cache'),
+ $container->loginManager->isLoggedIn()
+ );
+ };
+
+ $container['feedBuilder'] = function (ShaarliContainer $container): FeedBuilder {
+ return new FeedBuilder(
+ $container->bookmarkService,
+ $container->formatterFactory->getFormatter(),
+ $container->environment,
+ $container->loginManager->isLoggedIn()
+ );
+ };
+
+ $container['thumbnailer'] = function (ShaarliContainer $container): Thumbnailer {
+ return new Thumbnailer($container->conf);
+ };
+
+ $container['httpAccess'] = function (): HttpAccess {
+ return new HttpAccess();
+ };
+
+ $container['netscapeBookmarkUtils'] = function (ShaarliContainer $container): NetscapeBookmarkUtils {
+ return new NetscapeBookmarkUtils($container->bookmarkService, $container->conf, $container->history);
+ };
+
+ $container['updater'] = function (ShaarliContainer $container): Updater {
+ return new Updater(
+ UpdaterUtils::readUpdatesFile($container->conf->get('resource.updates')),
+ $container->bookmarkService,
+ $container->conf,
+ $container->loginManager->isLoggedIn()
+ );
+ };
+
+ $container['notFoundHandler'] = function (ShaarliContainer $container): ErrorNotFoundController {
+ return new ErrorNotFoundController($container);
+ };
+ $container['errorHandler'] = function (ShaarliContainer $container): ErrorController {
+ return new ErrorController($container);
+ };
+ $container['phpErrorHandler'] = function (ShaarliContainer $container): ErrorController {
+ return new ErrorController($container);
+ };
+
+ return $container;
+ }
+}
diff --git a/application/container/ShaarliContainer.php b/application/container/ShaarliContainer.php
new file mode 100644
index 00000000..be0a8300
--- /dev/null
+++ b/application/container/ShaarliContainer.php
@@ -0,0 +1,54 @@
+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 7c859474..d5d74fd1 100644
--- a/application/feed/FeedBuilder.php
+++ b/application/feed/FeedBuilder.php
@@ -1,7 +1,11 @@
linkDB = $linkDB;
- $this->feedType = $feedType;
+ $this->formatter = $formatter;
$this->serverInfo = $serverInfo;
- $this->userInput = $userInput;
$this->isLoggedIn = $isLoggedIn;
}
/**
* Build data for feed templates.
*
+ * @param string $feedType Type of feed (RSS/ATOM).
+ * @param array $userInput $_GET.
+ *
* @return array Formatted data for feeds templates.
*/
- public function buildData()
+ public function buildData(string $feedType, ?array $userInput)
{
- // Search for untagged links
- if (isset($this->userInput['searchtags']) && empty($this->userInput['searchtags'])) {
- $this->userInput['searchtags'] = false;
+ // Search for untagged bookmarks
+ if (isset($this->userInput['searchtags']) && empty($userInput['searchtags'])) {
+ $userInput['searchtags'] = false;
}
+ $limit = $this->getLimit($userInput);
+
// Optionally filter the results:
- $linksToDisplay = $this->linkDB->filterSearch($this->userInput);
-
- $nblinksToDisplay = $this->getNbLinks(count($linksToDisplay));
-
- // Can't use array_keys() because $link is a LinkDB instance and not a real array.
- $keys = array();
- foreach ($linksToDisplay as $key => $value) {
- $keys[] = $key;
- }
+ $searchResult = $this->linkDB->search($userInput ?? [], null, false, false, true, ['limit' => $limit]);
$pageaddr = escape(index_url($this->serverInfo));
- $linkDisplayed = array();
- for ($i = 0; $i < $nblinksToDisplay && $i < count($keys); $i++) {
- $linkDisplayed[$keys[$i]] = $this->buildItem($linksToDisplay[$keys[$i]], $pageaddr);
+ $this->formatter->addContextData('index_url', $pageaddr);
+ $links = [];
+ foreach ($searchResult->getBookmarks() as $key => $bookmark) {
+ $links[$key] = $this->buildItem($feedType, $bookmark, $pageaddr);
}
- $data['language'] = $this->getTypeLanguage();
- $data['last_update'] = $this->getLatestDateFormatted();
+ $data['language'] = $this->getTypeLanguage($feedType);
+ $data['last_update'] = $this->getLatestDateFormatted($feedType);
$data['show_dates'] = !$this->hideDates || $this->isLoggedIn;
- // Remove leading slash from REQUEST_URI.
- $data['self_link'] = escape(server_url($this->serverInfo))
- . escape($this->serverInfo['REQUEST_URI']);
+ // Remove leading path from REQUEST_URI (already contained in $pageaddr).
+ $requestUri = preg_replace('#(.*?/)(feed.*)#', '$2', escape($this->serverInfo['REQUEST_URI']));
+ $data['self_link'] = $pageaddr . $requestUri;
$data['index_url'] = $pageaddr;
$data['usepermalinks'] = $this->usePermalinks === true;
- $data['links'] = $linkDisplayed;
+ $data['links'] = $links;
return $data;
}
/**
- * Build a feed item (one per shaare).
- *
- * @param array $link Single link array extracted from LinkDB.
- * @param string $pageaddr Index URL.
- *
- * @return array Link array with feed attributes.
- */
- protected function buildItem($link, $pageaddr)
- {
- $link['guid'] = $pageaddr . '?' . $link['shorturl'];
- // Prepend the root URL for notes
- if (is_note($link['url'])) {
- $link['url'] = $pageaddr . $link['url'];
- }
- if ($this->usePermalinks === true) {
- $permalink = '' . t('Direct link') . '';
- } else {
- $permalink = '' . t('Permalink') . '';
- }
- $link['description'] = format_description($link['description'], $pageaddr);
- $link['description'] .= PHP_EOL . '
— ' . $permalink;
-
- $pubDate = $link['created'];
- $link['pub_iso_date'] = $this->getIsoDate($pubDate);
-
- // atom:entry elements MUST contain exactly one atom:updated element.
- if (!empty($link['updated'])) {
- $upDate = $link['updated'];
- $link['up_iso_date'] = $this->getIsoDate($upDate, DateTime::ATOM);
- } else {
- $link['up_iso_date'] = $this->getIsoDate($pubDate, DateTime::ATOM);
- }
-
- // Save the more recent item.
- if (empty($this->latestDate) || $this->latestDate < $pubDate) {
- $this->latestDate = $pubDate;
- }
- if (!empty($upDate) && $this->latestDate < $upDate) {
- $this->latestDate = $upDate;
- }
-
- $taglist = array_filter(explode(' ', $link['tags']), 'strlen');
- uasort($taglist, 'strcasecmp');
- $link['taglist'] = $taglist;
-
- return $link;
- }
-
- /**
- * Set this to true to use permalinks instead of direct links.
+ * Set this to true to use permalinks instead of direct bookmarks.
*
* @param boolean $usePermalinks true to force permalinks.
*/
@@ -215,22 +157,64 @@ public function setLocale($locale)
$this->locale = strtolower($locale);
}
+ /**
+ * Build a feed item (one per shaare).
+ *
+ * @param string $feedType Type of feed (RSS/ATOM).
+ * @param Bookmark $link Single link array extracted from LinkDB.
+ * @param string $pageaddr Index URL.
+ *
+ * @return array Link array with feed attributes.
+ */
+ protected function buildItem(string $feedType, $link, $pageaddr)
+ {
+ $data = $this->formatter->format($link);
+ $data['guid'] = rtrim($pageaddr, '/') . '/shaare/' . $data['shorturl'];
+ if ($this->usePermalinks === true) {
+ $permalink = '' . t('Direct link') . '';
+ } else {
+ $permalink = '' . t('Permalink') . '';
+ }
+ $data['description'] .= PHP_EOL . PHP_EOL . '
— ' . $permalink;
+
+ $data['pub_iso_date'] = $this->getIsoDate($feedType, $data['created']);
+
+ // atom:entry elements MUST contain exactly one atom:updated element.
+ if (!empty($link->getUpdated())) {
+ $data['up_iso_date'] = $this->getIsoDate($feedType, $data['updated'], DateTime::ATOM);
+ } else {
+ $data['up_iso_date'] = $this->getIsoDate($feedType, $data['created'], DateTime::ATOM);
+ }
+
+ // Save the more recent item.
+ if (empty($this->latestDate) || $this->latestDate < $data['created']) {
+ $this->latestDate = $data['created'];
+ }
+ if (!empty($data['updated']) && $this->latestDate < $data['updated']) {
+ $this->latestDate = $data['updated'];
+ }
+
+ return $data;
+ }
+
/**
* Get the language according to the feed type, based on the locale:
*
* - RSS format: en-us (default: 'en-en').
* - ATOM format: fr (default: 'en').
*
+ * @param string $feedType Type of feed (RSS/ATOM).
+ *
* @return string The language.
*/
- public function getTypeLanguage()
+ protected function getTypeLanguage(string $feedType)
{
// Use the locale do define the language, if available.
if (!empty($this->locale) && preg_match('/^\w{2}[_\-]\w{2}/', $this->locale)) {
- $length = ($this->feedType === self::$FEED_RSS) ? 5 : 2;
+ $length = ($feedType === self::$FEED_RSS) ? 5 : 2;
return str_replace('_', '-', substr($this->locale, 0, $length));
}
- return ($this->feedType === self::$FEED_RSS) ? 'en-en' : 'en';
+ return ($feedType === self::$FEED_RSS) ? 'en-en' : 'en';
}
/**
@@ -238,32 +222,35 @@ public function getTypeLanguage()
*
* Return an empty string if invalid DateTime is passed.
*
+ * @param string $feedType Type of feed (RSS/ATOM).
+ *
* @return string Formatted date.
*/
- protected function getLatestDateFormatted()
+ protected function getLatestDateFormatted(string $feedType)
{
if (empty($this->latestDate) || !$this->latestDate instanceof DateTime) {
return '';
}
- $type = ($this->feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM;
+ $type = ($feedType == self::$FEED_RSS) ? DateTime::RSS : DateTime::ATOM;
return $this->latestDate->format($type);
}
/**
* Get ISO date from DateTime according to feed type.
*
+ * @param string $feedType Type of feed (RSS/ATOM).
* @param DateTime $date Date to format.
* @param string|bool $format Force format.
*
* @return string Formatted date.
*/
- protected function getIsoDate(DateTime $date, $format = false)
+ protected function getIsoDate(string $feedType, DateTime $date, $format = false)
{
if ($format !== false) {
return $date->format($format);
}
- if ($this->feedType == self::$FEED_RSS) {
+ if ($feedType == self::$FEED_RSS) {
return $date->format(DateTime::RSS);
}
return $date->format(DateTime::ATOM);
@@ -273,23 +260,23 @@ protected function getIsoDate(DateTime $date, $format = false)
* Returns the number of link to display according to 'nb' user input parameter.
*
* If 'nb' not set or invalid, default value: $DEFAULT_NB_LINKS.
- * If 'nb' is set to 'all', display all filtered links (max parameter).
+ * If 'nb' is set to 'all', display all filtered bookmarks (max parameter).
*
- * @param int $max maximum number of links to display.
+ * @param array $userInput $_GET.
*
- * @return int number of links to display.
+ * @return int number of bookmarks to display.
*/
- public function getNbLinks($max)
+ protected function getLimit(?array $userInput)
{
- if (empty($this->userInput['nb'])) {
+ if (empty($userInput['nb'])) {
return self::$DEFAULT_NB_LINKS;
}
- if ($this->userInput['nb'] == 'all') {
- return $max;
+ if ($userInput['nb'] == 'all') {
+ return null;
}
- $intNb = intval($this->userInput['nb']);
+ $intNb = intval($userInput['nb']);
if (!is_int($intNb) || $intNb == 0) {
return self::$DEFAULT_NB_LINKS;
}
diff --git a/application/formatter/BookmarkDefaultFormatter.php b/application/formatter/BookmarkDefaultFormatter.php
new file mode 100644
index 00000000..70f65818
--- /dev/null
+++ b/application/formatter/BookmarkDefaultFormatter.php
@@ -0,0 +1,229 @@
+getTitle());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function formatTitleHtml($bookmark)
+ {
+ $title = $this->tokenizeSearchHighlightField(
+ $bookmark->getTitle() ?? '',
+ $bookmark->getAdditionalContentEntry('search_highlight')['title'] ?? []
+ );
+
+ return $this->replaceTokens(escape($title));
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function formatDescription($bookmark)
+ {
+ $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
+ $description = $this->tokenizeSearchHighlightField(
+ $bookmark->getDescription() ?? '',
+ $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? []
+ );
+ $description = format_description(
+ escape($description),
+ $indexUrl,
+ $this->conf->get('formatter_settings.autolink', true)
+ );
+
+ return $this->replaceTokens($description);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function formatTagList($bookmark)
+ {
+ return escape(parent::formatTagList($bookmark));
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function formatTagListHtml($bookmark)
+ {
+ $tagsSeparator = $this->conf->get('general.tags_separator', ' ');
+ if (empty($bookmark->getAdditionalContentEntry('search_highlight')['tags'])) {
+ return $this->formatTagList($bookmark);
+ }
+
+ $tags = $this->tokenizeSearchHighlightField(
+ $bookmark->getTagsString($tagsSeparator),
+ $bookmark->getAdditionalContentEntry('search_highlight')['tags']
+ );
+ $tags = $this->filterTagList(tags_str2array($tags, $tagsSeparator));
+ $tags = escape($tags);
+ $tags = $this->replaceTokensArray($tags);
+
+ return $tags;
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function formatTagString($bookmark)
+ {
+ return implode($this->conf->get('general.tags_separator'), $this->formatTagList($bookmark));
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function formatUrl($bookmark)
+ {
+ if ($bookmark->isNote() && isset($this->contextData['index_url'])) {
+ return rtrim($this->contextData['index_url'], '/') . '/' . escape(ltrim($bookmark->getUrl(), '/'));
+ }
+
+ return escape($bookmark->getUrl());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function formatRealUrl($bookmark)
+ {
+ if ($bookmark->isNote()) {
+ if (isset($this->contextData['index_url'])) {
+ $prefix = rtrim($this->contextData['index_url'], '/') . '/';
+ }
+
+ if (isset($this->contextData['base_path'])) {
+ $prefix = rtrim($this->contextData['base_path'], '/') . '/';
+ }
+
+ return escape($prefix ?? '') . escape(ltrim($bookmark->getUrl() ?? '', '/'));
+ }
+
+ return escape($bookmark->getUrl());
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function formatUrlHtml($bookmark)
+ {
+ $url = $this->tokenizeSearchHighlightField(
+ $bookmark->getUrl() ?? '',
+ $bookmark->getAdditionalContentEntry('search_highlight')['url'] ?? []
+ );
+
+ return $this->replaceTokens(escape($url));
+ }
+
+ /**
+ * @inheritdoc
+ */
+ protected function formatThumbnail($bookmark)
+ {
+ return escape($bookmark->getThumbnail());
+ }
+
+ /**
+ * @inheritDoc
+ */
+ protected function formatAdditionalContent(Bookmark $bookmark): array
+ {
+ $additionalContent = parent::formatAdditionalContent($bookmark);
+
+ unset($additionalContent['search_highlight']);
+
+ return $additionalContent;
+ }
+
+ /**
+ * Insert search highlight token in provided field content based on a list of search result positions
+ *
+ * @param string $fieldContent
+ * @param array|null $positions List of of search results with 'start' and 'end' positions.
+ *
+ * @return string Updated $fieldContent.
+ */
+ protected function tokenizeSearchHighlightField(string $fieldContent, ?array $positions): string
+ {
+ if (empty($positions)) {
+ return $fieldContent;
+ }
+
+ $insertedTokens = 0;
+ $tokenLength = strlen(static::SEARCH_HIGHLIGHT_OPEN);
+ foreach ($positions as $position) {
+ $position = [
+ 'start' => $position['start'] + ($insertedTokens * $tokenLength),
+ 'end' => $position['end'] + ($insertedTokens * $tokenLength),
+ ];
+
+ $content = mb_substr($fieldContent, 0, $position['start']);
+ $content .= static::SEARCH_HIGHLIGHT_OPEN;
+ $content .= mb_substr($fieldContent, $position['start'], $position['end'] - $position['start']);
+ $content .= static::SEARCH_HIGHLIGHT_CLOSE;
+ $content .= mb_substr($fieldContent, $position['end']);
+
+ $fieldContent = $content;
+
+ $insertedTokens += 2;
+ }
+
+ return $fieldContent;
+ }
+
+ /**
+ * Replace search highlight tokens with HTML highlighted span.
+ *
+ * @param string $fieldContent
+ *
+ * @return string updated content.
+ */
+ protected function replaceTokens(string $fieldContent): string
+ {
+ return str_replace(
+ [static::SEARCH_HIGHLIGHT_OPEN, static::SEARCH_HIGHLIGHT_CLOSE],
+ ['', ''],
+ $fieldContent
+ );
+ }
+
+ /**
+ * Apply replaceTokens to an array of content strings.
+ *
+ * @param string[] $fieldContents
+ *
+ * @return array
+ */
+ protected function replaceTokensArray(array $fieldContents): array
+ {
+ foreach ($fieldContents as &$entry) {
+ $entry = $this->replaceTokens($entry);
+ }
+
+ return $fieldContents;
+ }
+}
diff --git a/application/formatter/BookmarkFormatter.php b/application/formatter/BookmarkFormatter.php
new file mode 100644
index 00000000..a43ac1df
--- /dev/null
+++ b/application/formatter/BookmarkFormatter.php
@@ -0,0 +1,390 @@
+conf = $conf;
+ $this->isLoggedIn = $isLoggedIn;
+ }
+
+ /**
+ * Convert a Bookmark into an array usable by templates and plugins.
+ *
+ * All Bookmark attributes are formatted through a format method
+ * that can be overridden in a formatter extending this class.
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return array formatted representation of a Bookmark
+ */
+ public function format($bookmark)
+ {
+ $out['id'] = $this->formatId($bookmark);
+ $out['shorturl'] = $this->formatShortUrl($bookmark);
+ $out['url'] = $this->formatUrl($bookmark);
+ $out['real_url'] = $this->formatRealUrl($bookmark);
+ $out['url_html'] = $this->formatUrlHtml($bookmark);
+ $out['title'] = $this->formatTitle($bookmark);
+ $out['title_html'] = $this->formatTitleHtml($bookmark);
+ $out['description'] = $this->formatDescription($bookmark);
+ $out['thumbnail'] = $this->formatThumbnail($bookmark);
+ $out['taglist'] = $this->formatTagList($bookmark);
+ $out['taglist_urlencoded'] = $this->formatTagListUrlEncoded($bookmark);
+ $out['taglist_html'] = $this->formatTagListHtml($bookmark);
+ $out['tags'] = $this->formatTagString($bookmark);
+ $out['tags_urlencoded'] = $this->formatTagStringUrlEncoded($bookmark);
+ $out['sticky'] = $bookmark->isSticky();
+ $out['private'] = $bookmark->isPrivate();
+ $out['class'] = $this->formatClass($bookmark);
+ $out['created'] = $this->formatCreated($bookmark);
+ $out['updated'] = $this->formatUpdated($bookmark);
+ $out['timestamp'] = $this->formatCreatedTimestamp($bookmark);
+ $out['updated_timestamp'] = $this->formatUpdatedTimestamp($bookmark);
+ $out['additional_content'] = $this->formatAdditionalContent($bookmark);
+
+ return $out;
+ }
+
+ /**
+ * Add additional data available to formatters.
+ * This is used for example to add `index_url` in description's links.
+ *
+ * @param string $key Context data key
+ * @param string $value Context data value
+ */
+ public function addContextData($key, $value)
+ {
+ $this->contextData[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Format ID
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return int formatted ID
+ */
+ protected function formatId($bookmark)
+ {
+ return $bookmark->getId();
+ }
+
+ /**
+ * Format ShortUrl
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted ShortUrl
+ */
+ protected function formatShortUrl($bookmark)
+ {
+ return $bookmark->getShortUrl();
+ }
+
+ /**
+ * Format Url
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted Url
+ */
+ protected function formatUrl($bookmark)
+ {
+ return $bookmark->getUrl();
+ }
+
+ /**
+ * Format RealUrl
+ * Legacy: identical to Url
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted RealUrl
+ */
+ protected function formatRealUrl($bookmark)
+ {
+ return $this->formatUrl($bookmark);
+ }
+
+ /**
+ * Format Url Html: to be displayed in HTML content, it can contains HTML tags.
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted Url HTML
+ */
+ protected function formatUrlHtml($bookmark)
+ {
+ return $this->formatUrl($bookmark);
+ }
+
+ /**
+ * Format Title
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted Title
+ */
+ protected function formatTitle($bookmark)
+ {
+ return $bookmark->getTitle();
+ }
+
+ /**
+ * Format Title HTML: to be displayed in HTML content, it can contains HTML tags.
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted Title
+ */
+ protected function formatTitleHtml($bookmark)
+ {
+ return $bookmark->getTitle();
+ }
+
+ /**
+ * Format Description
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted Description
+ */
+ protected function formatDescription($bookmark)
+ {
+ return $bookmark->getDescription();
+ }
+
+ /**
+ * Format Thumbnail
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted Thumbnail
+ */
+ protected function formatThumbnail($bookmark)
+ {
+ return $bookmark->getThumbnail();
+ }
+
+ /**
+ * Format Tags
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return array formatted Tags
+ */
+ protected function formatTagList($bookmark)
+ {
+ return $this->filterTagList($bookmark->getTags());
+ }
+
+ /**
+ * Format Url Encoded Tags
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return array formatted Tags
+ */
+ protected function formatTagListUrlEncoded($bookmark)
+ {
+ return array_map('urlencode', $this->filterTagList($bookmark->getTags()));
+ }
+
+ /**
+ * Format Tags HTML: to be displayed in HTML content, it can contains HTML tags.
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return array formatted Tags
+ */
+ protected function formatTagListHtml($bookmark)
+ {
+ return $this->formatTagList($bookmark);
+ }
+
+ /**
+ * Format TagString
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted TagString
+ */
+ protected function formatTagString($bookmark)
+ {
+ return implode($this->conf->get('general.tags_separator', ' '), $this->formatTagList($bookmark));
+ }
+
+ /**
+ * Format TagString
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted TagString
+ */
+ protected function formatTagStringUrlEncoded($bookmark)
+ {
+ return implode(' ', $this->formatTagListUrlEncoded($bookmark));
+ }
+
+ /**
+ * Format Class
+ * Used to add specific CSS class for a link
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return string formatted Class
+ */
+ protected function formatClass($bookmark)
+ {
+ return $bookmark->isPrivate() ? 'private' : '';
+ }
+
+ /**
+ * Format Created
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return DateTimeInterface instance
+ */
+ protected function formatCreated(Bookmark $bookmark)
+ {
+ return $bookmark->getCreated();
+ }
+
+ /**
+ * Format Updated
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return DateTimeInterface instance
+ */
+ protected function formatUpdated(Bookmark $bookmark)
+ {
+ return $bookmark->getUpdated();
+ }
+
+ /**
+ * Format CreatedTimestamp
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return int formatted CreatedTimestamp
+ */
+ protected function formatCreatedTimestamp(Bookmark $bookmark)
+ {
+ if (! empty($bookmark->getCreated())) {
+ return $bookmark->getCreated()->getTimestamp();
+ }
+ return 0;
+ }
+
+ /**
+ * Format UpdatedTimestamp
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return int formatted UpdatedTimestamp
+ */
+ protected function formatUpdatedTimestamp(Bookmark $bookmark)
+ {
+ if (! empty($bookmark->getUpdated())) {
+ return $bookmark->getUpdated()->getTimestamp();
+ }
+ return 0;
+ }
+
+ /**
+ * Format bookmark's additional content
+ *
+ * @param Bookmark $bookmark instance
+ *
+ * @return mixed[]
+ */
+ protected function formatAdditionalContent(Bookmark $bookmark): array
+ {
+ return $bookmark->getAdditionalContent();
+ }
+
+ /**
+ * Format tag list, e.g. remove private tags if the user is not logged in.
+ * TODO: this method is called multiple time to format tags, the result should be cached.
+ *
+ * @param array $tags
+ *
+ * @return array
+ */
+ protected function filterTagList(array $tags): array
+ {
+ if ($this->isLoggedIn === true) {
+ return $tags;
+ }
+
+ $out = [];
+ foreach ($tags as $tag) {
+ if (strpos($tag, '.') === 0) {
+ continue;
+ }
+
+ $out[] = $tag;
+ }
+
+ return $out;
+ }
+}
diff --git a/application/formatter/BookmarkMarkdownExtraFormatter.php b/application/formatter/BookmarkMarkdownExtraFormatter.php
new file mode 100644
index 00000000..da539bfd
--- /dev/null
+++ b/application/formatter/BookmarkMarkdownExtraFormatter.php
@@ -0,0 +1,24 @@
+parsedown = new ShaarliParsedownExtra();
+ }
+}
diff --git a/application/formatter/BookmarkMarkdownFormatter.php b/application/formatter/BookmarkMarkdownFormatter.php
new file mode 100644
index 00000000..d4dccee6
--- /dev/null
+++ b/application/formatter/BookmarkMarkdownFormatter.php
@@ -0,0 +1,221 @@
+parsedown = new ShaarliParsedown();
+ $this->escape = $conf->get('security.markdown_escape', true);
+ $this->allowedProtocols = $conf->get('security.allowed_protocols', []);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function formatDescription($bookmark)
+ {
+ if (in_array(self::NO_MD_TAG, $bookmark->getTags())) {
+ return parent::formatDescription($bookmark);
+ }
+
+ $processedDescription = $this->tokenizeSearchHighlightField(
+ $bookmark->getDescription() ?? '',
+ $bookmark->getAdditionalContentEntry('search_highlight')['description'] ?? []
+ );
+ $processedDescription = $this->filterProtocols($processedDescription);
+ $processedDescription = $this->formatHashTags($processedDescription);
+ $processedDescription = $this->reverseEscapedHtml($processedDescription);
+ $processedDescription = $this->parsedown
+ ->setMarkupEscaped($this->escape)
+ ->setBreaksEnabled(true)
+ ->text($processedDescription);
+ $processedDescription = $this->sanitizeHtml($processedDescription);
+ $processedDescription = $this->replaceTokens($processedDescription);
+
+ if (!empty($processedDescription)) {
+ $processedDescription = '' . $processedDescription . '';
+ }
+
+ return $processedDescription;
+ }
+
+ /**
+ * Remove the NO markdown tag if it is present
+ *
+ * @inheritdoc
+ */
+ protected function formatTagList($bookmark)
+ {
+ $out = parent::formatTagList($bookmark);
+ if ($this->isLoggedIn === false && ($pos = array_search(self::NO_MD_TAG, $out)) !== false) {
+ unset($out[$pos]);
+ return array_values($out);
+ }
+ return $out;
+ }
+
+ /**
+ * Replace not whitelisted protocols with http:// in given description.
+ * Also adds `index_url` to relative links if it's specified
+ *
+ * @param string $description input description text.
+ *
+ * @return string $description without malicious link.
+ */
+ protected function filterProtocols($description)
+ {
+ $allowedProtocols = $this->allowedProtocols;
+ $indexUrl = ! empty($this->contextData['index_url']) ? $this->contextData['index_url'] : '';
+
+ return preg_replace_callback(
+ '#]\((.*?)\)#is',
+ function ($match) use ($allowedProtocols, $indexUrl) {
+ $link = startsWith($match[1], '?') || startsWith($match[1], '/') ? $indexUrl : '';
+ $link .= whitelist_protocols($match[1], $allowedProtocols);
+ return '](' . $link . ')';
+ },
+ $description
+ );
+ }
+
+ /**
+ * Replace hashtag in Markdown links format
+ * E.g. `#hashtag` becomes `[#hashtag](./add-tag/hashtag)`
+ * It includes the index URL if specified.
+ *
+ * @param string $description
+ *
+ * @return string
+ */
+ 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
+ * \p{Pc} - to match underscore
+ * \p{N} - numeric character in any script
+ * \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}' . $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 = '';
+ $codeBlockOn = false;
+ $lineCount = 0;
+
+ foreach ($descriptionLines as $descriptionLine) {
+ // Detect line of code: starting with 4 spaces,
+ // except lists which can start with +/*/- or `2.` after spaces.
+ $codeLineOn = preg_match('/^ +(?=[^\+\*\-])(?=(?!\d\.).)/', $descriptionLine) > 0;
+ // Detect and toggle block of code
+ if (!$codeBlockOn) {
+ $codeBlockOn = preg_match('/^```/', $descriptionLine) > 0;
+ } elseif (preg_match('/^```/', $descriptionLine) > 0) {
+ $codeBlockOn = false;
+ }
+
+ if (!$codeBlockOn && !$codeLineOn) {
+ $descriptionLine = preg_replace_callback($regex, $replacement, $descriptionLine);
+ }
+
+ $descriptionOut .= $descriptionLine;
+ if ($lineCount++ < count($descriptionLines) - 1) {
+ $descriptionOut .= PHP_EOL;
+ }
+ }
+
+ return $descriptionOut;
+ }
+
+ /**
+ * Remove dangerous HTML tags (tags, iframe, etc.).
+ * Doesn't affect content (already escaped by Parsedown).
+ *
+ * @param string $description input description text.
+ *
+ * @return string given string escaped.
+ */
+ protected function sanitizeHtml($description)
+ {
+ $escapeTags = [
+ 'script',
+ 'style',
+ 'link',
+ 'iframe',
+ 'frameset',
+ 'frame',
+ ];
+ foreach ($escapeTags as $tag) {
+ $description = preg_replace_callback(
+ '#<\s*' . $tag . '[^>]*>(.*\s*' . $tag . '[^>]*>)?#is',
+ function ($match) {
+ return escape($match[0]);
+ },
+ $description
+ );
+ }
+ $description = preg_replace(
+ '#(<[^>]+\s)on[a-z]*="?[^ "]*"?#is',
+ '$1',
+ $description
+ );
+ return $description;
+ }
+
+ protected function reverseEscapedHtml($description)
+ {
+ return unescape($description);
+ }
+}
diff --git a/application/formatter/BookmarkRawFormatter.php b/application/formatter/BookmarkRawFormatter.php
new file mode 100644
index 00000000..4ff07cdf
--- /dev/null
+++ b/application/formatter/BookmarkRawFormatter.php
@@ -0,0 +1,15 @@
+conf = $conf;
+ $this->isLoggedIn = $isLoggedIn;
+ }
+
+ /**
+ * Instanciate a BookmarkFormatter depending on the configuration or provided formatter type.
+ *
+ * @param string|null $type force a specific type regardless of the configuration
+ *
+ * @return BookmarkFormatter instance.
+ */
+ public function getFormatter(string $type = null): BookmarkFormatter
+ {
+ $type = $type ? $type : $this->conf->get('formatter', 'default');
+ $className = '\\Shaarli\\Formatter\\Bookmark' . ucfirst($type) . 'Formatter';
+ if (!class_exists($className)) {
+ $className = '\\Shaarli\\Formatter\\BookmarkDefaultFormatter';
+ }
+
+ return new $className($this->conf, $this->isLoggedIn);
+ }
+}
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/ShaarliAdminMiddleware.php b/application/front/ShaarliAdminMiddleware.php
new file mode 100644
index 00000000..35ce4a3b
--- /dev/null
+++ b/application/front/ShaarliAdminMiddleware.php
@@ -0,0 +1,27 @@
+initBasePath($request);
+
+ if (true !== $this->container->loginManager->isLoggedIn()) {
+ $returnUrl = urlencode($this->container->environment['REQUEST_URI']);
+
+ return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl);
+ }
+
+ return parent::__invoke($request, $response, $next);
+ }
+}
diff --git a/application/front/ShaarliMiddleware.php b/application/front/ShaarliMiddleware.php
new file mode 100644
index 00000000..164217f4
--- /dev/null
+++ b/application/front/ShaarliMiddleware.php
@@ -0,0 +1,116 @@
+container = $container;
+ }
+
+ /**
+ * Middleware execution:
+ * - run updates
+ * - if not logged in open shaarli, redirect to login
+ * - execute the controller
+ * - return the response
+ *
+ * In case of error, the error template will be displayed with the exception message.
+ *
+ * @param Request $request Slim request
+ * @param Response $response Slim response
+ * @param callable $next Next action
+ *
+ * @return Response response.
+ */
+ public function __invoke(Request $request, Response $response, callable $next): Response
+ {
+ $this->initBasePath($request);
+
+ try {
+ if (
+ !is_file($this->container->conf->getConfigFileExt())
+ && !in_array($next->getName(), ['displayInstall', 'saveInstall'], true)
+ ) {
+ return $response->withRedirect($this->container->basePath . '/install');
+ }
+
+ $this->runUpdates();
+ $this->checkOpenShaarli($request, $response, $next);
+
+ return $next($request, $response);
+ } catch (UnauthorizedException $e) {
+ $returnUrl = urlencode($this->container->environment['REQUEST_URI']);
+
+ return $response->withRedirect($this->container->basePath . '/login?returnurl=' . $returnUrl);
+ }
+ // Other exceptions are handled by ErrorController
+ }
+
+ /**
+ * Run the updater for every requests processed while logged in.
+ */
+ protected function runUpdates(): void
+ {
+ if ($this->container->loginManager->isLoggedIn() !== true) {
+ return;
+ }
+
+ $this->container->updater->setBasePath($this->container->basePath);
+ $newUpdates = $this->container->updater->update();
+ if (!empty($newUpdates)) {
+ $this->container->updater->writeUpdates(
+ $this->container->conf->get('resource.updates'),
+ $this->container->updater->getDoneUpdates()
+ );
+
+ $this->container->pageCacheManager->invalidateCaches();
+ }
+ }
+
+ /**
+ * Access is denied to most pages with `hide_public_links` + `force_login` settings.
+ */
+ protected function checkOpenShaarli(Request $request, Response $response, callable $next): bool
+ {
+ if (
+// if the user isn't logged in
+ !$this->container->loginManager->isLoggedIn()
+ // and Shaarli doesn't have public content...
+ && $this->container->conf->get('privacy.hide_public_links')
+ // and is configured to enforce the login
+ && $this->container->conf->get('privacy.force_login')
+ // and the current page isn't already the login page
+ // and the user is not requesting a feed (which would lead to a different content-type as expected)
+ && !in_array($next->getName(), ['login', 'processLogin', 'atom', 'rss'], true)
+ ) {
+ throw new UnauthorizedException();
+ }
+
+ return true;
+ }
+
+ /**
+ * Initialize the URL base path if it hasn't been defined yet.
+ */
+ protected function initBasePath(Request $request): void
+ {
+ if (null === $this->container->basePath) {
+ $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/');
+ }
+ }
+}
diff --git a/application/front/controller/admin/ConfigureController.php b/application/front/controller/admin/ConfigureController.php
new file mode 100644
index 00000000..dc421661
--- /dev/null
+++ b/application/front/controller/admin/ConfigureController.php
@@ -0,0 +1,132 @@
+assignView('title', $this->container->conf->get('general.title', 'Shaarli'));
+ $this->assignView('theme', $this->container->conf->get('resource.theme'));
+ $this->assignView(
+ 'theme_available',
+ ThemeUtils::getThemes($this->container->conf->get('resource.raintpl_tpl'))
+ );
+ $this->assignView('formatter_available', ['default', 'markdown', 'markdownExtra']);
+ list($continents, $cities) = generateTimeZoneData(
+ timezone_identifiers_list(),
+ $this->container->conf->get('general.timezone')
+ );
+ $this->assignView('continents', $continents);
+ $this->assignView('cities', $cities);
+ $this->assignView('retrieve_description', $this->container->conf->get('general.retrieve_description', false));
+ $this->assignView('private_links_default', $this->container->conf->get('privacy.default_private_links', false));
+ $this->assignView(
+ 'session_protection_disabled',
+ $this->container->conf->get('security.session_protection_disabled', false)
+ );
+ $this->assignView('enable_rss_permalinks', $this->container->conf->get('feed.rss_permalinks', false));
+ $this->assignView('enable_update_check', $this->container->conf->get('updates.check_updates', true));
+ $this->assignView('hide_public_links', $this->container->conf->get('privacy.hide_public_links', false));
+ $this->assignView('api_enabled', $this->container->conf->get('api.enabled', true));
+ $this->assignView('api_secret', $this->container->conf->get('api.secret'));
+ $this->assignView('languages', Languages::getAvailableLanguages());
+ $this->assignView('gd_enabled', extension_loaded('gd'));
+ $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
+ $this->assignView(
+ 'pagetitle',
+ t('Configure') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render(TemplatePage::CONFIGURE));
+ }
+
+ /**
+ * POST /admin/configure - Update Shaarli's configuration
+ */
+ public function save(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $continent = $request->getParam('continent');
+ $city = $request->getParam('city');
+ $tz = 'UTC';
+ if (null !== $continent && null !== $city && isTimeZoneValid($continent, $city)) {
+ $tz = $continent . '/' . $city;
+ }
+
+ $this->container->conf->set('general.timezone', $tz);
+ $this->container->conf->set('general.title', escape($request->getParam('title')));
+ $this->container->conf->set('general.header_link', escape($request->getParam('titleLink')));
+ $this->container->conf->set('general.retrieve_description', !empty($request->getParam('retrieveDescription')));
+ $this->container->conf->set('resource.theme', escape($request->getParam('theme')));
+ $this->container->conf->set(
+ 'security.session_protection_disabled',
+ !empty($request->getParam('disablesessionprotection'))
+ );
+ $this->container->conf->set(
+ 'privacy.default_private_links',
+ !empty($request->getParam('privateLinkByDefault'))
+ );
+ $this->container->conf->set('feed.rss_permalinks', !empty($request->getParam('enableRssPermalinks')));
+ $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
+ $this->container->conf->set('privacy.hide_public_links', !empty($request->getParam('hidePublicLinks')));
+ $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
+ $this->container->conf->set('api.secret', escape($request->getParam('apiSecret')));
+ $this->container->conf->set('formatter', escape($request->getParam('formatter')));
+
+ if (!empty($request->getParam('language'))) {
+ $this->container->conf->set('translation.language', escape($request->getParam('language')));
+ }
+
+ $thumbnailsMode = extension_loaded('gd') ? $request->getParam('enableThumbnails') : Thumbnailer::MODE_NONE;
+ if (
+ $thumbnailsMode !== Thumbnailer::MODE_NONE
+ && $thumbnailsMode !== $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
+ ) {
+ $this->saveWarningMessage(
+ t('You have enabled or changed thumbnails mode.') .
+ '' .
+ t('Please synchronize them.') .
+ ''
+ );
+ }
+ $this->container->conf->set('thumbnails.mode', $thumbnailsMode);
+
+ try {
+ $this->container->conf->write($this->container->loginManager->isLoggedIn());
+ $this->container->history->updateSettings();
+ $this->container->pageCacheManager->invalidateCaches();
+ } catch (Throwable $e) {
+ $this->assignView('message', t('Error while writing config file after configuration update.'));
+
+ if ($this->container->conf->get('dev.debug', false)) {
+ $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString());
+ }
+
+ return $response->write($this->render('error'));
+ }
+
+ $this->saveSuccessMessage(t('Configuration was saved.'));
+
+ return $this->redirect($response, '/admin/configure');
+ }
+}
diff --git a/application/front/controller/admin/ExportController.php b/application/front/controller/admin/ExportController.php
new file mode 100644
index 00000000..f01d7e9b
--- /dev/null
+++ b/application/front/controller/admin/ExportController.php
@@ -0,0 +1,80 @@
+assignView('pagetitle', t('Export') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'));
+
+ return $response->write($this->render(TemplatePage::EXPORT));
+ }
+
+ /**
+ * POST /admin/export - Process export, and serve download file named
+ * bookmarks_(all|private|public)_datetime.html
+ */
+ public function export(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $selection = $request->getParam('selection');
+
+ if (empty($selection)) {
+ $this->saveErrorMessage(t('Please select an export mode.'));
+
+ return $this->redirect($response, '/admin/export');
+ }
+
+ $prependNoteUrl = filter_var($request->getParam('prepend_note_url') ?? false, FILTER_VALIDATE_BOOLEAN);
+
+ try {
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+
+ $this->assignView(
+ 'links',
+ $this->container->netscapeBookmarkUtils->filterAndFormat(
+ $formatter,
+ $selection,
+ $prependNoteUrl,
+ index_url($this->container->environment)
+ )
+ );
+ } catch (\Exception $exc) {
+ $this->saveErrorMessage($exc->getMessage());
+
+ return $this->redirect($response, '/admin/export');
+ }
+
+ $now = new DateTime();
+ $response = $response->withHeader('Content-Type', 'text/html; charset=utf-8');
+ $response = $response->withHeader(
+ 'Content-disposition',
+ 'attachment; filename=bookmarks_' . $selection . '_' . $now->format(Bookmark::LINK_DATE_FORMAT) . '.html'
+ );
+
+ $this->assignView('date', $now->format(DateTime::RFC822));
+ $this->assignView('eol', PHP_EOL);
+ $this->assignView('selection', $selection);
+
+ return $response->write($this->render(TemplatePage::NETSCAPE_EXPORT_BOOKMARKS));
+ }
+}
diff --git a/application/front/controller/admin/ImportController.php b/application/front/controller/admin/ImportController.php
new file mode 100644
index 00000000..c2ad6a09
--- /dev/null
+++ b/application/front/controller/admin/ImportController.php
@@ -0,0 +1,82 @@
+assignView(
+ 'maxfilesize',
+ get_max_upload_size(
+ ini_get('post_max_size'),
+ ini_get('upload_max_filesize'),
+ false
+ )
+ );
+ $this->assignView(
+ 'maxfilesizeHuman',
+ get_max_upload_size(
+ ini_get('post_max_size'),
+ ini_get('upload_max_filesize'),
+ true
+ )
+ );
+ $this->assignView('pagetitle', t('Import') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'));
+
+ return $response->write($this->render(TemplatePage::IMPORT));
+ }
+
+ /**
+ * POST /admin/import - Process import file provided and create bookmarks
+ */
+ public function import(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $file = ($request->getUploadedFiles() ?? [])['filetoupload'] ?? null;
+ if (!$file instanceof UploadedFileInterface) {
+ $this->saveErrorMessage(t('No import file provided.'));
+
+ return $this->redirect($response, '/admin/import');
+ }
+
+
+ // Import bookmarks from an uploaded file
+ if (0 === $file->getSize()) {
+ // The file is too big or some form field may be missing.
+ $msg = sprintf(
+ t(
+ 'The file you are trying to upload is probably bigger than what this webserver can accept'
+ . ' (%s). Please upload in smaller chunks.'
+ ),
+ get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
+ );
+ $this->saveErrorMessage($msg);
+
+ return $this->redirect($response, '/admin/import');
+ }
+
+ $status = $this->container->netscapeBookmarkUtils->import($request->getParams(), $file);
+
+ $this->saveSuccessMessage($status);
+
+ return $this->redirect($response, '/admin/import');
+ }
+}
diff --git a/application/front/controller/admin/LogoutController.php b/application/front/controller/admin/LogoutController.php
new file mode 100644
index 00000000..28165129
--- /dev/null
+++ b/application/front/controller/admin/LogoutController.php
@@ -0,0 +1,33 @@
+container->pageCacheManager->invalidateCaches();
+ $this->container->sessionManager->logout();
+ $this->container->cookieManager->setCookieParameter(
+ CookieManager::STAY_SIGNED_IN,
+ 'false',
+ 0,
+ $this->container->basePath . '/'
+ );
+
+ return $this->redirect($response, '/');
+ }
+}
diff --git a/application/front/controller/admin/ManageTagController.php b/application/front/controller/admin/ManageTagController.php
new file mode 100644
index 00000000..1333cce7
--- /dev/null
+++ b/application/front/controller/admin/ManageTagController.php
@@ -0,0 +1,124 @@
+getParam('fromtag') ?? '';
+
+ $this->assignView('fromtag', escape($fromTag));
+ $separator = escape($this->container->conf->get('general.tags_separator', ' '));
+ if ($separator === ' ') {
+ $separator = ' ';
+ $this->assignView('tags_separator_desc', t('whitespace'));
+ }
+ $this->assignView('tags_separator', $separator);
+ $this->assignView(
+ 'pagetitle',
+ t('Manage tags') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render(TemplatePage::CHANGE_TAG));
+ }
+
+ /**
+ * POST /admin/tags - Update or delete provided tag
+ */
+ public function save(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $isDelete = null !== $request->getParam('deletetag') && null === $request->getParam('renametag');
+
+ $fromTag = trim($request->getParam('fromtag') ?? '');
+ $toTag = trim($request->getParam('totag') ?? '');
+
+ if (0 === strlen($fromTag) || false === $isDelete && 0 === strlen($toTag)) {
+ $this->saveWarningMessage(t('Invalid tags provided.'));
+
+ return $this->redirect($response, '/admin/tags');
+ }
+
+ // TODO: move this to bookmark service
+ $searchResult = $this->container->bookmarkService->search(
+ ['searchtags' => $fromTag],
+ BookmarkFilter::$ALL,
+ true
+ );
+ foreach ($searchResult->getBookmarks() as $bookmark) {
+ if (false === $isDelete) {
+ $bookmark->renameTag($fromTag, $toTag);
+ } else {
+ $bookmark->deleteTag($fromTag);
+ }
+
+ $this->container->bookmarkService->set($bookmark, false);
+ $this->container->history->updateLink($bookmark);
+ }
+
+ $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),
+ $count
+ );
+ } else {
+ $alert = sprintf(
+ t('The tag was renamed in %d bookmark.', 'The tag was renamed in %d bookmarks.', $count),
+ $count
+ );
+ }
+
+ $this->saveSuccessMessage($alert);
+
+ $redirect = true === $isDelete ? '/admin/tags' : '/?searchtags=' . urlencode($toTag);
+
+ return $this->redirect($response, $redirect);
+ }
+
+ /**
+ * POST /admin/tags/change-separator - Change tag separator
+ */
+ public function changeSeparator(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $reservedCharacters = ['-', '.', '*'];
+ $newSeparator = $request->getParam('separator');
+ if ($newSeparator === null || mb_strlen($newSeparator) !== 1) {
+ $this->saveErrorMessage(t('Tags separator must be a single character.'));
+ } elseif (in_array($newSeparator, $reservedCharacters, true)) {
+ $reservedCharacters = implode(' ', array_map(function (string $character) {
+ return '' . $character . '
';
+ }, $reservedCharacters));
+ $this->saveErrorMessage(
+ t('These characters are reserved and can\'t be used as tags separator: ') . $reservedCharacters
+ );
+ } else {
+ $this->container->conf->set('general.tags_separator', $newSeparator, true, true);
+
+ $this->saveSuccessMessage('Your tags separator setting has been updated!');
+ }
+
+ return $this->redirect($response, '/admin/tags');
+ }
+}
diff --git a/application/front/controller/admin/MetadataController.php b/application/front/controller/admin/MetadataController.php
new file mode 100644
index 00000000..ff845944
--- /dev/null
+++ b/application/front/controller/admin/MetadataController.php
@@ -0,0 +1,29 @@
+getParam('url');
+
+ // Only try to extract metadata from URL with HTTP(s) scheme
+ if (!empty($url) && strpos(get_url_scheme($url) ?: '', 'http') !== false) {
+ return $response->withJson($this->container->metadataRetriever->retrieve($url));
+ }
+
+ return $response->withJson([]);
+ }
+}
diff --git a/application/front/controller/admin/PasswordController.php b/application/front/controller/admin/PasswordController.php
new file mode 100644
index 00000000..4aaf1f82
--- /dev/null
+++ b/application/front/controller/admin/PasswordController.php
@@ -0,0 +1,101 @@
+assignView(
+ 'pagetitle',
+ t('Change password') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+ }
+
+ /**
+ * GET /admin/password - Displays the change password template
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ return $response->write($this->render(TemplatePage::CHANGE_PASSWORD));
+ }
+
+ /**
+ * POST /admin/password - Change admin password - existing and new passwords need to be provided.
+ */
+ public function change(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ if ($this->container->conf->get('security.open_shaarli', false)) {
+ throw new OpenShaarliPasswordException();
+ }
+
+ $oldPassword = $request->getParam('oldpassword');
+ $newPassword = $request->getParam('setpassword');
+
+ if (empty($newPassword) || empty($oldPassword)) {
+ $this->saveErrorMessage(t('You must provide the current and new password to change it.'));
+
+ return $response
+ ->withStatus(400)
+ ->write($this->render(TemplatePage::CHANGE_PASSWORD))
+ ;
+ }
+
+ // Make sure old password is correct.
+ $oldHash = sha1(
+ $oldPassword .
+ $this->container->conf->get('credentials.login') .
+ $this->container->conf->get('credentials.salt')
+ );
+
+ if ($oldHash !== $this->container->conf->get('credentials.hash')) {
+ $this->saveErrorMessage(t('The old password is not correct.'));
+
+ return $response
+ ->withStatus(400)
+ ->write($this->render(TemplatePage::CHANGE_PASSWORD))
+ ;
+ }
+
+ // Save new password
+ // Salt renders rainbow-tables attacks useless.
+ $this->container->conf->set('credentials.salt', sha1(uniqid('', true) . '_' . mt_rand()));
+ $this->container->conf->set(
+ 'credentials.hash',
+ sha1(
+ $newPassword
+ . $this->container->conf->get('credentials.login')
+ . $this->container->conf->get('credentials.salt')
+ )
+ );
+
+ try {
+ $this->container->conf->write($this->container->loginManager->isLoggedIn());
+ } catch (Throwable $e) {
+ throw new ShaarliFrontException($e->getMessage(), 500, $e);
+ }
+
+ $this->saveSuccessMessage(t('Your password has been changed'));
+
+ return $response->write($this->render(TemplatePage::CHANGE_PASSWORD));
+ }
+}
diff --git a/application/front/controller/admin/PluginsController.php b/application/front/controller/admin/PluginsController.php
new file mode 100644
index 00000000..ae47c1af
--- /dev/null
+++ b/application/front/controller/admin/PluginsController.php
@@ -0,0 +1,85 @@
+container->pluginManager->getPluginsMeta();
+
+ // Split plugins into 2 arrays: ordered enabled plugins and disabled.
+ $enabledPlugins = array_filter($pluginMeta, function ($v) {
+ return ($v['order'] ?? false) !== false;
+ });
+ $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $this->container->conf->get('plugins', []));
+ uasort(
+ $enabledPlugins,
+ function ($a, $b) {
+ return $a['order'] - $b['order'];
+ }
+ );
+ $disabledPlugins = array_filter($pluginMeta, function ($v) {
+ return ($v['order'] ?? false) === false;
+ });
+
+ $this->assignView('enabledPlugins', $enabledPlugins);
+ $this->assignView('disabledPlugins', $disabledPlugins);
+ $this->assignView(
+ 'pagetitle',
+ t('Plugin Administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render(TemplatePage::PLUGINS_ADMIN));
+ }
+
+ /**
+ * POST /admin/plugins - Update Shaarli's configuration
+ */
+ public function save(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ try {
+ $parameters = $request->getParams() ?? [];
+
+ $this->executePageHooks('save_plugin_parameters', $parameters);
+
+ if (isset($parameters['parameters_form'])) {
+ unset($parameters['parameters_form']);
+ unset($parameters['token']);
+ foreach ($parameters as $param => $value) {
+ $this->container->conf->set('plugins.' . $param, escape($value));
+ }
+ } else {
+ $this->container->conf->set('general.enabled_plugins', save_plugin_config($parameters));
+ }
+
+ $this->container->conf->write($this->container->loginManager->isLoggedIn());
+ $this->container->history->updateSettings();
+
+ $this->saveSuccessMessage(t('Setting successfully saved.'));
+ } catch (Exception $e) {
+ $this->saveErrorMessage(
+ t('Error while saving plugin configuration: ') . PHP_EOL . $e->getMessage()
+ );
+ }
+
+ return $this->redirect($response, '/admin/plugins');
+ }
+}
diff --git a/application/front/controller/admin/ServerController.php b/application/front/controller/admin/ServerController.php
new file mode 100644
index 00000000..4b74f4a9
--- /dev/null
+++ b/application/front/controller/admin/ServerController.php
@@ -0,0 +1,101 @@
+container->conf->get('updates.check_updates', true)) {
+ $latestVersion = 'v' . ApplicationUtils::getVersion(
+ ApplicationUtils::$GIT_RAW_URL . '/latest/' . ApplicationUtils::$VERSION_FILE
+ );
+ $releaseUrl .= 'tag/' . $latestVersion;
+ } else {
+ $latestVersion = t('Check disabled');
+ }
+
+ $currentVersion = ApplicationUtils::getVersion('./shaarli_version.php');
+ $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', $permissions);
+ $this->assignView('release_url', $releaseUrl);
+ $this->assignView('latest_version', $latestVersion);
+ $this->assignView('current_version', $currentVersion);
+ $this->assignView('thumbnails_mode', $this->container->conf->get('thumbnails.mode'));
+ $this->assignView('index_url', index_url($this->container->environment));
+ $this->assignView('client_ip', client_ip_id($this->container->environment));
+ $this->assignView('trusted_proxies', $this->container->conf->get('security.trusted_proxies', []));
+
+ $this->assignView(
+ 'pagetitle',
+ t('Server administration') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render('server'));
+ }
+
+ /**
+ * GET /admin/clear-cache?type={$type} - Action to trigger cache folder clearing (either main or thumbnails).
+ */
+ public function clearCache(Request $request, Response $response): Response
+ {
+ $exclude = ['.htaccess'];
+
+ if ($request->getQueryParam('type') === static::CACHE_THUMB) {
+ $folders = [$this->container->conf->get('resource.thumbnails_cache')];
+
+ $this->saveWarningMessage(
+ t('Thumbnails cache has been cleared.') . ' ' .
+ '' .
+ t('Please synchronize them.') .
+ ''
+ );
+ } else {
+ $folders = [
+ $this->container->conf->get('resource.page_cache'),
+ $this->container->conf->get('resource.raintpl_tmp'),
+ ];
+
+ $this->saveSuccessMessage(t('Shaarli\'s cache folder has been cleared!'));
+ }
+
+ // Make sure that we don't delete root cache folder
+ $folders = array_map('realpath', array_values(array_filter(array_map('trim', $folders))));
+ foreach ($folders as $folder) {
+ FileUtils::clearFolder($folder, false, $exclude);
+ }
+
+ return $this->redirect($response, '/admin/server');
+ }
+}
diff --git a/application/front/controller/admin/SessionFilterController.php b/application/front/controller/admin/SessionFilterController.php
new file mode 100644
index 00000000..0917b6d2
--- /dev/null
+++ b/application/front/controller/admin/SessionFilterController.php
@@ -0,0 +1,48 @@
+container->loginManager->isLoggedIn()) {
+ return $this->redirectFromReferer($request, $response, ['visibility']);
+ }
+
+ $newVisibility = $args['visibility'] ?? null;
+ if (false === in_array($newVisibility, [BookmarkFilter::$PRIVATE, BookmarkFilter::$PUBLIC], true)) {
+ $newVisibility = null;
+ }
+
+ $currentVisibility = $this->container->sessionManager->getSessionParameter(SessionManager::KEY_VISIBILITY);
+
+ // Visibility not set or not already expected value, set expected value, otherwise reset it
+ if ($newVisibility !== null && (null === $currentVisibility || $currentVisibility !== $newVisibility)) {
+ // See only public bookmarks
+ $this->container->sessionManager->setSessionParameter(
+ SessionManager::KEY_VISIBILITY,
+ $newVisibility
+ );
+ } else {
+ $this->container->sessionManager->deleteSessionParameter(SessionManager::KEY_VISIBILITY);
+ }
+
+ return $this->redirectFromReferer($request, $response, ['visibility']);
+ }
+}
diff --git a/application/front/controller/admin/ShaareAddController.php b/application/front/controller/admin/ShaareAddController.php
new file mode 100644
index 00000000..ab8e7f40
--- /dev/null
+++ b/application/front/controller/admin/ShaareAddController.php
@@ -0,0 +1,34 @@
+container->bookmarkService->bookmarksCountPerTag();
+ if ($this->container->conf->get('formatter') === 'markdown') {
+ $tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
+ }
+
+ $this->assignView(
+ 'pagetitle',
+ t('Shaare a new link') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+ $this->assignView('tags', $tags);
+ $this->assignView('default_private_links', $this->container->conf->get('privacy.default_private_links', false));
+ $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true));
+
+ return $response->write($this->render(TemplatePage::ADDLINK));
+ }
+}
diff --git a/application/front/controller/admin/ShaareManageController.php b/application/front/controller/admin/ShaareManageController.php
new file mode 100644
index 00000000..05b81678
--- /dev/null
+++ b/application/front/controller/admin/ShaareManageController.php
@@ -0,0 +1,287 @@
+checkToken($request);
+
+ $ids = escape(trim($request->getParam('id') ?? ''));
+ if (empty($ids) || strpos($ids, ' ') !== false) {
+ // multiple, space-separated ids provided
+ $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
+ } else {
+ $ids = [$ids];
+ }
+
+ // assert at least one id is given
+ if (0 === count($ids)) {
+ $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
+
+ return $this->redirectFromReferer($request, $response, [], ['delete-shaare']);
+ }
+
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+ $count = 0;
+ foreach ($ids as $id) {
+ try {
+ $bookmark = $this->container->bookmarkService->get((int) $id);
+ } catch (BookmarkNotFoundException $e) {
+ $this->saveErrorMessage(sprintf(
+ t('Bookmark with identifier %s could not be found.'),
+ $id
+ ));
+
+ continue;
+ }
+
+ $data = $formatter->format($bookmark);
+ $this->executePageHooks('delete_link', $data);
+ $this->container->bookmarkService->remove($bookmark, false);
+ ++$count;
+ }
+
+ if ($count > 0) {
+ $this->container->bookmarkService->save();
+ }
+
+ // If we are called from the bookmarklet, we must close the popup:
+ if ($request->getParam('source') === 'bookmarklet') {
+ 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/']);
+ }
+
+ /**
+ * GET /admin/shaare/visibility
+ *
+ * Change visibility (public/private) of one or multiple bookmarks (depending on `id` query parameter).
+ */
+ public function changeVisibility(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $ids = trim(escape($request->getParam('id') ?? ''));
+ if (empty($ids) || strpos($ids, ' ') !== false) {
+ // multiple, space-separated ids provided
+ $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
+ } else {
+ // only a single id provided
+ $ids = [$ids];
+ }
+
+ // assert at least one id is given
+ if (0 === count($ids)) {
+ $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
+
+ return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
+ }
+
+ // assert that the visibility is valid
+ $visibility = $request->getParam('newVisibility');
+ if (null === $visibility || false === in_array($visibility, ['public', 'private'], true)) {
+ $this->saveErrorMessage(t('Invalid visibility provided.'));
+
+ return $this->redirectFromReferer($request, $response, [], ['change_visibility']);
+ } else {
+ $isPrivate = $visibility === 'private';
+ }
+
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+ $count = 0;
+
+ foreach ($ids as $id) {
+ try {
+ $bookmark = $this->container->bookmarkService->get((int) $id);
+ } catch (BookmarkNotFoundException $e) {
+ $this->saveErrorMessage(sprintf(
+ t('Bookmark with identifier %s could not be found.'),
+ $id
+ ));
+
+ continue;
+ }
+
+ $bookmark->setPrivate($isPrivate);
+
+ // To preserve backward compatibility with 3rd parties, plugins still use arrays
+ $data = $formatter->format($bookmark);
+ $this->executePageHooks('save_link', $data);
+ $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
+
+ $this->container->bookmarkService->set($bookmark, false);
+ ++$count;
+ }
+
+ if ($count > 0) {
+ $this->container->bookmarkService->save();
+ }
+
+ return $this->redirectFromReferer($request, $response, ['/visibility'], ['change_visibility']);
+ }
+
+ /**
+ * GET /admin/shaare/{id}/pin - Pin or unpin a bookmark.
+ */
+ public function pinBookmark(Request $request, Response $response, array $args): Response
+ {
+ $this->checkToken($request);
+
+ $id = $args['id'] ?? '';
+ try {
+ if (false === ctype_digit($id)) {
+ throw new BookmarkNotFoundException();
+ }
+ $bookmark = $this->container->bookmarkService->get((int) $id); // Read database
+ } catch (BookmarkNotFoundException $e) {
+ $this->saveErrorMessage(sprintf(
+ t('Bookmark with identifier %s could not be found.'),
+ $id
+ ));
+
+ return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
+ }
+
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+
+ $bookmark->setSticky(!$bookmark->isSticky());
+
+ // To preserve backward compatibility with 3rd parties, plugins still use arrays
+ $data = $formatter->format($bookmark);
+ $this->executePageHooks('save_link', $data);
+ $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
+
+ $this->container->bookmarkService->set($bookmark);
+
+ return $this->redirectFromReferer($request, $response, ['/pin'], ['pin']);
+ }
+
+ /**
+ * GET /admin/shaare/private/{hash} - Attach a private key to given bookmark, then redirect to the sharing URL.
+ */
+ public function sharePrivate(Request $request, Response $response, array $args): Response
+ {
+ $this->checkToken($request);
+
+ $hash = $args['hash'] ?? '';
+ $bookmark = $this->container->bookmarkService->findByHash($hash);
+
+ if ($bookmark->isPrivate() !== true) {
+ return $this->redirect($response, '/shaare/' . $hash);
+ }
+
+ if (empty($bookmark->getAdditionalContentEntry('private_key'))) {
+ $privateKey = bin2hex(random_bytes(16));
+ $bookmark->setAdditionalContentEntry('private_key', $privateKey);
+ $this->container->bookmarkService->set($bookmark);
+ }
+
+ return $this->redirect(
+ $response,
+ '/shaare/' . $hash . '?key=' . $bookmark->getAdditionalContentEntry('private_key')
+ );
+ }
+
+ /**
+ * POST /admin/shaare/update-tags
+ *
+ * Bulk add or delete a tags on one or multiple bookmarks.
+ */
+ public function addOrDeleteTags(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ $ids = trim(escape($request->getParam('id') ?? ''));
+ if (empty($ids) || strpos($ids, ' ') !== false) {
+ // multiple, space-separated ids provided
+ $ids = array_values(array_filter(preg_split('/\s+/', $ids), 'ctype_digit'));
+ } else {
+ // only a single id provided
+ $ids = [$ids];
+ }
+
+ // assert at least one id is given
+ if (0 === count($ids)) {
+ $this->saveErrorMessage(t('Invalid bookmark ID provided.'));
+
+ return $this->redirectFromReferer($request, $response, ['/updateTag'], []);
+ }
+
+ // assert that the action is valid
+ $action = $request->getParam('action');
+ if (!in_array($action, ['add', 'delete'], true)) {
+ $this->saveErrorMessage(t('Invalid action provided.'));
+
+ return $this->redirectFromReferer($request, $response, ['/updateTag'], []);
+ }
+
+ // assert that the tag name is valid
+ $tagString = trim($request->getParam('tag'));
+ if (empty($tagString)) {
+ $this->saveErrorMessage(t('Invalid tag name provided.'));
+
+ return $this->redirectFromReferer($request, $response, ['/updateTag'], []);
+ }
+
+ $tags = tags_str2array($tagString, $this->container->conf->get('general.tags_separator', ' '));
+ $formatter = $this->container->formatterFactory->getFormatter('raw');
+ $count = 0;
+
+ foreach ($ids as $id) {
+ try {
+ $bookmark = $this->container->bookmarkService->get((int) $id);
+ } catch (BookmarkNotFoundException $e) {
+ $this->saveErrorMessage(sprintf(
+ t('Bookmark with identifier %s could not be found.'),
+ $id
+ ));
+
+ continue;
+ }
+
+ foreach ($tags as $tag) {
+ if ($action === 'add') {
+ $bookmark->addTag($tag);
+ } else {
+ $bookmark->deleteTag($tag);
+ }
+ }
+
+ // To preserve backward compatibility with 3rd parties, plugins still use arrays
+ $data = $formatter->format($bookmark);
+ $this->executePageHooks('save_link', $data);
+ $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
+
+ $this->container->bookmarkService->set($bookmark, false);
+ ++$count;
+ }
+
+ if ($count > 0) {
+ $this->container->bookmarkService->save();
+ }
+
+ return $this->redirectFromReferer($request, $response, ['/updateTag'], []);
+ }
+}
diff --git a/application/front/controller/admin/ShaarePublishController.php b/application/front/controller/admin/ShaarePublishController.php
new file mode 100644
index 00000000..fb9cacc2
--- /dev/null
+++ b/application/front/controller/admin/ShaarePublishController.php
@@ -0,0 +1,274 @@
+getParam('post'));
+ $link = $this->buildLinkDataFromUrl($request, $url);
+
+ return $this->displayForm($link, $link['linkIsNew'], $request, $response);
+ }
+
+ /**
+ * POST /admin/shaare-batch - Displays multiple creation/edit forms from bulk add in add-link page.
+ */
+ public function displayCreateBatchForms(Request $request, Response $response): Response
+ {
+ $urls = array_map('cleanup_url', explode(PHP_EOL, $request->getParam('urls')));
+
+ $links = [];
+ foreach ($urls as $url) {
+ if (empty($url)) {
+ continue;
+ }
+ $link = $this->buildLinkDataFromUrl($request, $url);
+ $data = $this->buildFormData($link, $link['linkIsNew'], $request);
+ $data['token'] = $this->container->sessionManager->generateToken();
+ $data['source'] = 'batch';
+
+ $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
+
+ $links[] = $data;
+ }
+
+ $this->assignView('links', $links);
+ $this->assignView('batch_mode', true);
+ $this->assignView('async_metadata', $this->container->conf->get('general.enable_async_metadata', true));
+
+ return $response->write($this->render(TemplatePage::EDIT_LINK_BATCH));
+ }
+
+ /**
+ * GET /admin/shaare/{id} - Displays the bookmark form in edition mode.
+ */
+ public function displayEditForm(Request $request, Response $response, array $args): Response
+ {
+ $id = $args['id'] ?? '';
+ try {
+ if (false === ctype_digit($id)) {
+ throw new BookmarkNotFoundException();
+ }
+ $bookmark = $this->container->bookmarkService->get((int) $id); // Read database
+ } catch (BookmarkNotFoundException $e) {
+ $this->saveErrorMessage(sprintf(
+ t('Bookmark with identifier %s could not be found.'),
+ $id
+ ));
+
+ return $this->redirect($response, '/');
+ }
+
+ $formatter = $this->getFormatter('raw');
+ $link = $formatter->format($bookmark);
+
+ return $this->displayForm($link, false, $request, $response);
+ }
+
+ /**
+ * POST /admin/shaare
+ */
+ public function save(Request $request, Response $response): Response
+ {
+ $this->checkToken($request);
+
+ // lf_id should only be present if the link exists.
+ $id = $request->getParam('lf_id') !== null ? intval(escape($request->getParam('lf_id'))) : null;
+ if (null !== $id && true === $this->container->bookmarkService->exists($id)) {
+ // Edit
+ $bookmark = $this->container->bookmarkService->get($id);
+ } else {
+ // New link
+ $bookmark = new Bookmark();
+ }
+
+ $bookmark->setTitle($request->getParam('lf_title'));
+ $bookmark->setDescription($request->getParam('lf_description'));
+ $bookmark->setUrl($request->getParam('lf_url'), $this->container->conf->get('security.allowed_protocols', []));
+ $bookmark->setPrivate(filter_var($request->getParam('lf_private'), FILTER_VALIDATE_BOOLEAN));
+ $bookmark->setTagsString(
+ $request->getParam('lf_tags'),
+ $this->container->conf->get('general.tags_separator', ' ')
+ );
+
+ if (
+ $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
+ && true !== $this->container->conf->get('general.enable_async_metadata', true)
+ && $bookmark->shouldUpdateThumbnail()
+ ) {
+ $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
+ }
+ $this->container->bookmarkService->addOrSet($bookmark, false);
+
+ // To preserve backward compatibility with 3rd parties, plugins still use arrays
+ $formatter = $this->getFormatter('raw');
+ $data = $formatter->format($bookmark);
+ $this->executePageHooks('save_link', $data);
+
+ $bookmark->fromArray($data, $this->container->conf->get('general.tags_separator', ' '));
+ $this->container->bookmarkService->set($bookmark);
+
+ // If we are called from the bookmarklet, we must close the popup:
+ if ($request->getParam('source') === 'bookmarklet') {
+ return $response->write('');
+ } elseif ($request->getParam('source') === 'batch') {
+ return $response;
+ }
+
+ if (!empty($request->getParam('returnurl'))) {
+ $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl');
+ }
+
+ return $this->redirectFromReferer(
+ $request,
+ $response,
+ ['/admin/add-shaare', '/admin/shaare'],
+ ['addlink', 'post', 'edit_link'],
+ $bookmark->getShortUrl()
+ );
+ }
+
+ /**
+ * Helper function used to display the shaare form whether it's a new or existing bookmark.
+ *
+ * @param array $link data used in template, either from parameters or from the data store
+ */
+ protected function displayForm(array $link, bool $isNew, Request $request, Response $response): Response
+ {
+ $data = $this->buildFormData($link, $isNew, $request);
+
+ $this->executePageHooks('render_editlink', $data, TemplatePage::EDIT_LINK);
+
+ foreach ($data as $key => $value) {
+ $this->assignView($key, $value);
+ }
+
+ $editLabel = false === $isNew ? t('Edit') . ' ' : '';
+ $this->assignView(
+ 'pagetitle',
+ $editLabel . t('Shaare') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render(TemplatePage::EDIT_LINK));
+ }
+
+ protected function buildLinkDataFromUrl(Request $request, string $url): array
+ {
+ // Check if URL is not already in database (in this case, we will edit the existing link)
+ $bookmark = $this->container->bookmarkService->findByUrl($url);
+ if (null === $bookmark) {
+ // Get shaare data if it was provided in URL (e.g.: by the bookmarklet).
+ $title = $request->getParam('title');
+ $description = $request->getParam('description');
+ $tags = $request->getParam('tags');
+ if ($request->getParam('private') !== null) {
+ $private = filter_var($request->getParam('private'), FILTER_VALIDATE_BOOLEAN);
+ } else {
+ $private = $this->container->conf->get('privacy.default_private_links', false);
+ }
+
+ // If this is an HTTP(S) link, we try go get the page to extract
+ // the title (otherwise we will to straight to the edit form.)
+ if (
+ true !== $this->container->conf->get('general.enable_async_metadata', true)
+ && empty($title)
+ && strpos(get_url_scheme($url) ?: '', 'http') !== false
+ ) {
+ $metadata = $this->container->metadataRetriever->retrieve($url);
+ }
+
+ if (empty($url)) {
+ $metadata['title'] = $this->container->conf->get('general.default_note_title', t('Note: '));
+ }
+
+ return [
+ 'title' => $title ?? $metadata['title'] ?? '',
+ 'url' => $url ?? '',
+ 'description' => $description ?? $metadata['description'] ?? '',
+ 'tags' => $tags ?? $metadata['tags'] ?? '',
+ 'private' => $private,
+ 'linkIsNew' => true,
+ ];
+ }
+
+ $formatter = $this->getFormatter('raw');
+ $link = $formatter->format($bookmark);
+ $link['linkIsNew'] = false;
+
+ return $link;
+ }
+
+ protected function buildFormData(array $link, bool $isNew, Request $request): array
+ {
+ $link['tags'] = $link['tags'] !== null && strlen($link['tags']) > 0
+ ? $link['tags'] . $this->container->conf->get('general.tags_separator', ' ')
+ : $link['tags']
+ ;
+
+ return escape([
+ 'link' => $link,
+ 'link_is_new' => $isNew,
+ 'http_referer' => $this->container->environment['HTTP_REFERER'] ?? '',
+ 'source' => $request->getParam('source') ?? '',
+ 'tags' => $this->getTags(),
+ 'default_private_links' => $this->container->conf->get('privacy.default_private_links', false),
+ 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true),
+ 'retrieve_description' => $this->container->conf->get('general.retrieve_description', false),
+ ]);
+ }
+
+ /**
+ * Memoize formatterFactory->getFormatter() calls.
+ */
+ protected function getFormatter(string $type): BookmarkFormatter
+ {
+ if (!array_key_exists($type, $this->formatters) || $this->formatters[$type] === null) {
+ $this->formatters[$type] = $this->container->formatterFactory->getFormatter($type);
+ }
+
+ return $this->formatters[$type];
+ }
+
+ /**
+ * Memoize bookmarkService->bookmarksCountPerTag() calls.
+ */
+ protected function getTags(): array
+ {
+ if ($this->tags === null) {
+ $this->tags = $this->container->bookmarkService->bookmarksCountPerTag();
+
+ if ($this->container->conf->get('formatter') === 'markdown') {
+ $this->tags[BookmarkMarkdownFormatter::NO_MD_TAG] = 1;
+ }
+ }
+
+ return $this->tags;
+ }
+}
diff --git a/application/front/controller/admin/ShaarliAdminController.php b/application/front/controller/admin/ShaarliAdminController.php
new file mode 100644
index 00000000..35fd1394
--- /dev/null
+++ b/application/front/controller/admin/ShaarliAdminController.php
@@ -0,0 +1,71 @@
+container->sessionManager->checkToken($request->getParam('token'))) {
+ throw new WrongTokenException();
+ }
+
+ return true;
+ }
+
+ /**
+ * Save a SUCCESS message in user session, which will be displayed on any template page.
+ */
+ protected function saveSuccessMessage(string $message): void
+ {
+ $this->saveMessage(SessionManager::KEY_SUCCESS_MESSAGES, $message);
+ }
+
+ /**
+ * Save a WARNING message in user session, which will be displayed on any template page.
+ */
+ protected function saveWarningMessage(string $message): void
+ {
+ $this->saveMessage(SessionManager::KEY_WARNING_MESSAGES, $message);
+ }
+
+ /**
+ * Save an ERROR message in user session, which will be displayed on any template page.
+ */
+ protected function saveErrorMessage(string $message): void
+ {
+ $this->saveMessage(SessionManager::KEY_ERROR_MESSAGES, $message);
+ }
+
+ /**
+ * Use the sessionManager to save the provided message using the proper type.
+ *
+ * @param string $type successes/warnings/errors
+ */
+ protected function saveMessage(string $type, string $message): void
+ {
+ $messages = $this->container->sessionManager->getSessionParameter($type) ?? [];
+ $messages[] = $message;
+
+ $this->container->sessionManager->setSessionParameter($type, $messages);
+ }
+}
diff --git a/application/front/controller/admin/ThumbnailsController.php b/application/front/controller/admin/ThumbnailsController.php
new file mode 100644
index 00000000..665dfd03
--- /dev/null
+++ b/application/front/controller/admin/ThumbnailsController.php
@@ -0,0 +1,65 @@
+container->bookmarkService->search()->getBookmarks() as $bookmark) {
+ // A note or not HTTP(S)
+ if ($bookmark->isNote() || !startsWith(strtolower($bookmark->getUrl()), 'http')) {
+ continue;
+ }
+
+ $ids[] = $bookmark->getId();
+ }
+
+ $this->assignView('ids', $ids);
+ $this->assignView(
+ 'pagetitle',
+ t('Thumbnails update') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render(TemplatePage::THUMBNAILS));
+ }
+
+ /**
+ * PATCH /admin/shaare/{id}/thumbnail-update - Route for AJAX calls
+ */
+ public function ajaxUpdate(Request $request, Response $response, array $args): Response
+ {
+ $id = $args['id'] ?? '';
+
+ if (false === ctype_digit($id)) {
+ return $response->withStatus(400);
+ }
+
+ try {
+ $bookmark = $this->container->bookmarkService->get((int) $id);
+ } catch (BookmarkNotFoundException $e) {
+ return $response->withStatus(404);
+ }
+
+ $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
+ $this->container->bookmarkService->set($bookmark);
+
+ return $response->withJson($this->container->formatterFactory->getFormatter('raw')->format($bookmark));
+ }
+}
diff --git a/application/front/controller/admin/TokenController.php b/application/front/controller/admin/TokenController.php
new file mode 100644
index 00000000..08d68d0a
--- /dev/null
+++ b/application/front/controller/admin/TokenController.php
@@ -0,0 +1,26 @@
+withHeader('Content-Type', 'text/plain');
+
+ return $response->write($this->container->sessionManager->generateToken());
+ }
+}
diff --git a/application/front/controller/admin/ToolsController.php b/application/front/controller/admin/ToolsController.php
new file mode 100644
index 00000000..560e5e3e
--- /dev/null
+++ b/application/front/controller/admin/ToolsController.php
@@ -0,0 +1,35 @@
+ index_url($this->container->environment),
+ 'sslenabled' => is_https($this->container->environment),
+ ];
+
+ $this->executePageHooks('render_tools', $data, TemplatePage::TOOLS);
+
+ foreach ($data as $key => $value) {
+ $this->assignView($key, $value);
+ }
+
+ $this->assignView('pagetitle', t('Tools') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'));
+
+ return $response->write($this->render(TemplatePage::TOOLS));
+ }
+}
diff --git a/application/front/controller/visitor/BookmarkListController.php b/application/front/controller/visitor/BookmarkListController.php
new file mode 100644
index 00000000..4aae2652
--- /dev/null
+++ b/application/front/controller/visitor/BookmarkListController.php
@@ -0,0 +1,239 @@
+processLegacyController($request, $response);
+ if (null !== $legacyResponse) {
+ return $legacyResponse;
+ }
+
+ $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');
+ $search = [
+ 'searchtags' => $searchTags,
+ 'searchterm' => $searchTerm,
+ ];
+
+ // 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'),
+ false,
+ ['offset' => $linksPerPage * ($page - 1), 'limit' => $linksPerPage]
+ ) ?? [];
+
+ $save = false;
+ $links = [];
+ foreach ($searchResult->getBookmarks() as $key => $bookmark) {
+ $save = $this->updateThumbnail($bookmark, false) || $save;
+ $links[$key] = $formatter->format($bookmark);
+ }
+
+ if ($save) {
+ $this->container->bookmarkService->save();
+ }
+
+ // Compute paging navigation
+ $searchtagsUrl = $searchTags === '' ? '' : '&searchtags=' . urlencode($searchTags);
+ $searchtermUrl = $searchTerm === '' ? '' : '&searchterm=' . urlencode($searchTerm);
+ $page = $searchResult->getPage();
+
+ $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));
+ $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
+
+ // Fill all template fields.
+ $data = array_merge(
+ $this->initializeTemplateVars(),
+ [
+ 'previous_page_url' => $previousPageUrl,
+ 'next_page_url' => $nextPageUrl,
+ 'page_current' => $page,
+ 'page_max' => $searchResult->getLastPage(),
+ 'result_count' => $searchResult->getTotalCount(),
+ 'search_term' => escape($searchTerm),
+ 'search_tags' => escape($searchTags),
+ 'search_tags_url' => $searchTagsUrlEncoded,
+ 'visibility' => $visibility,
+ 'links' => $links,
+ ]
+ );
+
+ if (!empty($searchTerm) || !empty($searchTags)) {
+ $data['pagetitle'] = t('Search: ');
+ $data['pagetitle'] .= ! empty($searchTerm) ? $searchTerm . ' ' : '';
+ $bracketWrap = function ($tag) {
+ return '[' . $tag . ']';
+ };
+ $data['pagetitle'] .= ! empty($searchTags)
+ ? implode(' ', array_map($bracketWrap, tags_str2array($searchTags, $tagsSeparator))) . ' '
+ : ''
+ ;
+ $data['pagetitle'] .= '- ';
+ }
+
+ $data['pagetitle'] = ($data['pagetitle'] ?? '') . $this->container->conf->get('general.title', 'Shaarli');
+
+ $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
+ $this->assignAllView($data);
+
+ return $response->write($this->render(TemplatePage::LINKLIST));
+ }
+
+ /**
+ * GET /shaare/{hash} - Display a single shaare
+ */
+ public function permalink(Request $request, Response $response, array $args): Response
+ {
+ $privateKey = $request->getParam('key');
+
+ try {
+ $bookmark = $this->container->bookmarkService->findByHash($args['hash'], $privateKey);
+ } catch (BookmarkNotFoundException $e) {
+ $this->assignView('error_message', $e->getMessage());
+
+ return $response->write($this->render(TemplatePage::ERROR_404));
+ }
+
+ $this->updateThumbnail($bookmark);
+
+ $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(),
+ [
+ 'pagetitle' => $bookmark->getTitle() . ' - ' . $this->container->conf->get('general.title', 'Shaarli'),
+ 'links' => [$formatter->format($bookmark)],
+ ]
+ );
+
+ $this->executePageHooks('render_linklist', $data, TemplatePage::LINKLIST);
+ $this->assignAllView($data);
+
+ return $response->write($this->render(TemplatePage::LINKLIST));
+ }
+
+ /**
+ * Update the thumbnail of a single bookmark if necessary.
+ */
+ protected function updateThumbnail(Bookmark $bookmark, bool $writeDatastore = true): bool
+ {
+ if (false === $this->container->loginManager->isLoggedIn()) {
+ return false;
+ }
+
+ // If thumbnail should be updated, we reset it to null
+ if ($bookmark->shouldUpdateThumbnail()) {
+ $bookmark->setThumbnail(null);
+
+ // Requires an update, not async retrieval, thumbnails enabled
+ if (
+ $bookmark->shouldUpdateThumbnail()
+ && true !== $this->container->conf->get('general.enable_async_metadata', true)
+ && $this->container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
+ ) {
+ $bookmark->setThumbnail($this->container->thumbnailer->get($bookmark->getUrl()));
+ $this->container->bookmarkService->set($bookmark, $writeDatastore);
+
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @return string[] Default template variables without values.
+ */
+ protected function initializeTemplateVars(): array
+ {
+ return [
+ 'previous_page_url' => '',
+ 'next_page_url' => '',
+ 'page_max' => '',
+ 'search_tags' => '',
+ 'result_count' => '',
+ 'async_metadata' => $this->container->conf->get('general.enable_async_metadata', true)
+ ];
+ }
+
+ /**
+ * Process legacy routes if necessary. They used query parameters.
+ * If no legacy routes is passed, return null.
+ */
+ protected function processLegacyController(Request $request, Response $response): ?Response
+ {
+ // Legacy smallhash filter
+ $queryString = $this->container->environment['QUERY_STRING'] ?? null;
+ if (null !== $queryString && 1 === preg_match('/^([a-zA-Z0-9-_@]{6})($|&|#)/', $queryString, $match)) {
+ return $this->redirect($response, '/shaare/' . $match[1]);
+ }
+
+ // Legacy controllers (mostly used for redirections)
+ if (null !== $request->getQueryParam('do')) {
+ $legacyController = new LegacyController($this->container);
+
+ try {
+ return $legacyController->process($request, $response, $request->getQueryParam('do'));
+ } catch (UnknowLegacyRouteException $e) {
+ // We ignore legacy 404
+ return null;
+ }
+ }
+
+ // Legacy GET admin routes
+ $legacyGetRoutes = array_intersect(
+ LegacyController::LEGACY_GET_ROUTES,
+ array_keys($request->getQueryParams() ?? [])
+ );
+ if (1 === count($legacyGetRoutes)) {
+ $legacyController = new LegacyController($this->container);
+
+ return $legacyController->process($request, $response, $legacyGetRoutes[0]);
+ }
+
+ return null;
+ }
+}
diff --git a/application/front/controller/visitor/DailyController.php b/application/front/controller/visitor/DailyController.php
new file mode 100644
index 00000000..3739ec16
--- /dev/null
+++ b/application/front/controller/visitor/DailyController.php
@@ -0,0 +1,206 @@
+container->bookmarkService->getLatest();
+ $dateTime = DailyPageHelper::extractRequestedDateTime($type, $request->getQueryParam($type), $latestBookmark);
+ $start = DailyPageHelper::getStartDateTimeByType($type, $dateTime);
+ $end = DailyPageHelper::getEndDateTimeByType($type, $dateTime);
+ $dailyDesc = DailyPageHelper::getDescriptionByType($type, $dateTime);
+
+ $linksToDisplay = $this->container->bookmarkService->findByDate(
+ $start,
+ $end,
+ $previousDay,
+ $nextDay
+ );
+
+ $formatter = $this->container->formatterFactory->getFormatter();
+ $formatter->addContextData('base_path', $this->container->basePath);
+ // We pre-format some fields for proper output.
+ foreach ($linksToDisplay as $key => $bookmark) {
+ $linksToDisplay[$key] = $formatter->format($bookmark);
+ // This page is a bit specific, we need raw description to calculate the length
+ $linksToDisplay[$key]['formatedDescription'] = $linksToDisplay[$key]['description'];
+ $linksToDisplay[$key]['description'] = $bookmark->getDescription();
+ }
+
+ $data = [
+ 'linksToDisplay' => $linksToDisplay,
+ 'dayDate' => $start,
+ 'day' => $start->getTimestamp(),
+ 'previousday' => $previousDay ? $previousDay->format($format) : '',
+ 'nextday' => $nextDay ? $nextDay->format($format) : '',
+ 'dayDesc' => $dailyDesc,
+ 'type' => $type,
+ 'localizedType' => $this->translateType($type),
+ ];
+
+ // Hooks are called before column construction so that plugins don't have to deal with columns.
+ $this->executePageHooks('render_daily', $data, TemplatePage::DAILY);
+
+ $data['cols'] = $this->calculateColumns($data['linksToDisplay']);
+
+ $this->assignAllView($data);
+
+ $mainTitle = $this->container->conf->get('general.title', 'Shaarli');
+ $this->assignView(
+ 'pagetitle',
+ $data['localizedType'] . ' - ' . $data['dayDesc'] . ' - ' . $mainTitle
+ );
+
+ return $response->write($this->render(TemplatePage::DAILY));
+ }
+
+ /**
+ * Daily RSS feed: 1 RSS entry per day giving all the bookmarks on that day.
+ * Gives the last 7 days (which have bookmarks).
+ * This RSS feed cannot be filtered and does not trigger plugins yet.
+ */
+ 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, $cacheDuration);
+
+ $cached = $cache->cachedVersion();
+ if (!empty($cached)) {
+ return $response->write($cached);
+ }
+
+ $days = [];
+ $format = DailyPageHelper::getFormatByType($type);
+ $length = DailyPageHelper::getRssLengthByType($type);
+ foreach ($this->container->bookmarkService->search()->getBookmarks() as $bookmark) {
+ $day = $bookmark->getCreated()->format($format);
+
+ // Stop iterating after DAILY_RSS_NB_DAYS entries
+ if (count($days) === $length && !isset($days[$day])) {
+ break;
+ }
+
+ $days[$day][] = $bookmark;
+ }
+
+ // Build the RSS feed.
+ $indexUrl = escape(index_url($this->container->environment));
+
+ $formatter = $this->container->formatterFactory->getFormatter();
+ $formatter->addContextData('index_url', $indexUrl);
+
+ $dataPerDay = [];
+
+ /** @var Bookmark[] $bookmarks */
+ foreach ($days as $day => $bookmarks) {
+ $dayDateTime = DailyPageHelper::extractRequestedDateTime($type, (string) $day);
+ $endDateTime = DailyPageHelper::getEndDateTimeByType($type, $dayDateTime);
+
+ // We only want the RSS entry to be published when the period is over.
+ if (new DateTime() < $endDateTime) {
+ continue;
+ }
+
+ $dataPerDay[$day] = [
+ 'date' => $endDateTime,
+ 'date_rss' => $endDateTime->format(DateTime::RSS),
+ 'date_human' => DailyPageHelper::getDescriptionByType($type, $dayDateTime, false),
+ 'absolute_url' => $indexUrl . 'daily?' . $type . '=' . $day,
+ 'links' => [],
+ ];
+
+ foreach ($bookmarks as $key => $bookmark) {
+ $dataPerDay[$day]['links'][$key] = $formatter->format($bookmark);
+
+ // Make permalink URL absolute
+ if ($bookmark->isNote()) {
+ $dataPerDay[$day]['links'][$key]['url'] = rtrim($indexUrl, '/') . $bookmark->getUrl();
+ }
+ }
+ }
+
+ $this->assignAllView([
+ 'title' => $this->container->conf->get('general.title', 'Shaarli'),
+ 'index_url' => $indexUrl,
+ 'page_url' => $pageUrl,
+ 'hide_timestamps' => $this->container->conf->get('privacy.hide_timestamps', false),
+ 'days' => $dataPerDay,
+ 'type' => $type,
+ 'localizedType' => $this->translateType($type),
+ ]);
+
+ $rssContent = $this->render(TemplatePage::DAILY_RSS);
+
+ $cache->cache($rssContent);
+
+ return $response->write($rssContent);
+ }
+
+ /**
+ * We need to spread the articles on 3 columns.
+ * did not want to use a JavaScript lib like http://masonry.desandro.com/
+ * so I manually spread entries with a simple method: I roughly evaluate the
+ * height of a div according to title and description length.
+ */
+ protected function calculateColumns(array $links): array
+ {
+ // Entries to display, for each column.
+ $columns = [[], [], []];
+ // Rough estimate of columns fill.
+ $fill = [0, 0, 0];
+ foreach ($links as $link) {
+ // Roughly estimate length of entry (by counting characters)
+ // Title: 30 chars = 1 line. 1 line is 30 pixels height.
+ // Description: 836 characters gives roughly 342 pixel height.
+ // This is not perfect, but it's usually OK.
+ $length = strlen($link['title'] ?? '') + (342 * strlen($link['description'] ?? '')) / 836;
+ if (! empty($link['thumbnail'])) {
+ $length += 100; // 1 thumbnails roughly takes 100 pixels height.
+ }
+ // Then put in column which is the less filled:
+ $smallest = min($fill); // find smallest value in array.
+ $index = array_search($smallest, $fill); // find index of this smallest value.
+ array_push($columns[$index], $link); // Put entry in this column.
+ $fill[$index] += $length;
+ }
+
+ return $columns;
+ }
+
+ protected function translateType($type): string
+ {
+ return [
+ t('day') => t('Daily'),
+ t('week') => t('Weekly'),
+ t('month') => t('Monthly'),
+ ][t($type)] ?? t('Daily');
+ }
+}
diff --git a/application/front/controller/visitor/ErrorController.php b/application/front/controller/visitor/ErrorController.php
new file mode 100644
index 00000000..428e8254
--- /dev/null
+++ b/application/front/controller/visitor/ErrorController.php
@@ -0,0 +1,47 @@
+container->pageBuilder->reset();
+
+ if ($throwable instanceof ShaarliFrontException) {
+ // Functional error
+ $this->assignView('message', nl2br($throwable->getMessage()));
+
+ $response = $response->withStatus($throwable->getCode());
+ } else {
+ // Internal error (any other Throwable)
+ if ($this->container->conf->get('dev.debug', false) || $this->container->loginManager->isLoggedIn()) {
+ $this->assignView('message', t('Error: ') . $throwable->getMessage());
+ $this->assignView(
+ 'text',
+ ''
+ . t('Please report it on Github.')
+ . ''
+ );
+ $this->assignView('stacktrace', exception2text($throwable));
+ } else {
+ $this->assignView('message', t('An unexpected error occurred.'));
+ }
+
+ $response = $response->withStatus(500);
+ }
+
+ return $response->write($this->render('error'));
+ }
+}
diff --git a/application/front/controller/visitor/ErrorNotFoundController.php b/application/front/controller/visitor/ErrorNotFoundController.php
new file mode 100644
index 00000000..758dd83b
--- /dev/null
+++ b/application/front/controller/visitor/ErrorNotFoundController.php
@@ -0,0 +1,29 @@
+getRequestTarget(), '/api/v1')) {
+ return $response->withStatus(404);
+ }
+
+ // This is required because the middleware is ignored if the route is not found.
+ $this->container->basePath = rtrim($request->getUri()->getBasePath(), '/');
+
+ $this->assignView('error_message', t('Requested page could not be found.'));
+
+ return $response->withStatus(404)->write($this->render('404'));
+ }
+}
diff --git a/application/front/controller/visitor/FeedController.php b/application/front/controller/visitor/FeedController.php
new file mode 100644
index 00000000..edc7ef43
--- /dev/null
+++ b/application/front/controller/visitor/FeedController.php
@@ -0,0 +1,58 @@
+processRequest(FeedBuilder::$FEED_ATOM, $request, $response);
+ }
+
+ public function rss(Request $request, Response $response): Response
+ {
+ return $this->processRequest(FeedBuilder::$FEED_RSS, $request, $response);
+ }
+
+ protected function processRequest(string $feedType, Request $request, Response $response): Response
+ {
+ $response = $response->withHeader('Content-Type', 'application/' . $feedType . '+xml; charset=utf-8');
+
+ $pageUrl = page_url($this->container->environment);
+ $cache = $this->container->pageCacheManager->getCachePage($pageUrl);
+
+ $cached = $cache->cachedVersion();
+ if (!empty($cached)) {
+ return $response->write($cached);
+ }
+
+ // Generate data.
+ $this->container->feedBuilder->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
+ $this->container->feedBuilder->setHideDates($this->container->conf->get('privacy.hide_timestamps', false));
+ $this->container->feedBuilder->setUsePermalinks(
+ null !== $request->getParam('permalinks') || !$this->container->conf->get('feed.rss_permalinks')
+ );
+
+ $data = $this->container->feedBuilder->buildData($feedType, $request->getParams());
+
+ $this->executePageHooks('render_feed', $data, 'feed.' . $feedType);
+ $this->assignAllView($data);
+
+ $content = $this->render('feed.' . $feedType);
+
+ $cache->cache($content);
+
+ return $response->write($content);
+ }
+}
diff --git a/application/front/controller/visitor/InstallController.php b/application/front/controller/visitor/InstallController.php
new file mode 100644
index 00000000..6ffeb2db
--- /dev/null
+++ b/application/front/controller/visitor/InstallController.php
@@ -0,0 +1,183 @@
+container->conf->getConfigFileExt())) {
+ throw new AlreadyInstalledException();
+ }
+ }
+
+ /**
+ * Display the install template page.
+ * Also test file permissions and sessions beforehand.
+ */
+ public function index(Request $request, Response $response): Response
+ {
+ // Before installation, we'll make sure that permissions are set properly, and sessions are working.
+ $this->checkPermissions();
+
+ if (
+ static::SESSION_TEST_VALUE
+ !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
+ ) {
+ $this->container->sessionManager->setSessionParameter(static::SESSION_TEST_KEY, static::SESSION_TEST_VALUE);
+
+ return $this->redirect($response, '/install/session-test');
+ }
+
+ [$continents, $cities] = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
+
+ $this->assignView('continents', $continents);
+ $this->assignView('cities', $cities);
+ $this->assignView('languages', Languages::getAvailableLanguages());
+
+ $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', $permissions);
+
+ $this->assignView('pagetitle', t('Install Shaarli'));
+
+ return $response->write($this->render('install'));
+ }
+
+ /**
+ * Route checking that the session parameter has been properly saved between two distinct requests.
+ * If the session parameter is preserved, redirect to install template page, otherwise displays error.
+ */
+ public function sessionTest(Request $request, Response $response): Response
+ {
+ // This part makes sure sessions works correctly.
+ // (Because on some hosts, session.save_path may not be set correctly,
+ // or we may not have write access to it.)
+ if (
+ static::SESSION_TEST_VALUE
+ !== $this->container->sessionManager->getSessionParameter(static::SESSION_TEST_KEY)
+ ) {
+ // Step 2: Check if data in session is correct.
+ $msg = t(
+ '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.
'
+ );
+ $msg = sprintf($msg, $this->container->sessionManager->getSavePath());
+
+ $this->assignView('message', $msg);
+
+ return $response->write($this->render('error'));
+ }
+
+ return $this->redirect($response, '/install');
+ }
+
+ /**
+ * Save installation form and initialize config file and datastore if necessary.
+ */
+ public function save(Request $request, Response $response): Response
+ {
+ $timezone = 'UTC';
+ if (
+ !empty($request->getParam('continent'))
+ && !empty($request->getParam('city'))
+ && isTimeZoneValid($request->getParam('continent'), $request->getParam('city'))
+ ) {
+ $timezone = $request->getParam('continent') . '/' . $request->getParam('city');
+ }
+ $this->container->conf->set('general.timezone', $timezone);
+
+ $login = $request->getParam('setlogin');
+ $this->container->conf->set('credentials.login', $login);
+ $salt = sha1(uniqid('', true) . '_' . mt_rand());
+ $this->container->conf->set('credentials.salt', $salt);
+ $this->container->conf->set('credentials.hash', sha1($request->getParam('setpassword') . $login . $salt));
+
+ if (!empty($request->getParam('title'))) {
+ $this->container->conf->set('general.title', escape($request->getParam('title')));
+ } else {
+ $this->container->conf->set(
+ 'general.title',
+ t('Shared Bookmarks')
+ );
+ }
+
+ $this->container->conf->set('translation.language', escape($request->getParam('language')));
+ $this->container->conf->set('updates.check_updates', !empty($request->getParam('updateCheck')));
+ $this->container->conf->set('api.enabled', !empty($request->getParam('enableApi')));
+ $this->container->conf->set(
+ 'api.secret',
+ generate_api_secret(
+ $this->container->conf->get('credentials.login'),
+ $this->container->conf->get('credentials.salt')
+ )
+ );
+ $this->container->conf->set('general.header_link', $this->container->basePath . '/');
+
+ try {
+ // Everything is ok, let's create config file.
+ $this->container->conf->write($this->container->loginManager->isLoggedIn());
+ } catch (\Exception $e) {
+ $this->assignView('message', t('Error while writing config file after configuration update.'));
+ $this->assignView('stacktrace', $e->getMessage() . PHP_EOL . $e->getTraceAsString());
+
+ return $response->write($this->render('error'));
+ }
+
+ $this->container->sessionManager->setSessionParameter(
+ SessionManager::KEY_SUCCESS_MESSAGES,
+ [t('Shaarli is now configured. Please login and start shaaring your bookmarks!')]
+ );
+
+ return $this->redirect($response, '/login');
+ }
+
+ protected function checkPermissions(): bool
+ {
+ // Ensure Shaarli has proper access to its resources
+ $errors = ApplicationUtils::checkResourcePermissions($this->container->conf, true);
+ if (empty($errors)) {
+ return true;
+ }
+
+ $message = t('Insufficient permissions:') . PHP_EOL;
+ foreach ($errors as $error) {
+ $message .= PHP_EOL . $error;
+ }
+
+ throw new ResourcePermissionException($message);
+ }
+}
diff --git a/application/front/controller/visitor/LoginController.php b/application/front/controller/visitor/LoginController.php
new file mode 100644
index 00000000..4b881535
--- /dev/null
+++ b/application/front/controller/visitor/LoginController.php
@@ -0,0 +1,155 @@
+checkLoginState();
+ } catch (CantLoginException $e) {
+ return $this->redirect($response, '/');
+ }
+
+ if ($request->getParam('login') !== null) {
+ $this->assignView('username', escape($request->getParam('login')));
+ }
+
+ $returnUrl = $request->getParam('returnurl') ?? $this->container->environment['HTTP_REFERER'] ?? null;
+
+ $this
+ ->assignView('returnurl', escape($returnUrl))
+ ->assignView('remember_user_default', $this->container->conf->get('privacy.remember_user_default', true))
+ ->assignView('pagetitle', t('Login') . ' - ' . $this->container->conf->get('general.title', 'Shaarli'))
+ ;
+
+ return $response->write($this->render(TemplatePage::LOGIN));
+ }
+
+ /**
+ * POST /login - Process login
+ */
+ public function login(Request $request, Response $response): Response
+ {
+ if (!$this->container->sessionManager->checkToken($request->getParam('token'))) {
+ throw new WrongTokenException();
+ }
+
+ try {
+ $this->checkLoginState();
+ } catch (CantLoginException $e) {
+ return $this->redirect($response, '/');
+ }
+
+ if (
+ !$this->container->loginManager->checkCredentials(
+ client_ip_id($this->container->environment),
+ $request->getParam('login'),
+ $request->getParam('password')
+ )
+ ) {
+ $this->container->loginManager->handleFailedLogin($this->container->environment);
+
+ $this->container->sessionManager->setSessionParameter(
+ SessionManager::KEY_ERROR_MESSAGES,
+ [t('Wrong login/password.')]
+ );
+
+ // Call controller directly instead of unnecessary redirection
+ return $this->index($request, $response);
+ }
+
+ $this->container->loginManager->handleSuccessfulLogin($this->container->environment);
+
+ $cookiePath = $this->container->basePath . '/';
+ $expirationTime = $this->saveLongLastingSession($request, $cookiePath);
+ $this->renewUserSession($cookiePath, $expirationTime);
+
+ // Force referer from given return URL
+ $this->container->environment['HTTP_REFERER'] = $request->getParam('returnurl');
+
+ return $this->redirectFromReferer($request, $response, ['login', 'install']);
+ }
+
+ /**
+ * Make sure that the user is allowed to login and/or displaying the login page:
+ * - not already logged in
+ * - not open shaarli
+ * - not banned
+ */
+ protected function checkLoginState(): bool
+ {
+ if (
+ $this->container->loginManager->isLoggedIn()
+ || $this->container->conf->get('security.open_shaarli', false)
+ ) {
+ throw new CantLoginException();
+ }
+
+ if (true !== $this->container->loginManager->canLogin($this->container->environment)) {
+ throw new LoginBannedException();
+ }
+
+ return true;
+ }
+
+ /**
+ * @return int Session duration in seconds
+ */
+ protected function saveLongLastingSession(Request $request, string $cookiePath): int
+ {
+ if (empty($request->getParam('longlastingsession'))) {
+ // Standard session expiration (=when browser closes)
+ $expirationTime = 0;
+ } else {
+ // Keep the session cookie even after the browser closes
+ $this->container->sessionManager->setStaySignedIn(true);
+ $expirationTime = $this->container->sessionManager->extendSession();
+ }
+
+ $this->container->cookieManager->setCookieParameter(
+ CookieManager::STAY_SIGNED_IN,
+ $this->container->loginManager->getStaySignedInToken(),
+ $expirationTime,
+ $cookiePath
+ );
+
+ return $expirationTime;
+ }
+
+ protected function renewUserSession(string $cookiePath, int $expirationTime): void
+ {
+ // Send cookie with the new expiration date to the browser
+ $this->container->sessionManager->destroy();
+ $this->container->sessionManager->cookieParameters(
+ $expirationTime,
+ $cookiePath,
+ $this->container->environment['SERVER_NAME']
+ );
+ $this->container->sessionManager->start();
+ $this->container->sessionManager->regenerateId(true);
+ }
+}
diff --git a/application/front/controller/visitor/OpenSearchController.php b/application/front/controller/visitor/OpenSearchController.php
new file mode 100644
index 00000000..36d60acf
--- /dev/null
+++ b/application/front/controller/visitor/OpenSearchController.php
@@ -0,0 +1,27 @@
+withHeader('Content-Type', 'application/opensearchdescription+xml; charset=utf-8');
+
+ $this->assignView('serverurl', index_url($this->container->environment));
+
+ return $response->write($this->render(TemplatePage::OPEN_SEARCH));
+ }
+}
diff --git a/application/front/controller/visitor/PictureWallController.php b/application/front/controller/visitor/PictureWallController.php
new file mode 100644
index 00000000..9c8f07d7
--- /dev/null
+++ b/application/front/controller/visitor/PictureWallController.php
@@ -0,0 +1,54 @@
+container->conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
+ throw new ThumbnailsDisabledException();
+ }
+
+ $this->assignView(
+ 'pagetitle',
+ t('Picture wall') . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ // Optionally filter the results:
+ $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 ($bookmarks as $key => $bookmark) {
+ if (!empty($bookmark->getThumbnail())) {
+ $links[] = $formatter->format($bookmark);
+ }
+ }
+
+ $data = ['linksToDisplay' => $links];
+ $this->executePageHooks('render_picwall', $data, TemplatePage::PICTURE_WALL);
+
+ foreach ($data as $key => $value) {
+ $this->assignView($key, $value);
+ }
+
+ return $response->write($this->render(TemplatePage::PICTURE_WALL));
+ }
+}
diff --git a/application/front/controller/visitor/PublicSessionFilterController.php b/application/front/controller/visitor/PublicSessionFilterController.php
new file mode 100644
index 00000000..1a66362d
--- /dev/null
+++ b/application/front/controller/visitor/PublicSessionFilterController.php
@@ -0,0 +1,46 @@
+getParam('nb') ?? null;
+ if (null === $linksPerPage || false === is_numeric($linksPerPage)) {
+ $linksPerPage = $this->container->conf->get('general.links_per_page', 20);
+ }
+
+ $this->container->sessionManager->setSessionParameter(
+ SessionManager::KEY_LINKS_PER_PAGE,
+ abs(intval($linksPerPage))
+ );
+
+ return $this->redirectFromReferer($request, $response, ['linksperpage'], ['nb']);
+ }
+
+ /**
+ * GET /untagged-only: allows to display only bookmarks without any tag
+ */
+ public function untaggedOnly(Request $request, Response $response): Response
+ {
+ $this->container->sessionManager->setSessionParameter(
+ SessionManager::KEY_UNTAGGED_ONLY,
+ empty($this->container->sessionManager->getSessionParameter(SessionManager::KEY_UNTAGGED_ONLY))
+ );
+
+ return $this->redirectFromReferer($request, $response, ['untaggedonly', 'untagged-only']);
+ }
+}
diff --git a/application/front/controller/visitor/ShaarliVisitorController.php b/application/front/controller/visitor/ShaarliVisitorController.php
new file mode 100644
index 00000000..d3f28f2f
--- /dev/null
+++ b/application/front/controller/visitor/ShaarliVisitorController.php
@@ -0,0 +1,186 @@
+container = $container;
+ }
+
+ /**
+ * Assign variables to RainTPL template through the PageBuilder.
+ *
+ * @param mixed $value Value to assign to the template
+ */
+ protected function assignView(string $name, $value): self
+ {
+ $this->container->pageBuilder->assign($name, $value);
+
+ return $this;
+ }
+
+ /**
+ * Assign variables to RainTPL template through the PageBuilder.
+ *
+ * @param mixed $data Values to assign to the template and their keys
+ */
+ protected function assignAllView(array $data): self
+ {
+ foreach ($data as $key => $value) {
+ $this->assignView($key, $value);
+ }
+
+ return $this;
+ }
+
+ 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));
+
+ $this->executeDefaultHooks($template);
+
+ $this->assignView('plugin_errors', $this->container->pluginManager->getErrors());
+
+ return $this->container->pageBuilder->render($template, $this->container->basePath);
+ }
+
+ /**
+ * Call plugin hooks for header, footer and includes, specifying which page will be rendered.
+ * Then assign generated data to RainTPL.
+ */
+ protected function executeDefaultHooks(string $template): void
+ {
+ $common_hooks = [
+ 'includes',
+ 'header',
+ 'footer',
+ ];
+
+ $parameters = $this->buildPluginParameters($template);
+
+ foreach ($common_hooks as $name) {
+ $pluginData = [];
+ $this->container->pluginManager->executeHooks(
+ 'render_' . $name,
+ $pluginData,
+ $parameters
+ );
+ $this->assignView('plugins_' . $name, $pluginData);
+ }
+ }
+
+ protected function executePageHooks(string $hook, array &$data, string $template = null): void
+ {
+ $this->container->pluginManager->executeHooks(
+ $hook,
+ $data,
+ $this->buildPluginParameters($template)
+ );
+ }
+
+ protected function buildPluginParameters(?string $template): array
+ {
+ return [
+ 'target' => $template,
+ 'loggedin' => $this->container->loginManager->isLoggedIn(),
+ 'basePath' => $this->container->basePath,
+ 'rootPath' => preg_replace('#/index\.php$#', '', $this->container->basePath),
+ 'bookmarkService' => $this->container->bookmarkService
+ ];
+ }
+
+ /**
+ * Simple helper which prepend the base path to redirect path.
+ *
+ * @param Response $response
+ * @param string $path Absolute path, e.g.: `/`, or `/admin/shaare/123` regardless of install directory
+ *
+ * @return Response updated
+ */
+ protected function redirect(Response $response, string $path): Response
+ {
+ return $response->withRedirect($this->container->basePath . $path);
+ }
+
+ /**
+ * Generates a redirection to the previous page, based on the HTTP_REFERER.
+ * It fails back to the home page.
+ *
+ * @param array $loopTerms Terms to remove from path and query string to prevent direction loop.
+ * @param array $clearParams List of parameter to remove from the query string of the referrer.
+ */
+ protected function redirectFromReferer(
+ Request $request,
+ Response $response,
+ array $loopTerms = [],
+ array $clearParams = [],
+ string $anchor = null
+ ): Response {
+ $defaultPath = $this->container->basePath . '/';
+ $referer = $this->container->environment['HTTP_REFERER'] ?? null;
+
+ if (null !== $referer) {
+ $currentUrl = parse_url($referer);
+ // If the referer is not related to Shaarli instance, redirect to default
+ if (
+ isset($currentUrl['host'])
+ && strpos(index_url($this->container->environment), $currentUrl['host']) === false
+ ) {
+ return $response->withRedirect($defaultPath);
+ }
+
+ parse_str($currentUrl['query'] ?? '', $params);
+ $path = $currentUrl['path'] ?? $defaultPath;
+ } else {
+ $params = [];
+ $path = $defaultPath;
+ }
+
+ // Prevent redirection loop
+ if (isset($currentUrl)) {
+ foreach ($clearParams as $value) {
+ unset($params[$value]);
+ }
+
+ $checkQuery = implode('', array_keys($params));
+ foreach ($loopTerms as $value) {
+ if (strpos($path . $checkQuery, $value) !== false) {
+ $params = [];
+ $path = $defaultPath;
+ break;
+ }
+ }
+ }
+
+ $queryString = count($params) > 0 ? '?' . http_build_query($params) : '';
+ $anchor = $anchor ? '#' . $anchor : '';
+
+ return $response->withRedirect($path . $queryString . $anchor);
+ }
+}
diff --git a/application/front/controller/visitor/TagCloudController.php b/application/front/controller/visitor/TagCloudController.php
new file mode 100644
index 00000000..46d62779
--- /dev/null
+++ b/application/front/controller/visitor/TagCloudController.php
@@ -0,0 +1,123 @@
+processRequest(static::TYPE_CLOUD, $request, $response);
+ }
+
+ /**
+ * Display the tag list through the template engine.
+ * This controller a few filters:
+ * - Visibility stored in the session for logged in users
+ * - `searchtags` query parameter: will return tags associated with filter in at least one bookmark
+ * - `sort` query parameters:
+ * + `usage` (default): most used tags first
+ * + `alpha`: alphabetical order
+ */
+ public function list(Request $request, Response $response): Response
+ {
+ return $this->processRequest(static::TYPE_LIST, $request, $response);
+ }
+
+ /**
+ * Process the request for both tag cloud and tag list endpoints.
+ */
+ protected function processRequest(string $type, Request $request, Response $response): Response
+ {
+ $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
+ if ($this->container->loginManager->isLoggedIn() === true) {
+ $visibility = $this->container->sessionManager->getSessionParameter('visibility');
+ }
+
+ $sort = $request->getQueryParam('sort');
+ $searchTags = $request->getQueryParam('searchtags');
+ $filteringTags = $searchTags !== null ? explode($tagsSeparator, $searchTags) : [];
+
+ $tags = $this->container->bookmarkService->bookmarksCountPerTag($filteringTags, $visibility ?? null);
+
+ if (static::TYPE_CLOUD === $type || 'alpha' === $sort) {
+ // TODO: the sorting should be handled by bookmarkService instead of the controller
+ alphabetical_sort($tags, false, true);
+ }
+
+ if (static::TYPE_CLOUD === $type) {
+ $tags = $this->formatTagsForCloud($tags);
+ }
+
+ $tagsUrl = [];
+ foreach ($tags as $tag => $value) {
+ $tagsUrl[escape($tag)] = urlencode((string) $tag);
+ }
+
+ $searchTags = tags_array2str($filteringTags, $tagsSeparator);
+ $searchTags = !empty($searchTags) ? trim($searchTags, $tagsSeparator) . $tagsSeparator : '';
+ $searchTagsUrl = urlencode($searchTags);
+ $data = [
+ 'search_tags' => escape($searchTags),
+ 'search_tags_url' => $searchTagsUrl,
+ 'tags' => escape($tags),
+ 'tags_url' => $tagsUrl,
+ ];
+ $this->executePageHooks('render_tag' . $type, $data, 'tag.' . $type);
+ $this->assignAllView($data);
+
+ $searchTags = !empty($searchTags) ? trim(str_replace($tagsSeparator, ' ', $searchTags)) . ' - ' : '';
+ $this->assignView(
+ 'pagetitle',
+ $searchTags . t('Tag ' . $type) . ' - ' . $this->container->conf->get('general.title', 'Shaarli')
+ );
+
+ return $response->write($this->render('tag.' . $type));
+ }
+
+ /**
+ * Format the tags array for the tag cloud template.
+ *
+ * @param array $tags List of tags as key with count as value
+ *
+ * @return mixed[] List of tags as key, with count and expected font size in a subarray
+ */
+ protected function formatTagsForCloud(array $tags): array
+ {
+ // We sort tags alphabetically, then choose a font size according to count.
+ // First, find max value.
+ $maxCount = count($tags) > 0 ? max($tags) : 0;
+ $logMaxCount = $maxCount > 1 ? log($maxCount, 30) : 1;
+ $tagList = [];
+ foreach ($tags as $key => $value) {
+ // Tag font size scaling:
+ // default 15 and 30 logarithm bases affect scaling,
+ // 2.2 and 0.8 are arbitrary font sizes in em.
+ $size = log($value, 15) / $logMaxCount * 2.2 + 0.8;
+ $tagList[$key] = [
+ 'count' => $value,
+ 'size' => number_format($size, 2, '.', ''),
+ ];
+ }
+
+ return $tagList;
+ }
+}
diff --git a/application/front/controller/visitor/TagController.php b/application/front/controller/visitor/TagController.php
new file mode 100644
index 00000000..3aa58542
--- /dev/null
+++ b/application/front/controller/visitor/TagController.php
@@ -0,0 +1,120 @@
+container->environment['HTTP_REFERER'] ?? null;
+
+ // In case browser does not send HTTP_REFERER, we search a single tag
+ if (null === $referer) {
+ if (null !== $newTag) {
+ return $this->redirect($response, '/?searchtags=' . urlencode($newTag));
+ }
+
+ return $this->redirect($response, '/');
+ }
+
+ $currentUrl = parse_url($referer);
+ parse_str($currentUrl['query'] ?? '', $params);
+
+ if (null === $newTag) {
+ return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params));
+ }
+
+ // Prevent redirection loop
+ if (isset($params['addtag'])) {
+ unset($params['addtag']);
+ }
+
+ $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
+ // Check if this tag is already in the search query and ignore it if it is.
+ // Each tag is always separated by a space
+ $currentTags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator);
+
+ $addtag = true;
+ foreach ($currentTags as $value) {
+ if ($value === $newTag) {
+ $addtag = false;
+ break;
+ }
+ }
+
+ // Append the tag if necessary
+ if (true === $addtag) {
+ $currentTags[] = trim($newTag);
+ }
+
+ $params['searchtags'] = tags_array2str($currentTags, $tagsSeparator);
+
+ // We also remove page (keeping the same page has no sense, since the results are different)
+ unset($params['page']);
+
+ return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params));
+ }
+
+ /**
+ * Remove a tag from the current search through an HTTP redirection.
+ *
+ * @param array $args Should contain `tag` key as tag to remove from current search
+ */
+ public function removeTag(Request $request, Response $response, array $args): Response
+ {
+ $referer = $this->container->environment['HTTP_REFERER'] ?? null;
+
+ // If the referrer is not provided, we can update the search, so we failback on the bookmark list
+ if (empty($referer)) {
+ return $this->redirect($response, '/');
+ }
+
+ $tagToRemove = $args['tag'] ?? null;
+ $currentUrl = parse_url($referer);
+ parse_str($currentUrl['query'] ?? '', $params);
+
+ if (null === $tagToRemove) {
+ return $response->withRedirect(($currentUrl['path'] ?? './') . '?' . http_build_query($params));
+ }
+
+ // Prevent redirection loop
+ if (isset($params['removetag'])) {
+ unset($params['removetag']);
+ }
+
+ if (isset($params['searchtags'])) {
+ $tagsSeparator = $this->container->conf->get('general.tags_separator', ' ');
+ $tags = tags_str2array($params['searchtags'] ?? '', $tagsSeparator);
+ // Remove value from array $tags.
+ $tags = array_diff($tags, [$tagToRemove]);
+ $params['searchtags'] = tags_array2str($tags, $tagsSeparator);
+
+ if (empty($params['searchtags'])) {
+ unset($params['searchtags']);
+ }
+
+ // We also remove page (keeping the same page has no sense, since the results are different)
+ unset($params['page']);
+ }
+
+ $queryParams = count($params) > 0 ? '?' . http_build_query($params) : '';
+
+ return $response->withRedirect(($currentUrl['path'] ?? './') . $queryParams);
+ }
+}
diff --git a/application/front/exceptions/AlreadyInstalledException.php b/application/front/exceptions/AlreadyInstalledException.php
new file mode 100644
index 00000000..4add86cf
--- /dev/null
+++ b/application/front/exceptions/AlreadyInstalledException.php
@@ -0,0 +1,15 @@
+';
+
+ /**
+ * Gets the latest version code from the Git repository
+ *
+ * The code is read from the raw content of the version file on the Git server.
+ *
+ * @param string $url URL to reach to get the latest version.
+ * @param int $timeout Timeout to check the URL (in seconds).
+ *
+ * @return mixed the version code from the repository if available, else 'false'
+ */
+ public static function getLatestGitVersionCode($url, $timeout = 2)
+ {
+ list($headers, $data) = get_http_response($url, $timeout);
+
+ if (preg_match('#HTTP/[\d\.]+ 200(?: OK)?#', $headers[0]) !== 1) {
+ error_log('Failed to retrieve ' . $url);
+ return false;
+ }
+
+ return $data;
+ }
+
+ /**
+ * Retrieve the version from a remote URL or a file.
+ *
+ * @param string $remote URL or file to fetch.
+ * @param int $timeout For URLs fetching.
+ *
+ * @return bool|string The version or false if it couldn't be retrieved.
+ */
+ public static function getVersion($remote, $timeout = 2)
+ {
+ if (startsWith($remote, 'http')) {
+ if (($data = static::getLatestGitVersionCode($remote, $timeout)) === false) {
+ return false;
+ }
+ } else {
+ if (!is_file($remote)) {
+ return false;
+ }
+ $data = file_get_contents($remote);
+ }
+
+ return str_replace(
+ [self::$VERSION_START_TAG, self::$VERSION_END_TAG, PHP_EOL],
+ ['', '', ''],
+ $data
+ );
+ }
+
+ /**
+ * Checks if a new Shaarli version has been published on the Git repository
+ *
+ * Updates checks are run periodically, according to the following criteria:
+ * - the update checks are enabled (install, global config);
+ * - the user is logged in (or this is an open instance);
+ * - the last check is older than a given interval;
+ * - the check is non-blocking if the HTTPS connection to Git fails;
+ * - in case of failure, the update file's modification date is updated,
+ * to avoid intempestive connection attempts.
+ *
+ * @param string $currentVersion the current version code
+ * @param string $updateFile the file where to store the latest version code
+ * @param int $checkInterval the minimum interval between update checks (in seconds
+ * @param bool $enableCheck whether to check for new versions
+ * @param bool $isLoggedIn whether the user is logged in
+ * @param string $branch check update for the given branch
+ *
+ * @throws Exception an invalid branch has been set for update checks
+ *
+ * @return mixed the new version code if available and greater, else 'false'
+ */
+ public static function checkUpdate(
+ $currentVersion,
+ $updateFile,
+ $checkInterval,
+ $enableCheck,
+ $isLoggedIn,
+ $branch = 'stable'
+ ) {
+ // Do not check versions for visitors
+ // Do not check if the user doesn't want to
+ // Do not check with dev version
+ if (!$isLoggedIn || empty($enableCheck) || $currentVersion === 'dev') {
+ return false;
+ }
+
+ if (is_file($updateFile) && (filemtime($updateFile) > time() - $checkInterval)) {
+ // Shaarli has checked for updates recently - skip HTTP query
+ $latestKnownVersion = file_get_contents($updateFile);
+
+ if (version_compare($latestKnownVersion, $currentVersion) == 1) {
+ return $latestKnownVersion;
+ }
+ return false;
+ }
+
+ if (!in_array($branch, self::$GIT_BRANCHES)) {
+ throw new Exception(
+ 'Invalid branch selected for updates: "' . $branch . '"'
+ );
+ }
+
+ // Late Static Binding allows overriding within tests
+ // See http://php.net/manual/en/language.oop5.late-static-bindings.php
+ $latestVersion = static::getVersion(
+ self::$GIT_RAW_URL . '/' . $branch . '/' . self::$VERSION_FILE
+ );
+
+ if (!$latestVersion) {
+ // Only update the file's modification date
+ file_put_contents($updateFile, $currentVersion);
+ return false;
+ }
+
+ // Update the file's content and modification date
+ file_put_contents($updateFile, $latestVersion);
+
+ if (version_compare($latestVersion, $currentVersion) == 1) {
+ return $latestVersion;
+ }
+
+ return false;
+ }
+
+ /**
+ * Checks the PHP version to ensure Shaarli can run
+ *
+ * @param string $minVersion minimum PHP required version
+ * @param string $curVersion current PHP version (use PHP_VERSION)
+ *
+ * @return bool true on success
+ *
+ * @throws Exception the PHP version is not supported
+ */
+ public static function checkPHPVersion($minVersion, $curVersion)
+ {
+ if (version_compare($curVersion, $minVersion) < 0) {
+ $msg = t(
+ 'Your PHP version is obsolete!'
+ . ' Shaarli requires at least PHP %s, and thus cannot run.'
+ . ' Your PHP version has known security vulnerabilities and should be'
+ . ' updated as soon as possible.'
+ );
+ throw new Exception(sprintf($msg, $minVersion));
+ }
+ return true;
+ }
+
+ /**
+ * Checks Shaarli has the proper access permissions to its resources
+ *
+ * @param ConfigManager $conf Configuration Manager instance.
+ * @param bool $minimalMode In minimal mode we only check permissions to be able to display a template.
+ * Currently we only need to be able to read the theme and write in raintpl cache.
+ *
+ * @return array A list of the detected configuration issues
+ */
+ public static function checkResourcePermissions(ConfigManager $conf, bool $minimalMode = false): array
+ {
+ $errors = [];
+ $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
+
+ // Check script and template directories are readable
+ foreach (
+ [
+ 'application',
+ 'inc',
+ 'plugins',
+ $rainTplDir,
+ $rainTplDir . '/' . $conf->get('resource.theme'),
+ ] as $path
+ ) {
+ if (!is_readable(realpath($path))) {
+ $errors[] = '"' . $path . '" ' . t('directory is not readable');
+ }
+ }
+
+ // Check cache and data directories are readable and writable
+ if ($minimalMode) {
+ $folders = [
+ $conf->get('resource.raintpl_tmp'),
+ ];
+ } else {
+ $folders = [
+ $conf->get('resource.thumbnails_cache'),
+ $conf->get('resource.data_dir'),
+ $conf->get('resource.page_cache'),
+ $conf->get('resource.raintpl_tmp'),
+ ];
+ }
+
+ foreach ($folders as $path) {
+ if (!is_readable(realpath($path))) {
+ $errors[] = '"' . $path . '" ' . t('directory is not readable');
+ }
+ if (!is_writable(realpath($path))) {
+ $errors[] = '"' . $path . '" ' . t('directory is not writable');
+ }
+ }
+
+ if ($minimalMode) {
+ return $errors;
+ }
+
+ // Check configuration files are readable and writable
+ foreach (
+ [
+ $conf->getConfigFileExt(),
+ $conf->get('resource.datastore'),
+ $conf->get('resource.ban_file'),
+ $conf->get('resource.log'),
+ $conf->get('resource.update_check'),
+ ] as $path
+ ) {
+ if (!is_string($path) || !is_file(realpath($path))) {
+ # the file may not exist yet
+ continue;
+ }
+
+ if (!is_readable(realpath($path))) {
+ $errors[] = '"' . $path . '" ' . t('file is not readable');
+ }
+ if (!is_writable(realpath($path))) {
+ $errors[] = '"' . $path . '" ' . t('file is not writable');
+ }
+ }
+
+ 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.
+ *
+ * Useful for assets browser cache.
+ *
+ * @param string $currentVersion of Shaarli
+ * @param string $salt User personal salt, also used for the authentication
+ *
+ * @return string version hash
+ */
+ public static function getVersionHash($currentVersion, $salt)
+ {
+ return hash_hmac('sha256', $currentVersion, $salt);
+ }
+
+ /**
+ * Get a list of PHP extensions used by Shaarli.
+ *
+ * @return array[] List of extension with following keys:
+ * - name: extension name
+ * - required: whether the extension is required to use Shaarli
+ * - desc: short description of extension usage in Shaarli
+ * - loaded: whether the extension is properly loaded or not
+ */
+ public static function getPhpExtensionsRequirement(): array
+ {
+ $extensions = [
+ ['name' => 'json', 'required' => true, 'desc' => t('Configuration parsing')],
+ ['name' => 'simplexml', 'required' => true, 'desc' => t('Slim Framework (routing, etc.)')],
+ ['name' => 'mbstring', 'required' => true, 'desc' => t('Multibyte (Unicode) string support')],
+ ['name' => 'gd', 'required' => false, 'desc' => t('Required to use thumbnails')],
+ ['name' => 'intl', 'required' => false, 'desc' => t('Localized text sorting (e.g. e->è->f)')],
+ ['name' => 'curl', 'required' => false, 'desc' => t('Better retrieval of bookmark metadata and thumbnail')],
+ ['name' => 'gettext', 'required' => false, 'desc' => t('Use the translation system in gettext mode')],
+ ['name' => 'ldap', 'required' => false, 'desc' => t('Login using LDAP server')],
+ ];
+
+ foreach ($extensions as &$extension) {
+ $extension['loaded'] = extension_loaded($extension['name']);
+ }
+
+ return $extensions;
+ }
+
+ /**
+ * Return the EOL date of given PHP version. If the version is unknown,
+ * we return today + 2 years.
+ *
+ * @param string $fullVersion PHP version, e.g. 7.4.7
+ *
+ * @return string Date format: YYYY-MM-DD
+ */
+ public static function getPhpEol(string $fullVersion): string
+ {
+ preg_match('/(\d+\.\d+)\.\d+/', $fullVersion, $matches);
+
+ return [
+ '7.1' => '2019-12-01',
+ '7.2' => '2020-11-30',
+ '7.3' => '2021-12-06',
+ '7.4' => '2022-11-28',
+ '8.0' => '2023-12-01',
+ ][$matches[1]] ?? (new \DateTime('+2 year'))->format('Y-m-d');
+ }
+}
diff --git a/application/helper/DailyPageHelper.php b/application/helper/DailyPageHelper.php
new file mode 100644
index 00000000..cb4494a8
--- /dev/null
+++ b/application/helper/DailyPageHelper.php
@@ -0,0 +1,241 @@
+getQueryParam(static::MONTH) !== null) {
+ return static::MONTH;
+ } elseif ($request->getQueryParam(static::WEEK) !== null) {
+ return static::WEEK;
+ }
+
+ return static::DAY;
+ }
+
+ /**
+ * Extracts a DateTimeImmutable from provided HTTP request.
+ * If no parameter is provided, we rely on the creation date of the latest provided created bookmark.
+ * If the datastore is empty or no bookmark is provided, we use the current date.
+ *
+ * @param string $type month/week/day
+ * @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.
+ *
+ * @throws Exception Type not supported.
+ */
+ public static function extractRequestedDateTime(
+ string $type,
+ ?string $requestedDate,
+ Bookmark $latestBookmark = null
+ ): DateTimeImmutable {
+ $format = static::getFormatByType($type);
+ if (empty($requestedDate)) {
+ return $latestBookmark instanceof Bookmark
+ ? new DateTimeImmutable($latestBookmark->getCreated()->format(\DateTime::ATOM))
+ : new DateTimeImmutable()
+ ;
+ }
+
+ // Don't use today's day of month (github issue #1844)
+ if ($type === static::MONTH) {
+ $format = '!' . $format;
+ }
+
+ // W is not supported by createFromFormat...
+ if ($type === static::WEEK) {
+ return (new DateTimeImmutable())
+ ->setISODate((int) substr($requestedDate, 0, 4), (int) substr($requestedDate, 4, 2))
+ ;
+ }
+
+ return DateTimeImmutable::createFromFormat($format, $requestedDate);
+ }
+
+ /**
+ * Get the DateTime format used by provided type
+ * Examples:
+ * - day: 20201016 ()
+ * - week: 202041 ()
+ * - month: 202010 ()
+ *
+ * @param string $type month/week/day
+ *
+ * @return string DateTime compatible format
+ *
+ * @see https://www.php.net/manual/en/datetime.format.php
+ *
+ * @throws Exception Type not supported.
+ */
+ public static function getFormatByType(string $type): string
+ {
+ switch ($type) {
+ case static::MONTH:
+ return 'Ym';
+ case static::WEEK:
+ return 'YW';
+ case static::DAY:
+ return 'Ymd';
+ default:
+ throw new Exception('Unsupported daily format type');
+ }
+ }
+
+ /**
+ * Get the first DateTime of the time period depending on given datetime and type.
+ * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
+ * and we don't want to alter original datetime.
+ *
+ * @param string $type month/week/day
+ * @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.
+ */
+ public static function getStartDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
+ {
+ switch ($type) {
+ case static::MONTH:
+ return $requested->modify('first day of this month midnight');
+ case static::WEEK:
+ return $requested->modify('Monday this week midnight');
+ case static::DAY:
+ return $requested->modify('Today midnight');
+ default:
+ throw new Exception('Unsupported daily format type');
+ }
+ }
+
+ /**
+ * Get the last DateTime of the time period depending on given datetime and type.
+ * Note: DateTimeImmutable is required because we rely heavily on DateTime->modify() syntax
+ * and we don't want to alter original datetime.
+ *
+ * @param string $type month/week/day
+ * @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.
+ */
+ public static function getEndDateTimeByType(string $type, DateTimeImmutable $requested): \DateTimeInterface
+ {
+ switch ($type) {
+ case static::MONTH:
+ return $requested->modify('last day of this month 23:59:59');
+ case static::WEEK:
+ return $requested->modify('Sunday this week 23:59:59');
+ case static::DAY:
+ return $requested->modify('Today 23:59:59');
+ default:
+ throw new Exception('Unsupported daily format type');
+ }
+ }
+
+ /**
+ * 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 bool $includeRelative Include relative date description (today, yesterday, etc.)
+ *
+ * @return string Localized time period description
+ *
+ * @throws Exception Type not supported.
+ */
+ public static function getDescriptionByType(
+ string $type,
+ \DateTimeImmutable $requested,
+ bool $includeRelative = true
+ ): string {
+ switch ($type) {
+ case static::MONTH:
+ return $requested->format('F') . ', ' . $requested->format('Y');
+ case static::WEEK:
+ $requested = $requested->modify('Monday this week');
+ return t('Week') . ' ' . $requested->format('W') . ' (' . format_date($requested, false) . ')';
+ case static::DAY:
+ $out = '';
+ if ($includeRelative && $requested->format('Ymd') === date('Ymd')) {
+ $out = t('Today') . ' - ';
+ } 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');
+ }
+ }
+
+ /**
+ * Get the number of items to display in the RSS feed depending on the given type.
+ *
+ * @param string $type month/week/day
+ *
+ * @return int number of elements
+ *
+ * @throws Exception Type not supported.
+ */
+ public static function getRssLengthByType(string $type): int
+ {
+ switch ($type) {
+ case static::MONTH:
+ return 12; // 1 year
+ case static::WEEK:
+ return 26; // ~6 months
+ case static::DAY:
+ return 30; // ~1 month
+ default:
+ 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/helper/FileUtils.php b/application/helper/FileUtils.php
new file mode 100644
index 00000000..e8a2168c
--- /dev/null
+++ b/application/helper/FileUtils.php
@@ -0,0 +1,140 @@
+';
+
+ /**
+ * Write data into a file (Shaarli database format).
+ * The data is stored in a PHP file, as a comment, in compressed base64 format.
+ *
+ * The file will be created if it doesn't exist.
+ *
+ * @param string $file File path.
+ * @param mixed $content Content to write.
+ *
+ * @return int|bool Number of bytes written or false if it fails.
+ *
+ * @throws IOException The destination file can't be written.
+ */
+ public static function writeFlatDB($file, $content)
+ {
+ if (is_file($file) && !is_writeable($file)) {
+ // The datastore exists but is not writeable
+ throw new IOException($file);
+ } elseif (!is_file($file) && !is_writeable(dirname($file))) {
+ // The datastore does not exist and its parent directory is not writeable
+ throw new IOException(dirname($file));
+ }
+
+ return file_put_contents(
+ $file,
+ self::$phpPrefix . base64_encode(gzdeflate(serialize($content))) . self::$phpSuffix
+ );
+ }
+
+ /**
+ * Read data from a file containing Shaarli database format content.
+ *
+ * If the file isn't readable or doesn't exist, default data will be returned.
+ *
+ * @param string $file File path.
+ * @param mixed $default The default value to return if the file isn't readable.
+ *
+ * @return mixed The content unserialized, or default if the file isn't readable, or false if it fails.
+ */
+ public static function readFlatDB($file, $default = null)
+ {
+ // Note that gzinflate is faster than gzuncompress.
+ // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
+ if (!is_readable($file)) {
+ return $default;
+ }
+
+ $data = file_get_contents($file);
+ if ($data == '') {
+ return $default;
+ }
+
+ return unserialize(
+ gzinflate(
+ base64_decode(
+ substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
+ )
+ )
+ );
+ }
+
+ /**
+ * Recursively deletes a folder content, and deletes itself optionally.
+ * If an excluded file is found, folders won't be deleted.
+ *
+ * Additional security: raise an exception if it tries to delete a folder outside of Shaarli directory.
+ *
+ * @param string $path
+ * @param bool $selfDelete Delete the provided folder if true, only its content if false.
+ * @param array $exclude
+ */
+ public static function clearFolder(string $path, bool $selfDelete, array $exclude = []): bool
+ {
+ $skipped = false;
+
+ if (!is_dir($path)) {
+ throw new IOException(t('Provided path is not a directory.'));
+ }
+
+ if (!static::isPathInShaarliFolder($path)) {
+ throw new IOException(t('Trying to delete a folder outside of Shaarli path.'));
+ }
+
+ foreach (new \DirectoryIterator($path) as $file) {
+ if ($file->isDot()) {
+ continue;
+ }
+
+ if (in_array($file->getBasename(), $exclude, true)) {
+ $skipped = true;
+ continue;
+ }
+
+ if ($file->isFile()) {
+ unlink($file->getPathname());
+ } elseif ($file->isDir()) {
+ $skipped = static::clearFolder($file->getRealPath(), true, $exclude) || $skipped;
+ }
+ }
+
+ if ($selfDelete && !$skipped) {
+ rmdir($path);
+ }
+
+ return $skipped;
+ }
+
+ /**
+ * Checks that the given path is inside Shaarli directory.
+ */
+ public static function isPathInShaarliFolder(string $path): bool
+ {
+ $rootDirectory = dirname(dirname(dirname(__FILE__)));
+
+ return strpos(realpath($path), $rootDirectory) !== false;
+ }
+}
diff --git a/application/http/HttpAccess.php b/application/http/HttpAccess.php
new file mode 100644
index 00000000..e80e0c01
--- /dev/null
+++ b/application/http/HttpAccess.php
@@ -0,0 +1,49 @@
+idnToAscii();
if (!filter_var($cleanUrl, FILTER_VALIDATE_URL) || !$urlObj->isHttp()) {
- return array(array(0 => 'Invalid HTTP UrlUtils'), false);
+ return [[0 => 'Invalid HTTP UrlUtils'], false];
}
$userAgent =
@@ -64,42 +71,39 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF
$ch = curl_init($cleanUrl);
if ($ch === false) {
- return array(array(0 => 'curl_init() error'), false);
+ return [[0 => 'curl_init() error'], false];
}
// General cURL settings
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
- curl_setopt($ch, CURLOPT_HEADER, true);
+ // Default header download if the $curlHeaderFunction is not defined
+ curl_setopt($ch, CURLOPT_HEADER, !is_callable($curlHeaderFunction));
curl_setopt(
$ch,
CURLOPT_HTTPHEADER,
- array('Accept-Language: ' . $acceptLanguage)
+ ['Accept-Language: ' . $acceptLanguage]
);
curl_setopt($ch, CURLOPT_MAXREDIRS, $maxRedirs);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
+ // Max download size management
+ curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024 * 16);
+ curl_setopt($ch, CURLOPT_NOPROGRESS, false);
+ if (is_callable($curlHeaderFunction)) {
+ curl_setopt($ch, CURLOPT_HEADERFUNCTION, $curlHeaderFunction);
+ }
if (is_callable($curlWriteFunction)) {
curl_setopt($ch, CURLOPT_WRITEFUNCTION, $curlWriteFunction);
}
-
- // Max download size management
- curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16);
- curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt(
$ch,
CURLOPT_PROGRESSFUNCTION,
- function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
- if (version_compare(phpversion(), '5.5', '<')) {
- // PHP version lower than 5.5
- // Callback has 4 arguments
- $downloaded = $arg1;
- } else {
- // Callback has 5 arguments
- $downloaded = $arg2;
- }
+ function ($arg0, $arg1, $arg2, $arg3, $arg4) use ($maxBytes) {
+ $downloaded = $arg2;
+
// Non-zero return stops downloading
return ($downloaded > $maxBytes) ? 1 : 0;
}
@@ -118,9 +122,9 @@ function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
* Removing this would require updating
* GetHttpUrlTest::testGetInvalidRemoteUrl()
*/
- return array(false, false);
+ return [false, false];
}
- return array(array(0 => 'curl_exec() error: ' . $errorStr), false);
+ return [[0 => 'curl_exec() error: ' . $errorStr], false];
}
// Formatting output like the fallback method
@@ -131,7 +135,7 @@ function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
$rawHeadersLastRedir = end($rawHeadersArrayRedirs);
$content = substr($response, $headSize);
- $headers = array();
+ $headers = [];
foreach (preg_split('~[\r\n]+~', $rawHeadersLastRedir) as $line) {
if (empty($line) || ctype_space($line)) {
continue;
@@ -142,7 +146,7 @@ function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
$value = $splitLine[1];
if (array_key_exists($key, $headers)) {
if (!is_array($headers[$key])) {
- $headers[$key] = array(0 => $headers[$key]);
+ $headers[$key] = [0 => $headers[$key]];
}
$headers[$key][] = $value;
} else {
@@ -153,7 +157,7 @@ function ($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) {
}
}
- return array($headers, $content);
+ return [$headers, $content];
}
/**
@@ -184,15 +188,15 @@ function get_http_response_fallback(
$acceptLanguage,
$maxRedr
) {
- $options = array(
- 'http' => array(
+ $options = [
+ 'http' => [
'method' => 'GET',
'timeout' => $timeout,
'user_agent' => $userAgent,
'header' => "Accept: */*\r\n"
. 'Accept-Language: ' . $acceptLanguage
- )
- );
+ ]
+ ];
stream_context_set_default($options);
list($headers, $finalUrl) = get_redirected_headers($cleanUrl, $maxRedr);
@@ -203,7 +207,7 @@ function get_http_response_fallback(
}
if (! $headers) {
- return array($headers, false);
+ return [$headers, false];
}
try {
@@ -211,10 +215,10 @@ function get_http_response_fallback(
$context = stream_context_create($options);
$content = file_get_contents($finalUrl, false, $context, -1, $maxBytes);
} catch (Exception $exc) {
- return array(array(0 => 'HTTP Error'), $exc->getMessage());
+ return [[0 => 'HTTP Error'], $exc->getMessage()];
}
- return array($headers, $content);
+ return [$headers, $content];
}
/**
@@ -233,10 +237,12 @@ function get_redirected_headers($url, $redirectionLimit = 3)
}
// Headers found, redirection found, and limit not reached.
- if ($redirectionLimit-- > 0
+ if (
+ $redirectionLimit-- > 0
&& !empty($headers)
&& (strpos($headers[0], '301') !== false || strpos($headers[0], '302') !== false)
- && !empty($headers['Location'])) {
+ && !empty($headers['Location'])
+ ) {
$redirection = is_array($headers['Location']) ? end($headers['Location']) : $headers['Location'];
if ($redirection != $url) {
$redirection = getAbsoluteUrl($url, $redirection);
@@ -244,7 +250,7 @@ function get_redirected_headers($url, $redirectionLimit = 3)
}
}
- return array($headers, $url);
+ return [$headers, $url];
}
/**
@@ -266,7 +272,7 @@ function getAbsoluteUrl($originalUrl, $newUrl)
}
$parts = parse_url($originalUrl);
- $final = $parts['scheme'] .'://'. $parts['host'];
+ $final = $parts['scheme'] . '://' . $parts['host'];
$final .= (!empty($parts['port'])) ? $parts['port'] : '';
$final .= '/';
if ($newUrl[0] != '/') {
@@ -319,7 +325,8 @@ function server_url($server)
$scheme = 'https';
}
- if (($scheme == 'http' && $port != '80')
+ if (
+ ($scheme == 'http' && $port != '80')
|| ($scheme == 'https' && $port != '443')
) {
$port = ':' . $port;
@@ -340,22 +347,26 @@ function server_url($server)
$host = $server['SERVER_NAME'];
}
- return $scheme.'://'.$host.$port;
+ return $scheme . '://' . $host . $port;
}
// SSL detection
- if ((! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on')
- || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443')) {
+ if (
+ (! empty($server['HTTPS']) && strtolower($server['HTTPS']) == 'on')
+ || (isset($server['SERVER_PORT']) && $server['SERVER_PORT'] == '443')
+ ) {
$scheme = 'https';
}
// Do not append standard port values
- if (($scheme == 'http' && $server['SERVER_PORT'] != '80')
- || ($scheme == 'https' && $server['SERVER_PORT'] != '443')) {
- $port = ':'.$server['SERVER_PORT'];
+ if (
+ ($scheme == 'http' && $server['SERVER_PORT'] != '80')
+ || ($scheme == 'https' && $server['SERVER_PORT'] != '443')
+ ) {
+ $port = ':' . $server['SERVER_PORT'];
}
- return $scheme.'://'.$server['SERVER_NAME'].$port;
+ return $scheme . '://' . $server['SERVER_NAME'] . $port;
}
/**
@@ -369,7 +380,11 @@ function server_url($server)
*/
function index_url($server)
{
- $scriptname = $server['SCRIPT_NAME'];
+ if (defined('SHAARLI_ROOT_URL') && null !== SHAARLI_ROOT_URL) {
+ return rtrim(SHAARLI_ROOT_URL, '/') . '/';
+ }
+
+ $scriptname = !empty($server['SCRIPT_NAME']) ? $server['SCRIPT_NAME'] : '/';
if (endsWith($scriptname, 'index.php')) {
$scriptname = substr($scriptname, 0, -9);
}
@@ -377,7 +392,7 @@ function index_url($server)
}
/**
- * Returns the absolute URL of the current script, with the query
+ * Returns the absolute URL of the current script, with current route and query
*
* If the resource is "index.php", then it is removed (for better-looking URLs)
*
@@ -387,10 +402,17 @@ function index_url($server)
*/
function page_url($server)
{
- if (! empty($server['QUERY_STRING'])) {
- return index_url($server).'?'.$server['QUERY_STRING'];
+ $scriptname = $server['SCRIPT_NAME'] ?? '';
+ if (endsWith($scriptname, 'index.php')) {
+ $scriptname = substr($scriptname, 0, -9);
}
- return index_url($server);
+
+ $route = preg_replace('@^' . $scriptname . '@', '', $server['REQUEST_URI'] ?? '');
+ if (! empty($server['QUERY_STRING'])) {
+ return index_url($server) . $route . '?' . $server['QUERY_STRING'];
+ }
+
+ return index_url($server) . $route;
}
/**
@@ -477,3 +499,138 @@ function is_https($server)
return ! empty($server['HTTPS']);
}
+
+/**
+ * Get cURL callback function for CURLOPT_WRITEFUNCTION
+ *
+ * @param string $charset to extract from the downloaded page (reference)
+ * @param string $curlGetInfo Optionally overrides curl_getinfo function
+ *
+ * @return Closure
+ */
+function get_curl_header_callback(
+ &$charset,
+ $curlGetInfo = 'curl_getinfo'
+) {
+ $isRedirected = false;
+
+ return function ($ch, $data) use ($curlGetInfo, &$charset, &$isRedirected) {
+ $responseCode = $curlGetInfo($ch, CURLINFO_RESPONSE_CODE);
+ $chunkLength = strlen($data);
+ if (!empty($responseCode) && in_array($responseCode, [301, 302])) {
+ $isRedirected = true;
+ return $chunkLength;
+ }
+ if (!empty($responseCode) && $responseCode !== 200) {
+ return false;
+ }
+ // After a redirection, the content type will keep the previous request value
+ // until it finds the next content-type header.
+ if (! $isRedirected || strpos(strtolower($data), 'content-type') !== false) {
+ $contentType = $curlGetInfo($ch, CURLINFO_CONTENT_TYPE);
+ }
+ if (!empty($contentType) && strpos($contentType, 'text/html') === false) {
+ return false;
+ }
+ if (!empty($contentType) && empty($charset)) {
+ $charset = header_extract_charset($contentType);
+ }
+
+ return $chunkLength;
+ };
+}
+
+/**
+ * Get cURL callback function for CURLOPT_WRITEFUNCTION
+ *
+ * @param string $charset to extract from the downloaded page (reference)
+ * @param string $title to extract from the downloaded page (reference)
+ * @param string $description to extract from the downloaded page (reference)
+ * @param string $keywords to extract from the downloaded page (reference)
+ * @param bool $retrieveDescription Automatically tries to retrieve description and keywords from HTML content
+ * @param string $curlGetInfo Optionally overrides curl_getinfo function
+ *
+ * @return Closure
+ */
+function get_curl_download_callback(
+ &$charset,
+ &$title,
+ &$description,
+ &$keywords,
+ $retrieveDescription,
+ $tagsSeparator
+) {
+ $currentChunk = 0;
+ $foundChunk = null;
+
+ /**
+ * cURL callback function for CURLOPT_WRITEFUNCTION (called during the download).
+ *
+ * While downloading the remote page, we check that the HTTP code is 200 and content type is 'html/text'
+ * Then we extract the title and the charset and stop the download when it's done.
+ *
+ * @param resource $ch cURL resource
+ * @param string $data chunk of data being downloaded
+ *
+ * @return int|bool length of $data or false if we need to stop the download
+ */
+ return function (
+ $ch,
+ $data
+ ) use (
+ $retrieveDescription,
+ $tagsSeparator,
+ &$charset,
+ &$title,
+ &$description,
+ &$keywords,
+ &$currentChunk,
+ &$foundChunk
+ ) {
+ $chunkLength = strlen($data);
+ $currentChunk++;
+
+ if (empty($charset)) {
+ $charset = html_extract_charset($data);
+ }
+ if (empty($title)) {
+ $title = html_extract_title($data);
+ $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
+ }
+ if (empty($title)) {
+ $title = html_extract_tag('title', $data);
+ $foundChunk = ! empty($title) ? $currentChunk : $foundChunk;
+ }
+ if ($retrieveDescription && empty($description)) {
+ $description = html_extract_tag('description', $data);
+ $foundChunk = ! empty($description) ? $currentChunk : $foundChunk;
+ }
+ if ($retrieveDescription && empty($keywords)) {
+ $keywords = html_extract_tag('keywords', $data);
+ if (! empty($keywords)) {
+ $foundChunk = $currentChunk;
+ // Keywords use the format tag1, tag2 multiple words, tag
+ // So we split the result with `,`, then if a tag contains the separator we replace it by `-`.
+ $keywords = tags_array2str(array_map(function (string $keyword) use ($tagsSeparator): string {
+ return tags_array2str(tags_str2array($keyword, $tagsSeparator), '-');
+ }, tags_str2array($keywords, ',')), $tagsSeparator);
+ }
+ }
+
+ // We got everything we want, stop the download.
+ // If we already found either the title, description or keywords,
+ // it's highly unlikely that we'll found the other metas further than
+ // in the same chunk of data or the next one. So we also stop the download after that.
+ if (
+ (!empty($responseCode) && !empty($contentType) && !empty($charset)) && $foundChunk !== null
+ && (! $retrieveDescription
+ || $foundChunk < $currentChunk
+ || (!empty($title) && !empty($description) && !empty($keywords))
+ )
+ ) {
+ return false;
+ }
+
+ return $chunkLength;
+ };
+}
diff --git a/application/http/MetadataRetriever.php b/application/http/MetadataRetriever.php
new file mode 100644
index 00000000..cfc72583
--- /dev/null
+++ b/application/http/MetadataRetriever.php
@@ -0,0 +1,74 @@
+conf = $conf;
+ $this->httpAccess = $httpAccess;
+ }
+
+ /**
+ * Retrieve metadata for given URL.
+ *
+ * @return array [
+ * 'title' => ,
+ * 'description' => ,
+ * 'tags' => ,
+ * ]
+ */
+ public function retrieve(string $url): array
+ {
+ $charset = null;
+ $title = null;
+ $description = null;
+ $tags = null;
+
+ // Short timeout to keep the application responsive
+ // The callback will fill $charset and $title with data from the downloaded page.
+ $this->httpAccess->getHttpResponse(
+ $url,
+ $this->conf->get('general.download_timeout', 30),
+ $this->conf->get('general.download_max_size', 4194304),
+ $this->httpAccess->getCurlHeaderCallback($charset),
+ $this->httpAccess->getCurlDownloadCallback(
+ $charset,
+ $title,
+ $description,
+ $tags,
+ $this->conf->get('general.retrieve_description'),
+ $this->conf->get('general.tags_separator', ' ')
+ )
+ );
+
+ if (!empty($title) && strtolower($charset) !== 'utf-8') {
+ $title = mb_convert_encoding($title, 'utf-8', $charset);
+ }
+
+ 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/http/Url.php b/application/http/Url.php
index 90444a2f..129957bf 100644
--- a/application/http/Url.php
+++ b/application/http/Url.php
@@ -17,7 +17,7 @@
*/
class Url
{
- private static $annoyingQueryParams = array(
+ private static $annoyingQueryParams = [
// Facebook
'action_object_map=',
'action_ref_map=',
@@ -37,15 +37,15 @@ class Url
// Other
'campaign_'
- );
+ ];
- private static $annoyingFragments = array(
+ private static $annoyingFragments = [
// ATInternet
'xtor=RSS-',
// Misc.
'tk.rss_all'
- );
+ ];
/*
* URL parts represented as an array
@@ -61,6 +61,7 @@ class Url
*/
public function __construct($url)
{
+ $url = $url ?? '';
$url = self::cleanupUnparsedUrl(trim($url));
$this->parts = parse_url($url);
@@ -120,7 +121,7 @@ protected function cleanupQuery()
foreach (self::$annoyingQueryParams as $annoying) {
foreach ($queryParams as $param) {
if (startsWith($param, $annoying)) {
- $queryParams = array_diff($queryParams, array($param));
+ $queryParams = array_diff($queryParams, [$param]);
continue;
}
}
diff --git a/application/http/UrlUtils.php b/application/http/UrlUtils.php
index 4bc84b82..de5b7db1 100644
--- a/application/http/UrlUtils.php
+++ b/application/http/UrlUtils.php
@@ -1,4 +1,5 @@
{$action}($request, $response);
+ }
+
+ /** Legacy route: ?post= */
+ public function post(Request $request, Response $response): Response
+ {
+ $route = '/admin/shaare';
+ $buildParameters = function (?array $parameters, bool $encode) {
+ if ($encode) {
+ $parameters = array_map('urlencode', $parameters);
+ }
+
+ return count($parameters) > 0 ? '?' . http_build_query($parameters) : '';
+ };
+
+
+ if (!$this->container->loginManager->isLoggedIn()) {
+ $parameters = $buildParameters($request->getQueryParams(), true);
+ return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route . $parameters);
+ }
+
+ $parameters = $buildParameters($request->getQueryParams(), false);
+
+ return $this->redirect($response, $route . $parameters);
+ }
+
+ /** Legacy route: ?addlink= */
+ protected function addlink(Request $request, Response $response): Response
+ {
+ $route = '/admin/add-shaare';
+
+ if (!$this->container->loginManager->isLoggedIn()) {
+ return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route);
+ }
+
+ return $this->redirect($response, $route);
+ }
+
+ /** Legacy route: ?do=login */
+ protected function login(Request $request, Response $response): Response
+ {
+ $returnUrl = $request->getQueryParam('returnurl');
+
+ return $this->redirect($response, '/login' . ($returnUrl ? '?returnurl=' . $returnUrl : ''));
+ }
+
+ /** Legacy route: ?do=logout */
+ protected function logout(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/admin/logout');
+ }
+
+ /** Legacy route: ?do=picwall */
+ protected function picwall(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/picture-wall');
+ }
+
+ /** Legacy route: ?do=tagcloud */
+ protected function tagcloud(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/tags/cloud');
+ }
+
+ /** Legacy route: ?do=taglist */
+ protected function taglist(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/tags/list');
+ }
+
+ /** Legacy route: ?do=daily */
+ protected function daily(Request $request, Response $response): Response
+ {
+ $dayParam = !empty($request->getParam('day')) ? '?day=' . escape($request->getParam('day')) : '';
+
+ return $this->redirect($response, '/daily' . $dayParam);
+ }
+
+ /** Legacy route: ?do=rss */
+ protected function rss(Request $request, Response $response): Response
+ {
+ return $this->feed($request, $response, FeedBuilder::$FEED_RSS);
+ }
+
+ /** Legacy route: ?do=atom */
+ protected function atom(Request $request, Response $response): Response
+ {
+ return $this->feed($request, $response, FeedBuilder::$FEED_ATOM);
+ }
+
+ /** Legacy route: ?do=opensearch */
+ protected function opensearch(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/open-search');
+ }
+
+ /** Legacy route: ?do=dailyrss */
+ protected function dailyrss(Request $request, Response $response): Response
+ {
+ return $this->redirect($response, '/daily-rss');
+ }
+
+ /** Legacy route: ?do=feed */
+ protected function feed(Request $request, Response $response, string $feedType): Response
+ {
+ $parameters = count($request->getQueryParams()) > 0 ? '?' . http_build_query($request->getQueryParams()) : '';
+
+ return $this->redirect($response, '/feed/' . $feedType . $parameters);
+ }
+
+ /** Legacy route: ?do=configure */
+ protected function configure(Request $request, Response $response): Response
+ {
+ $route = '/admin/configure';
+
+ if (!$this->container->loginManager->isLoggedIn()) {
+ return $this->redirect($response, '/login?returnurl=' . $this->getBasePath() . $route);
+ }
+
+ return $this->redirect($response, $route);
+ }
+
+ protected function getBasePath(): string
+ {
+ return $this->container->basePath ?: '';
+ }
+}
diff --git a/application/legacy/LegacyLinkDB.php b/application/legacy/LegacyLinkDB.php
new file mode 100644
index 00000000..cb19eda5
--- /dev/null
+++ b/application/legacy/LegacyLinkDB.php
@@ -0,0 +1,585 @@
+link offset)
+ private $urls;
+
+ /**
+ * @var array List of all bookmarks IDS mapped with their array offset.
+ * Map: id->offset.
+ */
+ protected $ids;
+
+ // List of offset keys (for the Iterator interface implementation)
+ private $keys;
+
+ // Position in the $this->keys array (for the Iterator interface)
+ private $position;
+
+ // Is the user logged in? (used to filter private bookmarks)
+ private $loggedIn;
+
+ // Hide public bookmarks
+ private $hidePublicLinks;
+
+ /**
+ * Creates a new LinkDB
+ *
+ * Checks if the datastore exists; else, attempts to create a dummy one.
+ *
+ * @param string $datastore datastore file path.
+ * @param boolean $isLoggedIn is the user logged in?
+ * @param boolean $hidePublicLinks if true all bookmarks are private.
+ */
+ public function __construct(
+ $datastore,
+ $isLoggedIn,
+ $hidePublicLinks
+ ) {
+
+ $this->datastore = $datastore;
+ $this->loggedIn = $isLoggedIn;
+ $this->hidePublicLinks = $hidePublicLinks;
+ $this->check();
+ $this->read();
+ }
+
+ /**
+ * Countable - Counts elements of an object
+ */
+ public function count(): int
+ {
+ return count($this->links);
+ }
+
+ /**
+ * ArrayAccess - Assigns a value to the specified offset
+ */
+ public function offsetSet($offset, $value): void
+ {
+ // TODO: use exceptions instead of "die"
+ if (!$this->loggedIn) {
+ die(t('You are not authorized to add a link.'));
+ }
+ if (!isset($value['id']) || empty($value['url'])) {
+ die(t('Internal Error: A link should always have an id and URL.'));
+ }
+ if (($offset !== null && !is_int($offset)) || !is_int($value['id'])) {
+ die(t('You must specify an integer as a key.'));
+ }
+ if ($offset !== null && $offset !== $value['id']) {
+ die(t('Array offset and link ID must be equal.'));
+ }
+
+ // If the link exists, we reuse the real offset, otherwise new entry
+ $existing = $this->getLinkOffset($offset);
+ if ($existing !== null) {
+ $offset = $existing;
+ } else {
+ $offset = count($this->links);
+ }
+ $this->links[$offset] = $value;
+ $this->urls[$value['url']] = $offset;
+ $this->ids[$value['id']] = $offset;
+ }
+
+ /**
+ * ArrayAccess - Whether or not an offset exists
+ */
+ public function offsetExists($offset): bool
+ {
+ return array_key_exists($this->getLinkOffset($offset), $this->links);
+ }
+
+ /**
+ * ArrayAccess - Unsets an offset
+ */
+ public function offsetUnset($offset): void
+ {
+ if (!$this->loggedIn) {
+ // TODO: raise an exception
+ die('You are not authorized to delete a link.');
+ }
+ $realOffset = $this->getLinkOffset($offset);
+ $url = $this->links[$realOffset]['url'];
+ unset($this->urls[$url]);
+ unset($this->ids[$realOffset]);
+ unset($this->links[$realOffset]);
+ }
+
+ /**
+ * ArrayAccess - Returns the value at specified offset
+ */
+ public function offsetGet($offset): ?array
+ {
+ $realOffset = $this->getLinkOffset($offset);
+ return isset($this->links[$realOffset]) ? $this->links[$realOffset] : null;
+ }
+
+ /**
+ * Iterator - Returns the current element
+ */
+ public function current(): array
+ {
+ return $this[$this->keys[$this->position]];
+ }
+
+ /**
+ * Iterator - Returns the key of the current element
+ *
+ * @return int|string
+ */
+ #[\ReturnTypeWillChange]
+ public function key()
+ {
+ return $this->keys[$this->position];
+ }
+
+ /**
+ * Iterator - Moves forward to next element
+ */
+ public function next(): void
+ {
+ ++$this->position;
+ }
+
+ /**
+ * Iterator - Rewinds the Iterator to the first element
+ *
+ * Entries are sorted by date (latest first)
+ */
+ public function rewind(): void
+ {
+ $this->keys = array_keys($this->ids);
+ $this->position = 0;
+ }
+
+ /**
+ * Iterator - Checks if current position is valid
+ */
+ public function valid(): bool
+ {
+ return isset($this->keys[$this->position]);
+ }
+
+ /**
+ * Checks if the DB directory and file exist
+ *
+ * If no DB file is found, creates a dummy DB.
+ */
+ private function check()
+ {
+ if (file_exists($this->datastore)) {
+ return;
+ }
+
+ // Create a dummy database for example
+ $this->links = [];
+ $link = [
+ 'id' => 1,
+ 'title' => t('The personal, minimalist, super-fast, database free, bookmarking service'),
+ 'url' => 'https://shaarli.readthedocs.io',
+ 'description' => t(
+ 'Welcome to Shaarli! This is your first public bookmark. '
+ . 'To edit or delete me, you must first login.
+
+To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
+
+You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'
+ ),
+ 'private' => 0,
+ 'created' => new DateTime(),
+ 'tags' => 'opensource software',
+ 'sticky' => false,
+ ];
+ $link['shorturl'] = link_small_hash($link['created'], $link['id']);
+ $this->links[1] = $link;
+
+ $link = [
+ 'id' => 0,
+ 'title' => t('My secret stuff... - Pastebin.com'),
+ 'url' => 'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
+ 'description' => t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
+ 'private' => 1,
+ 'created' => new DateTime('1 minute ago'),
+ 'tags' => 'secretstuff',
+ 'sticky' => false,
+ ];
+ $link['shorturl'] = link_small_hash($link['created'], $link['id']);
+ $this->links[0] = $link;
+
+ // Write database to disk
+ $this->write();
+ }
+
+ /**
+ * Reads database from disk to memory
+ */
+ private function read()
+ {
+ // Public bookmarks are hidden and user not logged in => nothing to show
+ if ($this->hidePublicLinks && !$this->loggedIn) {
+ $this->links = [];
+ return;
+ }
+
+ $this->urls = [];
+ $this->ids = [];
+ $this->links = FileUtils::readFlatDB($this->datastore, []);
+
+ $toremove = [];
+ foreach ($this->links as $key => &$link) {
+ if (!$this->loggedIn && $link['private'] != 0) {
+ // Transition for not upgraded databases.
+ unset($this->links[$key]);
+ continue;
+ }
+
+ // Sanitize data fields.
+ sanitizeLink($link);
+
+ // Remove private tags if the user is not logged in.
+ if (!$this->loggedIn) {
+ $link['tags'] = preg_replace('/(^|\s+)\.[^($|\s)]+\s*/', ' ', $link['tags']);
+ }
+
+ $link['real_url'] = $link['url'];
+
+ $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
+
+ // To be able to load bookmarks before running the update, and prepare the update
+ if (!isset($link['created'])) {
+ $link['id'] = $link['linkdate'];
+ $link['created'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['linkdate']);
+ if (!empty($link['updated'])) {
+ $link['updated'] = DateTime::createFromFormat(self::LINK_DATE_FORMAT, $link['updated']);
+ }
+ $link['shorturl'] = smallHash($link['linkdate']);
+ }
+
+ $this->urls[$link['url']] = $key;
+ $this->ids[$link['id']] = $key;
+ }
+ }
+
+ /**
+ * Saves the database from memory to disk
+ *
+ * @throws IOException the datastore is not writable
+ */
+ private function write()
+ {
+ $this->reorder();
+ FileUtils::writeFlatDB($this->datastore, $this->links);
+ }
+
+ /**
+ * Saves the database from memory to disk
+ *
+ * @param string $pageCacheDir page cache directory
+ */
+ public function save($pageCacheDir)
+ {
+ if (!$this->loggedIn) {
+ // TODO: raise an Exception instead
+ die('You are not authorized to change the database.');
+ }
+
+ $this->write();
+
+ $pageCacheManager = new PageCacheManager($pageCacheDir, $this->loggedIn);
+ $pageCacheManager->invalidateCaches();
+ }
+
+ /**
+ * Returns the link for a given URL, or False if it does not exist.
+ *
+ * @param string $url URL to search for
+ *
+ * @return mixed the existing link if it exists, else 'false'
+ */
+ public function getLinkFromUrl($url)
+ {
+ if (isset($this->urls[$url])) {
+ return $this->links[$this->urls[$url]];
+ }
+ return false;
+ }
+
+ /**
+ * Returns the shaare corresponding to a smallHash.
+ *
+ * @param string $request QUERY_STRING server parameter.
+ *
+ * @return array $filtered array containing permalink data.
+ *
+ * @throws BookmarkNotFoundException if the smallhash is malformed or doesn't match any link.
+ */
+ public function filterHash($request)
+ {
+ $request = substr($request, 0, 6);
+ $linkFilter = new LegacyLinkFilter($this->links);
+ return $linkFilter->filter(LegacyLinkFilter::$FILTER_HASH, $request);
+ }
+
+ /**
+ * Returns the list of articles for a given day.
+ *
+ * @param string $request day to filter. Format: YYYYMMDD.
+ *
+ * @return array list of shaare found.
+ */
+ public function filterDay($request)
+ {
+ $linkFilter = new LegacyLinkFilter($this->links);
+ return $linkFilter->filter(LegacyLinkFilter::$FILTER_DAY, $request);
+ }
+
+ /**
+ * Filter bookmarks according to search parameters.
+ *
+ * @param array $filterRequest Search request content. Supported keys:
+ * - searchtags: list of tags
+ * - searchterm: term search
+ * @param bool $casesensitive Optional: Perform case sensitive filter
+ * @param string $visibility return only all/private/public bookmarks
+ * @param bool $untaggedonly return only untagged bookmarks
+ *
+ * @return array filtered bookmarks, all bookmarks if no suitable filter was provided.
+ */
+ public function filterSearch(
+ $filterRequest = [],
+ $casesensitive = false,
+ $visibility = 'all',
+ $untaggedonly = false
+ ) {
+
+ // Filter link database according to parameters.
+ $searchtags = isset($filterRequest['searchtags']) ? escape($filterRequest['searchtags']) : '';
+ $searchterm = isset($filterRequest['searchterm']) ? escape($filterRequest['searchterm']) : '';
+
+ // Search tags + fullsearch - blank string parameter will return all bookmarks.
+ $type = LegacyLinkFilter::$FILTER_TAG | LegacyLinkFilter::$FILTER_TEXT; // == "vuotext"
+ $request = [$searchtags, $searchterm];
+
+ $linkFilter = new LegacyLinkFilter($this);
+ return $linkFilter->filter($type, $request, $casesensitive, $visibility, $untaggedonly);
+ }
+
+ /**
+ * Returns the list tags appearing in the bookmarks with the given tags
+ *
+ * @param array $filteringTags tags selecting the bookmarks to consider
+ * @param string $visibility process only all/private/public bookmarks
+ *
+ * @return array tag => linksCount
+ */
+ public function linksCountPerTag($filteringTags = [], $visibility = 'all')
+ {
+ $links = $this->filterSearch(['searchtags' => $filteringTags], false, $visibility);
+ $tags = [];
+ $caseMapping = [];
+ foreach ($links as $link) {
+ foreach (preg_split('/\s+/', $link['tags'], 0, PREG_SPLIT_NO_EMPTY) as $tag) {
+ if (empty($tag)) {
+ continue;
+ }
+ // The first case found will be displayed.
+ if (!isset($caseMapping[strtolower($tag)])) {
+ $caseMapping[strtolower($tag)] = $tag;
+ $tags[$caseMapping[strtolower($tag)]] = 0;
+ }
+ $tags[$caseMapping[strtolower($tag)]]++;
+ }
+ }
+
+ /*
+ * Formerly used arsort(), which doesn't define the sort behaviour for equal values.
+ * Also, this function doesn't produce the same result between PHP 5.6 and 7.
+ *
+ * So we now use array_multisort() to sort tags by DESC occurrences,
+ * then ASC alphabetically for equal values.
+ *
+ * @see https://github.com/shaarli/Shaarli/issues/1142
+ */
+ $keys = array_keys($tags);
+ $tmpTags = array_combine($keys, $keys);
+ array_multisort($tags, SORT_DESC, $tmpTags, SORT_ASC, $tags);
+ return $tags;
+ }
+
+ /**
+ * Rename or delete a tag across all bookmarks.
+ *
+ * @param string $from Tag to rename
+ * @param string $to New tag. If none is provided, the from tag will be deleted
+ *
+ * @return array|bool List of altered bookmarks or false on error
+ */
+ public function renameTag($from, $to)
+ {
+ if (empty($from)) {
+ return false;
+ }
+ $delete = empty($to);
+ // True for case-sensitive tag search.
+ $linksToAlter = $this->filterSearch(['searchtags' => $from], true);
+ foreach ($linksToAlter as $key => &$value) {
+ $tags = preg_split('/\s+/', trim($value['tags']));
+ if (($pos = array_search($from, $tags)) !== false) {
+ if ($delete) {
+ unset($tags[$pos]); // Remove tag.
+ } else {
+ $tags[$pos] = trim($to);
+ }
+ $value['tags'] = trim(implode(' ', array_unique($tags)));
+ $this[$value['id']] = $value;
+ }
+ }
+
+ return $linksToAlter;
+ }
+
+ /**
+ * Returns the list of days containing articles (oldest first)
+ * Output: An array containing days (in format YYYYMMDD).
+ */
+ public function days()
+ {
+ $linkDays = [];
+ foreach ($this->links as $link) {
+ $linkDays[$link['created']->format('Ymd')] = 0;
+ }
+ $linkDays = array_keys($linkDays);
+ sort($linkDays);
+
+ return $linkDays;
+ }
+
+ /**
+ * Reorder bookmarks by creation date (newest first).
+ *
+ * Also update the urls and ids mapping arrays.
+ *
+ * @param string $order ASC|DESC
+ */
+ public function reorder($order = 'DESC')
+ {
+ $order = $order === 'ASC' ? -1 : 1;
+ // Reorder array by dates.
+ usort($this->links, function ($a, $b) use ($order) {
+ if (isset($a['sticky']) && isset($b['sticky']) && $a['sticky'] !== $b['sticky']) {
+ return $a['sticky'] ? -1 : 1;
+ }
+ if ($a['created'] == $b['created']) {
+ return $a['id'] < $b['id'] ? 1 * $order : -1 * $order;
+ }
+ return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
+ });
+
+ $this->urls = [];
+ $this->ids = [];
+ foreach ($this->links as $key => $link) {
+ $this->urls[$link['url']] = $key;
+ $this->ids[$link['id']] = $key;
+ }
+ }
+
+ /**
+ * Return the next key for link creation.
+ * E.g. If the last ID is 597, the next will be 598.
+ *
+ * @return int next ID.
+ */
+ public function getNextId()
+ {
+ if (!empty($this->ids)) {
+ return max(array_keys($this->ids)) + 1;
+ }
+ return 0;
+ }
+
+ /**
+ * Returns a link offset in bookmarks array from its unique ID.
+ *
+ * @param int $id Persistent ID of a link.
+ *
+ * @return int Real offset in local array, or null if doesn't exist.
+ */
+ protected function getLinkOffset($id)
+ {
+ if (isset($this->ids[$id])) {
+ return $this->ids[$id];
+ }
+ return null;
+ }
+}
diff --git a/application/legacy/LegacyLinkFilter.php b/application/legacy/LegacyLinkFilter.php
new file mode 100644
index 00000000..e6d186c4
--- /dev/null
+++ b/application/legacy/LegacyLinkFilter.php
@@ -0,0 +1,451 @@
+links = $links;
+ }
+
+ /**
+ * Filter links according to parameters.
+ *
+ * @param string $type Type of filter (eg. tags, permalink, etc.).
+ * @param mixed $request Filter content.
+ * @param bool $casesensitive Optional: Perform case sensitive filter if true.
+ * @param string $visibility Optional: return only all/private/public links
+ * @param string $untaggedonly Optional: return only untagged links. Applies only if $type includes FILTER_TAG
+ *
+ * @return array filtered link list.
+ */
+ public function filter($type, $request, $casesensitive = false, $visibility = 'all', $untaggedonly = false)
+ {
+ if (!in_array($visibility, ['all', 'public', 'private'])) {
+ $visibility = 'all';
+ }
+
+ switch ($type) {
+ case self::$FILTER_HASH:
+ return $this->filterSmallHash($request);
+ case self::$FILTER_TAG | self::$FILTER_TEXT: // == "vuotext"
+ $noRequest = empty($request) || (empty($request[0]) && empty($request[1]));
+ if ($noRequest) {
+ if ($untaggedonly) {
+ return $this->filterUntagged($visibility);
+ }
+ return $this->noFilter($visibility);
+ }
+ if ($untaggedonly) {
+ $filtered = $this->filterUntagged($visibility);
+ } else {
+ $filtered = $this->links;
+ }
+ if (!empty($request[0])) {
+ $filtered = (new LegacyLinkFilter($filtered))->filterTags($request[0], $casesensitive, $visibility);
+ }
+ if (!empty($request[1])) {
+ $filtered = (new LegacyLinkFilter($filtered))->filterFulltext($request[1], $visibility);
+ }
+ return $filtered;
+ case self::$FILTER_TEXT:
+ return $this->filterFulltext($request, $visibility);
+ case self::$FILTER_TAG:
+ if ($untaggedonly) {
+ return $this->filterUntagged($visibility);
+ } else {
+ return $this->filterTags($request, $casesensitive, $visibility);
+ }
+ case self::$FILTER_DAY:
+ return $this->filterDay($request);
+ default:
+ return $this->noFilter($visibility);
+ }
+ }
+
+ /**
+ * Unknown filter, but handle private only.
+ *
+ * @param string $visibility Optional: return only all/private/public links
+ *
+ * @return array filtered links.
+ */
+ private function noFilter($visibility = 'all')
+ {
+ if ($visibility === 'all') {
+ return $this->links;
+ }
+
+ $out = [];
+ foreach ($this->links as $key => $value) {
+ if ($value['private'] && $visibility === 'private') {
+ $out[$key] = $value;
+ } elseif (!$value['private'] && $visibility === 'public') {
+ $out[$key] = $value;
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Returns the shaare corresponding to a smallHash.
+ *
+ * @param string $smallHash permalink hash.
+ *
+ * @return array $filtered array containing permalink data.
+ *
+ * @throws BookmarkNotFoundException if the smallhash doesn't match any link.
+ */
+ private function filterSmallHash($smallHash)
+ {
+ $filtered = [];
+ foreach ($this->links as $key => $l) {
+ if ($smallHash == $l['shorturl']) {
+ // Yes, this is ugly and slow
+ $filtered[$key] = $l;
+ return $filtered;
+ }
+ }
+
+ if (empty($filtered)) {
+ throw new BookmarkNotFoundException();
+ }
+
+ return $filtered;
+ }
+
+ /**
+ * Returns the list of links corresponding to a full-text search
+ *
+ * Searches:
+ * - in the URLs, title and description;
+ * - are case-insensitive;
+ * - terms surrounded by quotes " are exact terms search.
+ * - terms starting with a dash - are excluded (except exact terms).
+ *
+ * Example:
+ * print_r($mydb->filterFulltext('hollandais'));
+ *
+ * mb_convert_case($val, MB_CASE_LOWER, 'UTF-8')
+ * - allows to perform searches on Unicode text
+ * - see https://github.com/shaarli/Shaarli/issues/75 for examples
+ *
+ * @param string $searchterms search query.
+ * @param string $visibility Optional: return only all/private/public links.
+ *
+ * @return array search results.
+ */
+ private function filterFulltext($searchterms, $visibility = 'all')
+ {
+ if (empty($searchterms)) {
+ return $this->noFilter($visibility);
+ }
+
+ $filtered = [];
+ $search = mb_convert_case(html_entity_decode($searchterms), MB_CASE_LOWER, 'UTF-8');
+ $exactRegex = '/"([^"]+)"/';
+ // Retrieve exact search terms.
+ preg_match_all($exactRegex, $search, $exactSearch);
+ $exactSearch = array_values(array_filter($exactSearch[1]));
+
+ // Remove exact search terms to get AND terms search.
+ $explodedSearchAnd = explode(' ', trim(preg_replace($exactRegex, '', $search)));
+ $explodedSearchAnd = array_values(array_filter($explodedSearchAnd));
+
+ // Filter excluding terms and update andSearch.
+ $excludeSearch = [];
+ $andSearch = [];
+ foreach ($explodedSearchAnd as $needle) {
+ if ($needle[0] == '-' && strlen($needle) > 1) {
+ $excludeSearch[] = substr($needle, 1);
+ } else {
+ $andSearch[] = $needle;
+ }
+ }
+
+ $keys = ['title', 'description', 'url', 'tags'];
+
+ // Iterate over every stored link.
+ foreach ($this->links as $id => $link) {
+ // ignore non private links when 'privatonly' is on.
+ if ($visibility !== 'all') {
+ if (!$link['private'] && $visibility === 'private') {
+ continue;
+ } elseif ($link['private'] && $visibility === 'public') {
+ continue;
+ }
+ }
+
+ // Concatenate link fields to search across fields.
+ // Adds a '\' separator for exact search terms.
+ $content = '';
+ foreach ($keys as $key) {
+ $content .= mb_convert_case($link[$key], MB_CASE_LOWER, 'UTF-8') . '\\';
+ }
+
+ // Be optimistic
+ $found = true;
+
+ // First, we look for exact term search
+ for ($i = 0; $i < count($exactSearch) && $found; $i++) {
+ $found = strpos($content, $exactSearch[$i]) !== false;
+ }
+
+ // Iterate over keywords, if keyword is not found,
+ // no need to check for the others. We want all or nothing.
+ for ($i = 0; $i < count($andSearch) && $found; $i++) {
+ $found = strpos($content, $andSearch[$i]) !== false;
+ }
+
+ // Exclude terms.
+ for ($i = 0; $i < count($excludeSearch) && $found; $i++) {
+ $found = strpos($content, $excludeSearch[$i]) === false;
+ }
+
+ if ($found) {
+ $filtered[$id] = $link;
+ }
+ }
+
+ 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
+ */
+ private static function tag2regex($tag)
+ {
+ $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
+ }
+ $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
+ // iterate over string, separating it into placeholder and content
+ for (; $i < $len; $i++) {
+ if ($tag[$i] === '*') {
+ // placeholder found
+ $regex .= '[^ ]*?';
+ } 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;
+ }
+ }
+ $regex .= '(?:$| ))'; // after the tag may only be a space or the end
+ return $regex;
+ }
+
+ /**
+ * Returns the list of links associated with a given list of tags
+ *
+ * You can specify one or more tags, separated by space or a comma, e.g.
+ * print_r($mydb->filterTags('linux programming'));
+ *
+ * @param string $tags list of tags separated by commas or blank spaces.
+ * @param bool $casesensitive ignore case if false.
+ * @param string $visibility Optional: return only all/private/public links.
+ *
+ * @return array filtered links.
+ */
+ public function filterTags($tags, $casesensitive = false, $visibility = 'all')
+ {
+ // get single tags (we may get passed an array, even though the docs say different)
+ $inputTags = $tags;
+ if (!is_array($tags)) {
+ // we got an input string, split tags
+ $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
+ }
+
+ if (!count($inputTags)) {
+ // no input tags
+ return $this->noFilter($visibility);
+ }
+
+ // build regex from all tags
+ $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
+ if (!$casesensitive) {
+ // make regex case insensitive
+ $re .= 'i';
+ }
+
+ // create resulting array
+ $filtered = [];
+
+ // iterate over each link
+ foreach ($this->links as $key => $link) {
+ // check level of visibility
+ // ignore non private links when 'privateonly' is on.
+ if ($visibility !== 'all') {
+ if (!$link['private'] && $visibility === 'private') {
+ continue;
+ } elseif ($link['private'] && $visibility === 'public') {
+ continue;
+ }
+ }
+ $search = $link['tags']; // build search string, start with tags of current link
+ if (strlen(trim($link['description'])) && strpos($link['description'], '#') !== 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(
+ '/(?links as $key => $link) {
+ if ($visibility !== 'all') {
+ if (!$link['private'] && $visibility === 'private') {
+ continue;
+ } elseif ($link['private'] && $visibility === 'public') {
+ continue;
+ }
+ }
+
+ if (empty(trim($link['tags']))) {
+ $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.
+ *
+ * @return array all link matching given day.
+ *
+ * @throws Exception if date format is invalid.
+ */
+ public function filterDay($day)
+ {
+ if (!checkDateFormat('Ymd', $day)) {
+ throw new Exception('Invalid date format');
+ }
+
+ $filtered = [];
+ foreach ($this->links as $key => $l) {
+ if ($l['created']->format('Ymd') == $day) {
+ $filtered[$key] = $l;
+ }
+ }
+
+ // sort by date ASC
+ return array_reverse($filtered, true);
+ }
+
+ /**
+ * Convert a list of tags (str) to an array. Also
+ * - handle case sensitivity.
+ * - accepts spaces commas as separator.
+ *
+ * @param string $tags string containing a list of tags.
+ * @param bool $casesensitive will convert everything to lowercase if false.
+ *
+ * @return array filtered tags string.
+ */
+ public static function tagsStrToArray($tags, $casesensitive)
+ {
+ // We use UTF-8 conversion to handle various graphemes (i.e. cyrillic, or greek)
+ $tagsOut = $casesensitive ? $tags : mb_convert_case($tags, MB_CASE_LOWER, 'UTF-8');
+ $tagsOut = str_replace(',', ' ', $tagsOut);
+
+ return preg_split('/\s+/', $tagsOut, -1, PREG_SPLIT_NO_EMPTY);
+ }
+}
diff --git a/application/legacy/LegacyRouter.php b/application/legacy/LegacyRouter.php
new file mode 100644
index 00000000..0449c7e1
--- /dev/null
+++ b/application/legacy/LegacyRouter.php
@@ -0,0 +1,63 @@
+doneUpdates = $doneUpdates;
+ $this->linkDB = $linkDB;
+ $this->conf = $conf;
+ $this->isLoggedIn = $isLoggedIn;
+ $this->session = &$session;
+
+ // Retrieve all update methods.
+ $class = new ReflectionClass($this);
+ $this->methods = $class->getMethods();
+ }
+
+ /**
+ * Run all new updates.
+ * Update methods have to start with 'updateMethod' and return true (on success).
+ *
+ * @return array An array containing ran updates.
+ *
+ * @throws UpdaterException If something went wrong.
+ */
+ public function update()
+ {
+ $updatesRan = [];
+
+ // If the user isn't logged in, exit without updating.
+ if ($this->isLoggedIn !== true) {
+ return $updatesRan;
+ }
+
+ if ($this->methods === null) {
+ throw new UpdaterException(t('Couldn\'t retrieve updater class methods.'));
+ }
+
+ foreach ($this->methods as $method) {
+ // Not an update method or already done, pass.
+ if (
+ !startsWith($method->getName(), 'updateMethod')
+ || in_array($method->getName(), $this->doneUpdates)
+ ) {
+ continue;
+ }
+
+ try {
+ $method->setAccessible(true);
+ $res = $method->invoke($this);
+ // Update method must return true to be considered processed.
+ if ($res === true) {
+ $updatesRan[] = $method->getName();
+ }
+ } catch (Exception $e) {
+ throw new UpdaterException($method, $e);
+ }
+ }
+
+ $this->doneUpdates = array_merge($this->doneUpdates, $updatesRan);
+
+ return $updatesRan;
+ }
+
+ /**
+ * @return array Updates methods already processed.
+ */
+ public function getDoneUpdates()
+ {
+ return $this->doneUpdates;
+ }
+
+ /**
+ * Move deprecated options.php to config.php.
+ *
+ * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
+ * options.php is not supported anymore.
+ */
+ public function updateMethodMergeDeprecatedConfigFile()
+ {
+ if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
+ include $this->conf->get('resource.data_dir') . '/options.php';
+
+ // Load GLOBALS into config
+ $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
+ $allowedKeys[] = 'config';
+ foreach ($GLOBALS as $key => $value) {
+ if (in_array($key, $allowedKeys)) {
+ $this->conf->set($key, $value);
+ }
+ }
+ $this->conf->write($this->isLoggedIn);
+ unlink($this->conf->get('resource.data_dir') . '/options.php');
+ }
+
+ return true;
+ }
+
+ /**
+ * Move old configuration in PHP to the new config system in JSON format.
+ *
+ * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
+ * It will also convert legacy setting keys to the new ones.
+ */
+ public function updateMethodConfigToJson()
+ {
+ // JSON config already exists, nothing to do.
+ if ($this->conf->getConfigIO() instanceof ConfigJson) {
+ return true;
+ }
+
+ $configPhp = new ConfigPhp();
+ $configJson = new ConfigJson();
+ $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
+ rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
+ $this->conf->setConfigIO($configJson);
+ $this->conf->reload();
+
+ $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
+ foreach (ConfigPhp::$ROOT_KEYS as $key) {
+ $this->conf->set($legacyMap[$key], $oldConfig[$key]);
+ }
+
+ // Set sub config keys (config and plugins)
+ $subConfig = ['config', 'plugins'];
+ foreach ($subConfig as $sub) {
+ foreach ($oldConfig[$sub] as $key => $value) {
+ if (isset($legacyMap[$sub . '.' . $key])) {
+ $configKey = $legacyMap[$sub . '.' . $key];
+ } else {
+ $configKey = $sub . '.' . $key;
+ }
+ $this->conf->set($configKey, $value);
+ }
+ }
+
+ try {
+ $this->conf->write($this->isLoggedIn);
+ return true;
+ } catch (IOException $e) {
+ error_log($e->getMessage());
+ return false;
+ }
+ }
+
+ /**
+ * Escape settings which have been manually escaped in every request in previous versions:
+ * - general.title
+ * - general.header_link
+ * - redirector.url
+ *
+ * @return bool true if the update is successful, false otherwise.
+ */
+ public function updateMethodEscapeUnescapedConfig()
+ {
+ try {
+ $this->conf->set('general.title', escape($this->conf->get('general.title')));
+ $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
+ $this->conf->write($this->isLoggedIn);
+ } catch (Exception $e) {
+ error_log($e->getMessage());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Update the database to use the new ID system, which replaces linkdate primary keys.
+ * Also, creation and update dates are now DateTime objects (done by LinkDB).
+ *
+ * Since this update is very sensitve (changing the whole database), the datastore will be
+ * automatically backed up into the file datastore..php.
+ *
+ * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
+ * which will be saved by this method.
+ *
+ * @return bool true if the update is successful, false otherwise.
+ */
+ public function updateMethodDatastoreIds()
+ {
+ $first = 'update';
+ foreach ($this->linkDB as $key => $link) {
+ $first = $key;
+ break;
+ }
+
+ // up to date database
+ if (is_int($first)) {
+ return true;
+ }
+
+ $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
+ copy($this->conf->get('resource.datastore'), $save);
+
+ $links = [];
+ foreach ($this->linkDB as $offset => $value) {
+ $links[] = $value;
+ unset($this->linkDB[$offset]);
+ }
+ $links = array_reverse($links);
+ $cpt = 0;
+ foreach ($links as $l) {
+ unset($l['linkdate']);
+ $l['id'] = $cpt;
+ $this->linkDB[$cpt++] = $l;
+ }
+
+ $this->linkDB->save($this->conf->get('resource.page_cache'));
+ $this->linkDB->reorder();
+
+ return true;
+ }
+
+ /**
+ * Rename tags starting with a '-' to work with tag exclusion search.
+ */
+ public function updateMethodRenameDashTags()
+ {
+ $linklist = $this->linkDB->filterSearch();
+ foreach ($linklist as $key => $link) {
+ $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
+ $link['tags'] = implode(' ', array_unique(BookmarkFilter::tagsStrToArray($link['tags'], true)));
+ $this->linkDB[$key] = $link;
+ }
+ $this->linkDB->save($this->conf->get('resource.page_cache'));
+ return true;
+ }
+
+ /**
+ * Initialize API settings:
+ * - api.enabled: true
+ * - api.secret: generated secret
+ */
+ public function updateMethodApiSettings()
+ {
+ if ($this->conf->exists('api.secret')) {
+ return true;
+ }
+
+ $this->conf->set('api.enabled', true);
+ $this->conf->set(
+ 'api.secret',
+ generate_api_secret(
+ $this->conf->get('credentials.login'),
+ $this->conf->get('credentials.salt')
+ )
+ );
+ $this->conf->write($this->isLoggedIn);
+ return true;
+ }
+
+ /**
+ * New setting: theme name. If the default theme is used, nothing to do.
+ *
+ * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
+ * and the current theme is set as default in the theme setting.
+ *
+ * @return bool true if the update is successful, false otherwise.
+ */
+ public function updateMethodDefaultTheme()
+ {
+ // raintpl_tpl isn't the root template directory anymore.
+ // We run the update only if this folder still contains the template files.
+ $tplDir = $this->conf->get('resource.raintpl_tpl');
+ $tplFile = $tplDir . '/linklist.html';
+ if (!file_exists($tplFile)) {
+ return true;
+ }
+
+ $parent = dirname($tplDir);
+ $this->conf->set('resource.raintpl_tpl', $parent);
+ $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
+ $this->conf->write($this->isLoggedIn);
+
+ // Dependency injection gore
+ RainTPL::$tpl_dir = $tplDir;
+
+ return true;
+ }
+
+ /**
+ * Move the file to inc/user.css to data/user.css.
+ *
+ * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
+ *
+ * @return bool true if the update is successful, false otherwise.
+ */
+ public function updateMethodMoveUserCss()
+ {
+ if (!is_file('inc/user.css')) {
+ return true;
+ }
+
+ return rename('inc/user.css', 'data/user.css');
+ }
+
+ /**
+ * * `markdown_escape` is a new setting, set to true as default.
+ *
+ * If the markdown plugin was already enabled, escaping is disabled to avoid
+ * breaking existing entries.
+ */
+ public function updateMethodEscapeMarkdown()
+ {
+ if ($this->conf->exists('security.markdown_escape')) {
+ return true;
+ }
+
+ if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
+ $this->conf->set('security.markdown_escape', false);
+ } else {
+ $this->conf->set('security.markdown_escape', true);
+ }
+ $this->conf->write($this->isLoggedIn);
+
+ return true;
+ }
+
+ /**
+ * Add 'http://' to Piwik URL the setting is set.
+ *
+ * @return bool true if the update is successful, false otherwise.
+ */
+ public function updateMethodPiwikUrl()
+ {
+ if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
+ return true;
+ }
+
+ $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL'));
+ $this->conf->write($this->isLoggedIn);
+
+ return true;
+ }
+
+ /**
+ * Use ATOM feed as default.
+ */
+ public function updateMethodAtomDefault()
+ {
+ if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) {
+ return true;
+ }
+
+ $this->conf->set('feed.show_atom', true);
+ $this->conf->write($this->isLoggedIn);
+
+ return true;
+ }
+
+ /**
+ * Update updates.check_updates_branch setting.
+ *
+ * If the current major version digit matches the latest branch
+ * major version digit, we set the branch to `latest`,
+ * otherwise we'll check updates on the `stable` branch.
+ *
+ * No update required for the dev version.
+ *
+ * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
+ *
+ * FIXME! This needs to be removed when we switch to first digit major version
+ * instead of the second one since the versionning process will change.
+ */
+ public function updateMethodCheckUpdateRemoteBranch()
+ {
+ if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
+ return true;
+ }
+
+ // Get latest branch major version digit
+ $latestVersion = ApplicationUtils::getLatestGitVersionCode(
+ 'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
+ 5
+ );
+ if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
+ return false;
+ }
+ $latestMajor = $matches[1];
+
+ // Get current major version digit
+ preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
+ $currentMajor = $matches[1];
+
+ if ($currentMajor === $latestMajor) {
+ $branch = 'latest';
+ } else {
+ $branch = 'stable';
+ }
+ $this->conf->set('updates.check_updates_branch', $branch);
+ $this->conf->write($this->isLoggedIn);
+ return true;
+ }
+
+ /**
+ * Reset history store file due to date format change.
+ */
+ public function updateMethodResetHistoryFile()
+ {
+ if (is_file($this->conf->get('resource.history'))) {
+ unlink($this->conf->get('resource.history'));
+ }
+ return true;
+ }
+
+ /**
+ * Save the datastore -> the link order is now applied when bookmarks are saved.
+ */
+ public function updateMethodReorderDatastore()
+ {
+ $this->linkDB->save($this->conf->get('resource.page_cache'));
+ return true;
+ }
+
+ /**
+ * Change privateonly session key to visibility.
+ */
+ public function updateMethodVisibilitySession()
+ {
+ if (isset($_SESSION['privateonly'])) {
+ unset($_SESSION['privateonly']);
+ $_SESSION['visibility'] = 'private';
+ }
+ return true;
+ }
+
+ /**
+ * Add download size and timeout to the configuration file
+ *
+ * @return bool true if the update is successful, false otherwise.
+ */
+ public function updateMethodDownloadSizeAndTimeoutConf()
+ {
+ if (
+ $this->conf->exists('general.download_max_size')
+ && $this->conf->exists('general.download_timeout')
+ ) {
+ return true;
+ }
+
+ if (!$this->conf->exists('general.download_max_size')) {
+ $this->conf->set('general.download_max_size', 1024 * 1024 * 4);
+ }
+
+ if (!$this->conf->exists('general.download_timeout')) {
+ $this->conf->set('general.download_timeout', 30);
+ }
+
+ $this->conf->write($this->isLoggedIn);
+ return true;
+ }
+
+ /**
+ * * Move thumbnails management to WebThumbnailer, coming with new settings.
+ */
+ public function updateMethodWebThumbnailer()
+ {
+ if ($this->conf->exists('thumbnails.mode')) {
+ return true;
+ }
+
+ $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
+ $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
+ $this->conf->set('thumbnails.width', 125);
+ $this->conf->set('thumbnails.height', 90);
+ $this->conf->remove('thumbnail');
+ $this->conf->write(true);
+
+ if ($thumbnailsEnabled) {
+ $this->session['warnings'][] = t(
+ t('You have enabled or changed thumbnails mode.') .
+ '' . t('Please synchronize them.') . ''
+ );
+ }
+
+ return true;
+ }
+
+ /**
+ * Set sticky = false on all bookmarks
+ *
+ * @return bool true if the update is successful, false otherwise.
+ */
+ public function updateMethodSetSticky()
+ {
+ foreach ($this->linkDB as $key => $link) {
+ if (isset($link['sticky'])) {
+ return true;
+ }
+ $link['sticky'] = false;
+ $this->linkDB[$key] = $link;
+ }
+
+ $this->linkDB->save($this->conf->get('resource.page_cache'));
+
+ return true;
+ }
+
+ /**
+ * Remove redirector settings.
+ */
+ public function updateMethodRemoveRedirector()
+ {
+ $this->conf->remove('redirector');
+ $this->conf->write(true);
+ return true;
+ }
+
+ /**
+ * Migrate the legacy arrays to Bookmark objects.
+ * Also make a backup of the datastore.
+ */
+ public function updateMethodMigrateDatabase()
+ {
+ $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '_1.php';
+ if (! copy($this->conf->get('resource.datastore'), $save)) {
+ die('Could not backup the datastore.');
+ }
+
+ $linksArray = new BookmarkArray();
+ foreach ($this->linkDB as $key => $link) {
+ $linksArray[$key] = (new Bookmark())->fromArray($link, $this->conf->get('general.tags_separator', ' '));
+ }
+ $linksIo = new BookmarkIO($this->conf);
+ $linksIo->write($linksArray);
+
+ return true;
+ }
+
+ /**
+ * Write the `formatter` setting in config file.
+ * Use markdown if the markdown plugin is enabled, the default one otherwise.
+ * Also remove markdown plugin setting as it is now integrated to the core.
+ */
+ public function updateMethodFormatterSetting()
+ {
+ if (!$this->conf->exists('formatter') || $this->conf->get('formatter') === 'default') {
+ $enabledPlugins = $this->conf->get('general.enabled_plugins');
+ if (($pos = array_search('markdown', $enabledPlugins)) !== false) {
+ $formatter = 'markdown';
+ unset($enabledPlugins[$pos]);
+ $this->conf->set('general.enabled_plugins', array_values($enabledPlugins));
+ } else {
+ $formatter = 'default';
+ }
+ $this->conf->set('formatter', $formatter);
+ $this->conf->write(true);
+ }
+
+ return true;
+ }
+}
diff --git a/application/legacy/UnknowLegacyRouteException.php b/application/legacy/UnknowLegacyRouteException.php
new file mode 100644
index 00000000..ae1518ad
--- /dev/null
+++ b/application/legacy/UnknowLegacyRouteException.php
@@ -0,0 +1,9 @@
+bookmarkService = $bookmarkService;
+ $this->conf = $conf;
+ $this->history = $history;
+ }
/**
- * Filters links and adds Netscape-formatted fields
+ * Filters bookmarks and adds Netscape-formatted fields
*
* Added fields:
* - timestamp link addition date, using the Unix epoch format
* - taglist comma-separated tag list
*
- * @param LinkDB $linkDb Link datastore
- * @param string $selection Which links to export: (all|private|public)
- * @param bool $prependNoteUrl Prepend note permalinks with the server's URL
- * @param string $indexUrl Absolute URL of the Shaarli index page
+ * @param BookmarkFormatter $formatter instance
+ * @param string $selection Which bookmarks to export: (all|private|public)
+ * @param bool $prependNoteUrl Prepend note permalinks with the server's URL
+ * @param string $indexUrl Absolute URL of the Shaarli index page
+ *
+ * @return array The bookmarks to be exported, with additional fields
*
* @throws Exception Invalid export selection
- *
- * @return array The links to be exported, with additional fields
*/
- public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $indexUrl)
- {
+ public function filterAndFormat(
+ $formatter,
+ $selection,
+ $prependNoteUrl,
+ $indexUrl
+ ) {
// see tpl/export.html for possible values
- if (!in_array($selection, array('all', 'public', 'private'))) {
+ if (!in_array($selection, ['all', 'public', 'private'])) {
throw new Exception(t('Invalid export selection:') . ' "' . $selection . '"');
}
- $bookmarkLinks = array();
- foreach ($linkDb as $link) {
- if ($link['private'] != 0 && $selection == 'public') {
- continue;
- }
- if ($link['private'] == 0 && $selection == 'private') {
- continue;
- }
- $date = $link['created'];
- $link['timestamp'] = $date->getTimestamp();
- $link['taglist'] = str_replace(' ', ',', $link['tags']);
-
- if (is_note($link['url']) && $prependNoteUrl) {
- $link['url'] = $indexUrl . $link['url'];
+ $bookmarkLinks = [];
+ foreach ($this->bookmarkService->search([], $selection)->getBookmarks() as $bookmark) {
+ $link = $formatter->format($bookmark);
+ $link['taglist'] = implode(',', $bookmark->getTags());
+ if ($bookmark->isNote() && $prependNoteUrl) {
+ $link['url'] = rtrim($indexUrl, '/') . '/' . ltrim($link['url'], '/');
}
$bookmarkLinks[] = $link;
@@ -64,19 +77,132 @@ public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $in
return $bookmarkLinks;
}
+ /**
+ * Imports Web bookmarks from an uploaded Netscape bookmark dump
+ *
+ * @param array $post Server $_POST parameters
+ * @param UploadedFileInterface $file File in PSR-7 object format
+ *
+ * @return string Summary of the bookmark import status
+ */
+ public function import($post, UploadedFileInterface $file)
+ {
+ $start = time();
+ $filename = $file->getClientFilename();
+ $filesize = $file->getSize();
+ $data = (string) $file->getStream();
+
+ if (preg_match('//i', $data) === 0) {
+ return $this->importStatus($filename, $filesize);
+ }
+
+ // Overwrite existing bookmarks?
+ $overwrite = !empty($post['overwrite']);
+
+ // Add tags to all imported bookmarks?
+ if (empty($post['default_tags'])) {
+ $defaultTags = [];
+ } else {
+ $defaultTags = tags_str2array(
+ escape($post['default_tags']),
+ $this->conf->get('general.tags_separator', ' ')
+ );
+ }
+
+ // Optionally Force all imported link to be either public or private.
+ $forcedPrivateStatus = !empty($post['privacy']) ? (string) $post['privacy'] : null;
+
+ $logger = new Logger(
+ $this->conf->get('resource.data_dir'),
+ !$this->conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
+ [
+ 'prefix' => 'import.',
+ 'extension' => 'log',
+ ]
+ );
+ $parser = new NetscapeBookmarkParser([], $logger);
+
+ $bookmarks = $parser->parseString($data);
+
+ $importCount = 0;
+ $overwriteCount = 0;
+ $skipCount = 0;
+
+ foreach ($bookmarks as $bkm) {
+ if ($forcedPrivateStatus == 'private') {
+ // all imported bookmarks are private
+ $isPrivate = true;
+ } elseif ($forcedPrivateStatus == 'public') {
+ // all imported bookmarks are public
+ $isPrivate = false;
+ } else {
+ // Use private value from imported file or default to public
+ $isPrivate = isset($bkm['public']) && !$bkm['public'];
+ }
+
+ $link = $this->bookmarkService->findByUrl($bkm['url']);
+ $existingLink = $link !== null;
+ if (! $existingLink) {
+ $link = new Bookmark();
+ }
+
+ if ($existingLink !== false) {
+ if ($overwrite === false) {
+ // Do not overwrite an existing link
+ $skipCount++;
+ continue;
+ }
+
+ $link->setUpdated(new DateTime());
+ $overwriteCount++;
+ } else {
+ $newLinkDate = new DateTime('@' . $bkm['dateCreated']);
+ $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
+ $link->setCreated($newLinkDate);
+ }
+
+ if (!empty($defaultTags)) {
+ $bkm['tags'] = array_merge($defaultTags, $bkm['tags']);
+ }
+
+ $link->setTitle($bkm['name']);
+ $link->setUrl($bkm['url'], $this->conf->get('security.allowed_protocols'));
+ $link->setDescription($bkm['description']);
+ $link->setPrivate($isPrivate);
+ $link->setTags($bkm['tags']);
+
+ $this->bookmarkService->addOrSet($link, false);
+ $importCount++;
+ }
+
+ $this->bookmarkService->save();
+ $this->history->importLinks();
+
+ $duration = time() - $start;
+
+ return $this->importStatus(
+ $filename,
+ $filesize,
+ $importCount,
+ $overwriteCount,
+ $skipCount,
+ $duration
+ );
+ }
+
/**
* Generates an import status summary
*
* @param string $filename name of the file to import
* @param int $filesize size of the file to import
- * @param int $importCount how many links were imported
- * @param int $overwriteCount how many links were overwritten
- * @param int $skipCount how many links were skipped
+ * @param int $importCount how many bookmarks were imported
+ * @param int $overwriteCount how many bookmarks were overwritten
+ * @param int $skipCount how many bookmarks were skipped
* @param int $duration how many seconds did the import take
*
* @return string Summary of the bookmark import status
*/
- private static function importStatus(
+ protected function importStatus(
$filename,
$filesize,
$importCount = 0,
@@ -91,135 +217,11 @@ private static function importStatus(
$status .= vsprintf(
t(
'was successfully processed in %d seconds: '
- . '%d links imported, %d links overwritten, %d links skipped.'
+ . '%d bookmarks imported, %d bookmarks overwritten, %d bookmarks skipped.'
),
[$duration, $importCount, $overwriteCount, $skipCount]
);
}
return $status;
}
-
- /**
- * Imports Web bookmarks from an uploaded Netscape bookmark dump
- *
- * @param array $post Server $_POST parameters
- * @param array $files Server $_FILES parameters
- * @param LinkDB $linkDb Loaded LinkDB instance
- * @param ConfigManager $conf instance
- * @param History $history History instance
- *
- * @return string Summary of the bookmark import status
- */
- public static function import($post, $files, $linkDb, $conf, $history)
- {
- $start = time();
- $filename = $files['filetoupload']['name'];
- $filesize = $files['filetoupload']['size'];
- $data = file_get_contents($files['filetoupload']['tmp_name']);
-
- if (preg_match('//i', $data) === 0) {
- return self::importStatus($filename, $filesize);
- }
-
- // Overwrite existing links?
- $overwrite = !empty($post['overwrite']);
-
- // Add tags to all imported links?
- if (empty($post['default_tags'])) {
- $defaultTags = array();
- } else {
- $defaultTags = preg_split(
- '/[\s,]+/',
- escape($post['default_tags'])
- );
- }
-
- // links are imported as public by default
- $defaultPrivacy = 0;
-
- $parser = new NetscapeBookmarkParser(
- true, // nested tag support
- $defaultTags, // additional user-specified tags
- strval(1 - $defaultPrivacy), // defaultPub = 1 - defaultPrivacy
- $conf->get('resource.data_dir') // log path, will be overridden
- );
- $logger = new Logger(
- $conf->get('resource.data_dir'),
- !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
- [
- 'prefix' => 'import.',
- 'extension' => 'log',
- ]
- );
- $parser->setLogger($logger);
- $bookmarks = $parser->parseString($data);
-
- $importCount = 0;
- $overwriteCount = 0;
- $skipCount = 0;
-
- foreach ($bookmarks as $bkm) {
- $private = $defaultPrivacy;
- if (empty($post['privacy']) || $post['privacy'] == 'default') {
- // use value from the imported file
- $private = $bkm['pub'] == '1' ? 0 : 1;
- } elseif ($post['privacy'] == 'private') {
- // all imported links are private
- $private = 1;
- } elseif ($post['privacy'] == 'public') {
- // all imported links are public
- $private = 0;
- }
-
- $newLink = array(
- 'title' => $bkm['title'],
- 'url' => $bkm['uri'],
- 'description' => $bkm['note'],
- 'private' => $private,
- 'tags' => $bkm['tags']
- );
-
- $existingLink = $linkDb->getLinkFromUrl($bkm['uri']);
-
- if ($existingLink !== false) {
- if ($overwrite === false) {
- // Do not overwrite an existing link
- $skipCount++;
- continue;
- }
-
- // Overwrite an existing link, keep its date
- $newLink['id'] = $existingLink['id'];
- $newLink['created'] = $existingLink['created'];
- $newLink['updated'] = new DateTime();
- $newLink['shorturl'] = $existingLink['shorturl'];
- $linkDb[$existingLink['id']] = $newLink;
- $importCount++;
- $overwriteCount++;
- continue;
- }
-
- // Add a new link - @ used for UNIX timestamps
- $newLinkDate = new DateTime('@' . strval($bkm['time']));
- $newLinkDate->setTimezone(new DateTimeZone(date_default_timezone_get()));
- $newLink['created'] = $newLinkDate;
- $newLink['id'] = $linkDb->getNextId();
- $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
- $linkDb[$newLink['id']] = $newLink;
- $importCount++;
- }
-
- $linkDb->save($conf->get('resource.page_cache'));
- $history->importLinks();
-
- $duration = time() - $start;
- return self::importStatus(
- $filename,
- $filesize,
- $importCount,
- $overwriteCount,
- $skipCount,
- $duration
- );
- }
}
diff --git a/application/plugin/PluginManager.php b/application/plugin/PluginManager.php
index f7b24a8e..b2cede28 100644
--- a/application/plugin/PluginManager.php
+++ b/application/plugin/PluginManager.php
@@ -1,8 +1,11 @@
/`.
+ * - `callable` string, function name or FQN class's method, e.g. `demo_plugin_custom_controller`.
+ */
+ protected $registeredRoutes = [];
/**
* @var ConfigManager Configuration Manager instance.
@@ -35,6 +46,9 @@ class PluginManager
*/
protected $errors;
+ /** @var callable[]|null Preloaded list of hook function for filterSearchEntry() */
+ protected $filterSearchEntryHooks = null;
+
/**
* Plugins subdirectory.
*
@@ -57,7 +71,7 @@ class PluginManager
public function __construct(&$conf)
{
$this->conf = $conf;
- $this->errors = array();
+ $this->errors = [];
}
/**
@@ -85,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]));
}
}
}
@@ -98,23 +115,38 @@ public function load($authorizedPlugins)
*
* @return void
*/
- public function executeHooks($hook, &$data, $params = array())
+ public function executeHooks($hook, &$data, $params = [])
{
- if (!empty($params['target'])) {
- $data['_PAGE_'] = $params['target'];
- }
+ $metadataParameters = [
+ 'target' => '_PAGE_',
+ 'loggedin' => '_LOGGEDIN_',
+ 'basePath' => '_BASE_PATH_',
+ 'rootPath' => '_ROOT_PATH_',
+ 'bookmarkService' => '_BOOKMARK_SERVICE_',
+ ];
- if (isset($params['loggedin'])) {
- $data['_LOGGEDIN_'] = $params['loggedin'];
+ foreach ($metadataParameters as $parameter => $metaKey) {
+ if (array_key_exists($parameter, $params)) {
+ $data[$metaKey] = $params[$parameter];
+ }
}
foreach ($this->loadedPlugins as $plugin) {
$hookFunction = $this->buildHookName($hook, $plugin);
if (function_exists($hookFunction)) {
- $data = call_user_func($hookFunction, $data, $this->conf);
+ try {
+ $data = call_user_func($hookFunction, $data, $this->conf);
+ } catch (\Throwable $e) {
+ $error = $plugin . t(' [plugin incompatibility]: ') . $e->getMessage();
+ $this->errors = array_unique(array_merge($this->errors, [$error]));
+ }
}
}
+
+ foreach ($metadataParameters as $metaKey) {
+ unset($data[$metaKey]);
+ }
}
/**
@@ -150,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;
}
@@ -181,7 +229,7 @@ public function buildHookName($hook, $pluginName)
*/
public function getPluginsMeta()
{
- $metaData = array();
+ $metaData = [];
$dirs = glob(self::$PLUGINS_PATH . '/*', GLOB_ONLYDIR | GLOB_MARK);
// Browse all plugin directories.
@@ -202,9 +250,9 @@ public function getPluginsMeta()
if (isset($metaData[$plugin]['parameters'])) {
$params = explode(';', $metaData[$plugin]['parameters']);
} else {
- $params = array();
+ $params = [];
}
- $metaData[$plugin]['parameters'] = array();
+ $metaData[$plugin]['parameters'] = [];
foreach ($params as $param) {
if (empty($param)) {
continue;
@@ -221,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.
*
@@ -230,4 +294,74 @@ 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).
+ *
+ * We do not check the format because Slim routes support regexes.
+ *
+ * @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('callable', $input)) {
+ return false;
+ }
+
+ return true;
+ }
}
diff --git a/application/plugin/exception/PluginFileNotFoundException.php b/application/plugin/exception/PluginFileNotFoundException.php
index e5386f02..21ac6604 100644
--- a/application/plugin/exception/PluginFileNotFoundException.php
+++ b/application/plugin/exception/PluginFileNotFoundException.php
@@ -1,4 +1,5 @@
message = 'trying to register invalid route.';
+ }
+}
diff --git a/application/render/PageBuilder.php b/application/render/PageBuilder.php
index 3f86fc26..bf0ae326 100644
--- a/application/render/PageBuilder.php
+++ b/application/render/PageBuilder.php
@@ -3,10 +3,12 @@
namespace Shaarli\Render;
use Exception;
+use Psr\Log\LoggerInterface;
use RainTPL;
-use Shaarli\ApplicationUtils;
-use Shaarli\Bookmark\LinkDB;
+use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
+use Shaarli\Helper\ApplicationUtils;
+use Shaarli\Security\SessionManager;
use Shaarli\Thumbnailer;
/**
@@ -33,10 +35,13 @@ class PageBuilder
*/
protected $session;
+ /** @var LoggerInterface */
+ protected $logger;
+
/**
- * @var LinkDB $linkDB instance.
+ * @var BookmarkServiceInterface $bookmarkService instance.
*/
- protected $linkDB;
+ protected $bookmarkService;
/**
* @var null|string XSRF token
@@ -52,22 +57,39 @@ class PageBuilder
* PageBuilder constructor.
* $tpl is initialized at false for lazy loading.
*
- * @param ConfigManager $conf Configuration Manager instance (reference).
- * @param array $session $_SESSION array
- * @param LinkDB $linkDB instance.
- * @param string $token Session token
- * @param bool $isLoggedIn
+ * @param ConfigManager $conf Configuration Manager instance (reference).
+ * @param array $session $_SESSION array
+ * @param LoggerInterface $logger
+ * @param null $linkDB instance.
+ * @param null $token Session token
+ * @param bool $isLoggedIn
*/
- public function __construct(&$conf, $session, $linkDB = null, $token = null, $isLoggedIn = false)
- {
+ public function __construct(
+ ConfigManager &$conf,
+ array $session,
+ LoggerInterface $logger,
+ $linkDB = null,
+ $token = null,
+ $isLoggedIn = false
+ ) {
$this->tpl = false;
$this->conf = $conf;
$this->session = $session;
- $this->linkDB = $linkDB;
+ $this->logger = $logger;
+ $this->bookmarkService = $linkDB;
$this->token = $token;
$this->isLoggedIn = $isLoggedIn;
}
+ /**
+ * Reset current state of template rendering.
+ * Mostly useful for error handling. We remove everything, and display the error template.
+ */
+ public function reset(): void
+ {
+ $this->tpl = false;
+ }
+
/**
* Initialize all default tpl tags.
*/
@@ -87,7 +109,7 @@ private function initialize()
$this->tpl->assign('newVersion', escape($version));
$this->tpl->assign('versionError', '');
} catch (Exception $exc) {
- logm($this->conf->get('resource.log'), $_SERVER['REMOTE_ADDR'], $exc->getMessage());
+ $this->logger->error(format_log('Error: ' . $exc->getMessage(), client_ip_id($_SERVER)));
$this->tpl->assign('newVersion', '');
$this->tpl->assign('versionError', escape($exc->getMessage()));
}
@@ -125,8 +147,8 @@ private function initialize()
$this->tpl->assign('language', $this->conf->get('translation.language'));
- if ($this->linkDB !== null) {
- $this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
+ if ($this->bookmarkService !== null) {
+ $this->tpl->assign('tags', escape($this->bookmarkService->bookmarksCountPerTag()));
}
$this->tpl->assign(
@@ -136,15 +158,45 @@ private function initialize()
$this->tpl->assign('thumbnails_width', $this->conf->get('thumbnails.width'));
$this->tpl->assign('thumbnails_height', $this->conf->get('thumbnails.height'));
- if (!empty($_SESSION['warnings'])) {
- $this->tpl->assign('global_warnings', $_SESSION['warnings']);
- unset($_SESSION['warnings']);
- }
+ $this->tpl->assign('formatter', $this->conf->get('formatter', 'default'));
+
+ $this->tpl->assign('links_per_page', $this->session['LINKS_PER_PAGE'] ?? 20);
+ $this->tpl->assign('tags_separator', $this->conf->get('general.tags_separator', ' '));
// To be removed with a proper theme configuration.
$this->tpl->assign('conf', $this->conf);
}
+ /**
+ * Affect variable after controller processing.
+ * Used for alert messages.
+ */
+ protected function finalize(string $basePath): void
+ {
+ // TODO: use the SessionManager
+ $messageKeys = [
+ SessionManager::KEY_SUCCESS_MESSAGES,
+ SessionManager::KEY_WARNING_MESSAGES,
+ SessionManager::KEY_ERROR_MESSAGES
+ ];
+ foreach ($messageKeys as $messageKey) {
+ if (!empty($_SESSION[$messageKey])) {
+ $this->tpl->assign('global_' . $messageKey, $_SESSION[$messageKey]);
+ unset($_SESSION[$messageKey]);
+ }
+ }
+
+ $rootPath = preg_replace('#/index\.php$#', '', $basePath);
+ $this->assign('base_path', $basePath);
+ $this->assign('root_path', $rootPath);
+ $this->assign(
+ 'asset_path',
+ $rootPath . '/' .
+ rtrim($this->conf->get('resource.raintpl_tpl', 'tpl'), '/') . '/' .
+ $this->conf->get('resource.theme', 'default')
+ );
+ }
+
/**
* The following assign() method is basically the same as RainTPL (except lazy loading)
*
@@ -183,33 +235,21 @@ public function assignAll($data)
}
/**
- * Render a specific page (using a template file).
- * e.g. $pb->renderPage('picwall');
+ * Render a specific page as string (using a template file).
+ * e.g. $pb->render('picwall');
*
* @param string $page Template filename (without extension).
+ *
+ * @return string Processed template content
*/
- public function renderPage($page)
+ public function render(string $page, string $basePath): string
{
if ($this->tpl === false) {
$this->initialize();
}
- $this->tpl->draw($page);
- }
+ $this->finalize($basePath);
- /**
- * Render a 404 page (uses the template : tpl/404.tpl)
- * usage: $PAGE->render404('The link was deleted')
- *
- * @param string $message A message to display what is not found
- */
- public function render404($message = '')
- {
- if (empty($message)) {
- $message = t('The page you are trying to reach does not exist or has been deleted.');
- }
- header($_SERVER['SERVER_PROTOCOL'] . ' ' . t('404 Not Found'));
- $this->tpl->assign('error_message', $message);
- $this->renderPage('404');
+ return $this->tpl->draw($page, true);
}
}
diff --git a/application/render/PageCacheManager.php b/application/render/PageCacheManager.php
new file mode 100644
index 00000000..fe74bf27
--- /dev/null
+++ b/application/render/PageCacheManager.php
@@ -0,0 +1,70 @@
+pageCacheDir = $pageCacheDir;
+ $this->isLoggedIn = $isLoggedIn;
+ }
+
+ /**
+ * Purges all cached pages
+ *
+ * @return string|null an error string if the directory is missing
+ */
+ public function purgeCachedPages(): ?string
+ {
+ if (!is_dir($this->pageCacheDir)) {
+ $error = sprintf(t('Cannot purge %s: no directory'), $this->pageCacheDir);
+ error_log($error);
+
+ return $error;
+ }
+
+ array_map('unlink', glob($this->pageCacheDir . '/*.cache'));
+
+ return null;
+ }
+
+ /**
+ * Invalidates caches when the database is changed or the user logs out.
+ */
+ public function invalidateCaches(): void
+ {
+ // Purge page cache shared by sessions.
+ $this->purgeCachedPages();
+ }
+
+ /**
+ * 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,
+ $validityPeriod
+ );
+ }
+}
diff --git a/application/render/TemplatePage.php b/application/render/TemplatePage.php
new file mode 100644
index 00000000..03b424f3
--- /dev/null
+++ b/application/render/TemplatePage.php
@@ -0,0 +1,34 @@
+trustedProxies = $trustedProxies;
$this->nbAttempts = $nbAttempts;
$this->banDuration = $banDuration;
$this->banFile = $banFile;
- $this->logFile = $logFile;
+ $this->logger = $logger;
+
$this->readBanFile();
}
@@ -78,11 +80,7 @@ public function handleFailedAttempt($server)
if ($this->failures[$ip] >= $this->nbAttempts) {
$this->bans[$ip] = time() + $this->banDuration;
- logm(
- $this->logFile,
- $server['REMOTE_ADDR'],
- 'IP address banned from login: '. $ip
- );
+ $this->logger->info(format_log('IP address banned from login: ' . $ip, $ip));
}
$this->writeBanFile();
}
@@ -138,7 +136,7 @@ public function isBanned($server)
unset($this->failures[$ip]);
}
unset($this->bans[$ip]);
- logm($this->logFile, $server['REMOTE_ADDR'], 'Ban lifted for: '. $ip);
+ $this->logger->info(format_log('Ban lifted for: ' . $ip, $ip));
$this->writeBanFile();
return false;
diff --git a/application/security/CookieManager.php b/application/security/CookieManager.php
new file mode 100644
index 00000000..cde4746e
--- /dev/null
+++ b/application/security/CookieManager.php
@@ -0,0 +1,33 @@
+cookies = $cookies;
+ }
+
+ public function setCookieParameter(string $key, string $value, int $expires, string $path): self
+ {
+ $this->cookies[$key] = $value;
+
+ setcookie($key, $value, $expires, $path);
+
+ return $this;
+ }
+
+ public function getCookieParameter(string $key, string $default = null): ?string
+ {
+ return $this->cookies[$key] ?? $default;
+ }
+}
diff --git a/application/security/LoginManager.php b/application/security/LoginManager.php
index 0b0ce0b1..b795b80e 100644
--- a/application/security/LoginManager.php
+++ b/application/security/LoginManager.php
@@ -1,6 +1,9 @@
configManager = $configManager;
$this->sessionManager = $sessionManager;
- $this->banManager = new BanManager(
- $this->configManager->get('security.trusted_proxies', []),
- $this->configManager->get('security.ban_after'),
- $this->configManager->get('security.ban_duration'),
- $this->configManager->get('resource.ban_file', 'data/ipbans.php'),
- $this->configManager->get('resource.log')
- );
+ $this->cookieManager = $cookieManager;
+ $this->banManager = $banManager;
+ $this->logger = $logger;
if ($this->configManager->get('security.open_shaarli') === true) {
$this->openShaarli = true;
@@ -85,10 +93,9 @@ public function getStaySignedInToken()
/**
* Check user session state and validity (expiration)
*
- * @param array $cookie The $_COOKIE array
* @param string $clientIpId Client IP address identifier
*/
- public function checkLoginState($cookie, $clientIpId)
+ public function checkLoginState($clientIpId)
{
if (! $this->configManager->exists('credentials.login')) {
// Shaarli is not configured yet
@@ -96,13 +103,12 @@ public function checkLoginState($cookie, $clientIpId)
return;
}
- if (isset($cookie[self::$STAY_SIGNED_IN_COOKIE])
- && $cookie[self::$STAY_SIGNED_IN_COOKIE] === $this->staySignedInToken
- ) {
+ if ($this->staySignedInToken === $this->cookieManager->getCookieParameter(CookieManager::STAY_SIGNED_IN)) {
// The user client has a valid stay-signed-in cookie
// Session information is updated with the current client information
$this->sessionManager->storeLoginInfo($clientIpId);
- } elseif ($this->sessionManager->hasSessionExpired()
+ } elseif (
+ $this->sessionManager->hasSessionExpired()
|| $this->sessionManager->hasClientIpChanged($clientIpId)
) {
$this->sessionManager->logout();
@@ -119,7 +125,7 @@ public function checkLoginState($cookie, $clientIpId)
*
* @return true when the user is logged in, false otherwise
*/
- public function isLoggedIn()
+ public function isLoggedIn(): bool
{
if ($this->openShaarli) {
return true;
@@ -130,6 +136,58 @@ public function isLoggedIn()
/**
* Check user credentials are valid
*
+ * @param string $clientIpId Client IP address identifier
+ * @param string $login Username
+ * @param string $password Password
+ *
+ * @return bool true if the provided credentials are valid, false otherwise
+ */
+ public function checkCredentials($clientIpId, $login, $password)
+ {
+ // Check credentials
+ try {
+ $useLdapLogin = !empty($this->configManager->get('ldap.host'));
+ if (
+ $login === $this->configManager->get('credentials.login')
+ && (
+ (false === $useLdapLogin && $this->checkCredentialsFromLocalConfig($login, $password))
+ || (true === $useLdapLogin && $this->checkCredentialsFromLdap($login, $password))
+ )
+ ) {
+ $this->sessionManager->storeLoginInfo($clientIpId);
+ $this->logger->info(format_log('Login successful', $clientIpId));
+
+ return true;
+ }
+ } catch (Exception $exception) {
+ $this->logger->info(format_log('Exception while checking credentials: ' . $exception, $clientIpId));
+ }
+
+ $this->logger->info(format_log('Login failed for user ' . $login, $clientIpId));
+
+ return false;
+ }
+
+
+ /**
+ * Check user credentials from local config
+ *
+ * @param string $login Username
+ * @param string $password Password
+ *
+ * @return bool true if the provided credentials are valid, false otherwise
+ */
+ public function checkCredentialsFromLocalConfig($login, $password)
+ {
+ $hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
+
+ return $login == $this->configManager->get('credentials.login')
+ && $hash == $this->configManager->get('credentials.hash');
+ }
+
+ /**
+ * Check user credentials are valid through LDAP bind
+ *
* @param string $remoteIp Remote client IP address
* @param string $clientIpId Client IP address identifier
* @param string $login Username
@@ -137,28 +195,24 @@ public function isLoggedIn()
*
* @return bool true if the provided credentials are valid, false otherwise
*/
- public function checkCredentials($remoteIp, $clientIpId, $login, $password)
+ public function checkCredentialsFromLdap($login, $password, $connect = null, $bind = null)
{
- $hash = sha1($password . $login . $this->configManager->get('credentials.salt'));
+ $connect = $connect ?? function ($host) {
+ $resource = ldap_connect($host);
- if ($login != $this->configManager->get('credentials.login')
- || $hash != $this->configManager->get('credentials.hash')
- ) {
- logm(
- $this->configManager->get('resource.log'),
- $remoteIp,
- 'Login failed for user ' . $login
- );
- return false;
- }
+ ldap_set_option($resource, LDAP_OPT_PROTOCOL_VERSION, 3);
- $this->sessionManager->storeLoginInfo($clientIpId);
- logm(
- $this->configManager->get('resource.log'),
- $remoteIp,
- 'Login successful'
+ return $resource;
+ };
+ $bind = $bind ?? function ($handle, $dn, $password) {
+ return ldap_bind($handle, $dn, $password);
+ };
+
+ return $bind(
+ $connect($this->configManager->get('ldap.host')),
+ sprintf($this->configManager->get('ldap.dn'), $login),
+ $password
);
- return true;
}
/**
diff --git a/application/security/SessionManager.php b/application/security/SessionManager.php
index b8b8ab8d..ca186626 100644
--- a/application/security/SessionManager.php
+++ b/application/security/SessionManager.php
@@ -1,4 +1,5 @@
session = &$session;
$this->conf = $conf;
+ $this->savePath = $savePath;
+ }
+
+ /**
+ * Initialize XSRF token and links per page session variables.
+ */
+ public function initialize(): void
+ {
+ if (!isset($this->session['tokens'])) {
+ $this->session['tokens'] = [];
+ }
+
+ if (!isset($this->session['LINKS_PER_PAGE'])) {
+ $this->session['LINKS_PER_PAGE'] = $this->conf->get('general.links_per_page', 20);
+ }
}
/**
@@ -52,7 +80,7 @@ public function setStaySignedIn($staySignedIn)
*/
public function generateToken()
{
- $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
+ $token = sha1(uniqid('', true) . '_' . mt_rand() . $this->conf->get('credentials.salt'));
$this->session['tokens'][$token] = 1;
return $token;
}
@@ -156,7 +184,6 @@ public function logout()
unset($this->session['expires_on']);
unset($this->session['username']);
unset($this->session['visibility']);
- unset($this->session['untaggedonly']);
}
}
@@ -196,4 +223,87 @@ public function hasClientIpChanged($clientIpId)
}
return true;
}
+
+ /** @return array Local reference to the global $_SESSION array */
+ public function getSession(): array
+ {
+ return $this->session;
+ }
+
+ /**
+ * @param mixed $default value which will be returned if the $key is undefined
+ *
+ * @return mixed Content stored in session
+ */
+ public function getSessionParameter(string $key, $default = null)
+ {
+ return $this->session[$key] ?? $default;
+ }
+
+ /**
+ * Store a variable in user session.
+ *
+ * @param string $key Session key
+ * @param mixed $value Session value to store
+ *
+ * @return $this
+ */
+ public function setSessionParameter(string $key, $value): self
+ {
+ $this->session[$key] = $value;
+
+ return $this;
+ }
+
+ /**
+ * Delete a variable in user session.
+ *
+ * @param string $key Session key
+ *
+ * @return $this
+ */
+ public function deleteSessionParameter(string $key): self
+ {
+ unset($this->session[$key]);
+
+ return $this;
+ }
+
+ public function getSavePath(): string
+ {
+ return $this->savePath;
+ }
+
+ /*
+ * Next public functions wrapping native PHP session API.
+ */
+
+ public function destroy(): bool
+ {
+ $this->session = [];
+
+ return session_destroy();
+ }
+
+ public function start(): bool
+ {
+ if (session_status() === PHP_SESSION_ACTIVE) {
+ $this->destroy();
+ }
+
+ return session_start();
+ }
+
+ /**
+ * Be careful, return type of session_set_cookie_params() changed between PHP 7.1 and 7.2.
+ */
+ public function cookieParameters(int $lifeTime, string $path, string $domain): void
+ {
+ session_set_cookie_params($lifeTime, $path, $domain);
+ }
+
+ public function regenerateId(bool $deleteOldSession = false): bool
+ {
+ return session_regenerate_id($deleteOldSession);
+ }
}
diff --git a/application/updater/Updater.php b/application/updater/Updater.php
index beb9ea9b..11b6c051 100644
--- a/application/updater/Updater.php
+++ b/application/updater/Updater.php
@@ -2,25 +2,14 @@
namespace Shaarli\Updater;
-use Exception;
-use RainTPL;
-use ReflectionClass;
-use ReflectionException;
-use ReflectionMethod;
-use Shaarli\ApplicationUtils;
-use Shaarli\Bookmark\LinkDB;
-use Shaarli\Bookmark\LinkFilter;
-use Shaarli\Config\ConfigJson;
+use Shaarli\Bookmark\BookmarkServiceInterface;
use Shaarli\Config\ConfigManager;
-use Shaarli\Config\ConfigPhp;
-use Shaarli\Exceptions\IOException;
-use Shaarli\Thumbnailer;
use Shaarli\Updater\Exception\UpdaterException;
/**
- * Class updater.
+ * Class Updater.
* Used to update stuff when a new Shaarli's version is reached.
- * Update methods are ran only once, and the stored in a JSON file.
+ * Update methods are ran only once, and the stored in a TXT file.
*/
class Updater
{
@@ -30,9 +19,9 @@ class Updater
protected $doneUpdates;
/**
- * @var LinkDB instance.
+ * @var BookmarkServiceInterface instance.
*/
- protected $linkDB;
+ protected $bookmarkService;
/**
* @var ConfigManager $conf Configuration Manager instance.
@@ -45,36 +34,32 @@ class Updater
protected $isLoggedIn;
/**
- * @var array $_SESSION
- */
- protected $session;
-
- /**
- * @var ReflectionMethod[] List of current class methods.
+ * @var \ReflectionMethod[] List of current class methods.
*/
protected $methods;
+ /**
+ * @var string $basePath Shaarli root directory (from HTTP Request)
+ */
+ protected $basePath = null;
+
/**
* Object constructor.
*
- * @param array $doneUpdates Updates which are already done.
- * @param LinkDB $linkDB LinkDB instance.
- * @param ConfigManager $conf Configuration Manager instance.
- * @param boolean $isLoggedIn True if the user is logged in.
- * @param array $session $_SESSION (by reference)
- *
- * @throws ReflectionException
+ * @param array $doneUpdates Updates which are already done.
+ * @param BookmarkServiceInterface $linkDB LinksService instance.
+ * @param ConfigManager $conf Configuration Manager instance.
+ * @param boolean $isLoggedIn True if the user is logged in.
*/
- public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session = [])
+ public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn)
{
$this->doneUpdates = $doneUpdates;
- $this->linkDB = $linkDB;
+ $this->bookmarkService = $linkDB;
$this->conf = $conf;
$this->isLoggedIn = $isLoggedIn;
- $this->session = &$session;
// Retrieve all update methods.
- $class = new ReflectionClass($this);
+ $class = new \ReflectionClass($this);
$this->methods = $class->getMethods();
}
@@ -82,13 +67,15 @@ public function __construct($doneUpdates, $linkDB, $conf, $isLoggedIn, &$session
* Run all new updates.
* Update methods have to start with 'updateMethod' and return true (on success).
*
+ * @param string $basePath Shaarli root directory (from HTTP Request)
+ *
* @return array An array containing ran updates.
*
* @throws UpdaterException If something went wrong.
*/
- public function update()
+ public function update(string $basePath = null)
{
- $updatesRan = array();
+ $updatesRan = [];
// If the user isn't logged in, exit without updating.
if ($this->isLoggedIn !== true) {
@@ -96,12 +83,13 @@ public function update()
}
if ($this->methods === null) {
- throw new UpdaterException(t('Couldn\'t retrieve updater class methods.'));
+ throw new UpdaterException('Couldn\'t retrieve LegacyUpdater class methods.');
}
foreach ($this->methods as $method) {
// Not an update method or already done, pass.
- if (!startsWith($method->getName(), 'updateMethod')
+ if (
+ ! startsWith($method->getName(), 'updateMethod')
|| in_array($method->getName(), $this->doneUpdates)
) {
continue;
@@ -114,7 +102,7 @@ public function update()
if ($res === true) {
$updatesRan[] = $method->getName();
}
- } catch (Exception $e) {
+ } catch (\Exception $e) {
throw new UpdaterException($method, $e);
}
}
@@ -132,431 +120,62 @@ public function getDoneUpdates()
return $this->doneUpdates;
}
- /**
- * Move deprecated options.php to config.php.
- *
- * Milestone 0.9 (old versioning) - shaarli/Shaarli#41:
- * options.php is not supported anymore.
- */
- public function updateMethodMergeDeprecatedConfigFile()
+ public function readUpdates(string $updatesFilepath): array
{
- if (is_file($this->conf->get('resource.data_dir') . '/options.php')) {
- include $this->conf->get('resource.data_dir') . '/options.php';
+ return UpdaterUtils::readUpdatesFile($updatesFilepath);
+ }
- // Load GLOBALS into config
- $allowedKeys = array_merge(ConfigPhp::$ROOT_KEYS);
- $allowedKeys[] = 'config';
- foreach ($GLOBALS as $key => $value) {
- if (in_array($key, $allowedKeys)) {
- $this->conf->set($key, $value);
- }
- }
- $this->conf->write($this->isLoggedIn);
- unlink($this->conf->get('resource.data_dir') . '/options.php');
+ public function writeUpdates(string $updatesFilepath, array $updates): void
+ {
+ UpdaterUtils::writeUpdatesFile($updatesFilepath, $updates);
+ }
+
+ /**
+ * With the Slim routing system, default header link should be `/subfolder/` instead of `?`.
+ * Otherwise you can not go back to the home page.
+ * Example: `/subfolder/picture-wall` -> `/subfolder/picture-wall?` instead of `/subfolder/`.
+ */
+ public function updateMethodRelativeHomeLink(): bool
+ {
+ if ('?' === trim($this->conf->get('general.header_link'))) {
+ $this->conf->set('general.header_link', $this->basePath . '/', true, true);
}
return true;
}
/**
- * Move old configuration in PHP to the new config system in JSON format.
- *
- * Will rename 'config.php' into 'config.save.php' and create 'config.json.php'.
- * It will also convert legacy setting keys to the new ones.
+ * With the Slim routing system, note bookmarks URL formatted `?abcdef`
+ * should be replaced with `/shaare/abcdef`
*/
- public function updateMethodConfigToJson()
+ public function updateMethodMigrateExistingNotesUrl(): bool
{
- // JSON config already exists, nothing to do.
- if ($this->conf->getConfigIO() instanceof ConfigJson) {
- return true;
- }
+ $updated = false;
- $configPhp = new ConfigPhp();
- $configJson = new ConfigJson();
- $oldConfig = $configPhp->read($this->conf->getConfigFile() . '.php');
- rename($this->conf->getConfigFileExt(), $this->conf->getConfigFile() . '.save.php');
- $this->conf->setConfigIO($configJson);
- $this->conf->reload();
+ foreach ($this->bookmarkService->search()->getBookmarks() as $bookmark) {
+ if (
+ $bookmark->isNote()
+ && startsWith($bookmark->getUrl(), '?')
+ && 1 === preg_match('/^\?([a-zA-Z0-9-_@]{6})($|&|#)/', $bookmark->getUrl(), $match)
+ ) {
+ $updated = true;
+ $bookmark = $bookmark->setUrl('/shaare/' . $match[1]);
- $legacyMap = array_flip(ConfigPhp::$LEGACY_KEYS_MAPPING);
- foreach (ConfigPhp::$ROOT_KEYS as $key) {
- $this->conf->set($legacyMap[$key], $oldConfig[$key]);
- }
-
- // Set sub config keys (config and plugins)
- $subConfig = array('config', 'plugins');
- foreach ($subConfig as $sub) {
- foreach ($oldConfig[$sub] as $key => $value) {
- if (isset($legacyMap[$sub . '.' . $key])) {
- $configKey = $legacyMap[$sub . '.' . $key];
- } else {
- $configKey = $sub . '.' . $key;
- }
- $this->conf->set($configKey, $value);
+ $this->bookmarkService->set($bookmark, false);
}
}
- try {
- $this->conf->write($this->isLoggedIn);
- return true;
- } catch (IOException $e) {
- error_log($e->getMessage());
- return false;
- }
- }
-
- /**
- * Escape settings which have been manually escaped in every request in previous versions:
- * - general.title
- * - general.header_link
- * - redirector.url
- *
- * @return bool true if the update is successful, false otherwise.
- */
- public function updateMethodEscapeUnescapedConfig()
- {
- try {
- $this->conf->set('general.title', escape($this->conf->get('general.title')));
- $this->conf->set('general.header_link', escape($this->conf->get('general.header_link')));
- $this->conf->write($this->isLoggedIn);
- } catch (Exception $e) {
- error_log($e->getMessage());
- return false;
- }
- return true;
- }
-
- /**
- * Update the database to use the new ID system, which replaces linkdate primary keys.
- * Also, creation and update dates are now DateTime objects (done by LinkDB).
- *
- * Since this update is very sensitve (changing the whole database), the datastore will be
- * automatically backed up into the file datastore..php.
- *
- * LinkDB also adds the field 'shorturl' with the precedent format (linkdate smallhash),
- * which will be saved by this method.
- *
- * @return bool true if the update is successful, false otherwise.
- */
- public function updateMethodDatastoreIds()
- {
- // up to date database
- if (isset($this->linkDB[0])) {
- return true;
- }
-
- $save = $this->conf->get('resource.data_dir') . '/datastore.' . date('YmdHis') . '.php';
- copy($this->conf->get('resource.datastore'), $save);
-
- $links = array();
- foreach ($this->linkDB as $offset => $value) {
- $links[] = $value;
- unset($this->linkDB[$offset]);
- }
- $links = array_reverse($links);
- $cpt = 0;
- foreach ($links as $l) {
- unset($l['linkdate']);
- $l['id'] = $cpt;
- $this->linkDB[$cpt++] = $l;
- }
-
- $this->linkDB->save($this->conf->get('resource.page_cache'));
- $this->linkDB->reorder();
-
- return true;
- }
-
- /**
- * Rename tags starting with a '-' to work with tag exclusion search.
- */
- public function updateMethodRenameDashTags()
- {
- $linklist = $this->linkDB->filterSearch();
- foreach ($linklist as $key => $link) {
- $link['tags'] = preg_replace('/(^| )\-/', '$1', $link['tags']);
- $link['tags'] = implode(' ', array_unique(LinkFilter::tagsStrToArray($link['tags'], true)));
- $this->linkDB[$key] = $link;
- }
- $this->linkDB->save($this->conf->get('resource.page_cache'));
- return true;
- }
-
- /**
- * Initialize API settings:
- * - api.enabled: true
- * - api.secret: generated secret
- */
- public function updateMethodApiSettings()
- {
- if ($this->conf->exists('api.secret')) {
- return true;
- }
-
- $this->conf->set('api.enabled', true);
- $this->conf->set(
- 'api.secret',
- generate_api_secret(
- $this->conf->get('credentials.login'),
- $this->conf->get('credentials.salt')
- )
- );
- $this->conf->write($this->isLoggedIn);
- return true;
- }
-
- /**
- * New setting: theme name. If the default theme is used, nothing to do.
- *
- * If the user uses a custom theme, raintpl_tpl dir is updated to the parent directory,
- * and the current theme is set as default in the theme setting.
- *
- * @return bool true if the update is successful, false otherwise.
- */
- public function updateMethodDefaultTheme()
- {
- // raintpl_tpl isn't the root template directory anymore.
- // We run the update only if this folder still contains the template files.
- $tplDir = $this->conf->get('resource.raintpl_tpl');
- $tplFile = $tplDir . '/linklist.html';
- if (!file_exists($tplFile)) {
- return true;
- }
-
- $parent = dirname($tplDir);
- $this->conf->set('resource.raintpl_tpl', $parent);
- $this->conf->set('resource.theme', trim(str_replace($parent, '', $tplDir), '/'));
- $this->conf->write($this->isLoggedIn);
-
- // Dependency injection gore
- RainTPL::$tpl_dir = $tplDir;
-
- return true;
- }
-
- /**
- * Move the file to inc/user.css to data/user.css.
- *
- * Note: Due to hardcoded paths, it's not unit testable. But one line of code should be fine.
- *
- * @return bool true if the update is successful, false otherwise.
- */
- public function updateMethodMoveUserCss()
- {
- if (!is_file('inc/user.css')) {
- return true;
- }
-
- return rename('inc/user.css', 'data/user.css');
- }
-
- /**
- * * `markdown_escape` is a new setting, set to true as default.
- *
- * If the markdown plugin was already enabled, escaping is disabled to avoid
- * breaking existing entries.
- */
- public function updateMethodEscapeMarkdown()
- {
- if ($this->conf->exists('security.markdown_escape')) {
- return true;
- }
-
- if (in_array('markdown', $this->conf->get('general.enabled_plugins'))) {
- $this->conf->set('security.markdown_escape', false);
- } else {
- $this->conf->set('security.markdown_escape', true);
- }
- $this->conf->write($this->isLoggedIn);
-
- return true;
- }
-
- /**
- * Add 'http://' to Piwik URL the setting is set.
- *
- * @return bool true if the update is successful, false otherwise.
- */
- public function updateMethodPiwikUrl()
- {
- if (!$this->conf->exists('plugins.PIWIK_URL') || startsWith($this->conf->get('plugins.PIWIK_URL'), 'http')) {
- return true;
- }
-
- $this->conf->set('plugins.PIWIK_URL', 'http://' . $this->conf->get('plugins.PIWIK_URL'));
- $this->conf->write($this->isLoggedIn);
-
- return true;
- }
-
- /**
- * Use ATOM feed as default.
- */
- public function updateMethodAtomDefault()
- {
- if (!$this->conf->exists('feed.show_atom') || $this->conf->get('feed.show_atom') === true) {
- return true;
- }
-
- $this->conf->set('feed.show_atom', true);
- $this->conf->write($this->isLoggedIn);
-
- return true;
- }
-
- /**
- * Update updates.check_updates_branch setting.
- *
- * If the current major version digit matches the latest branch
- * major version digit, we set the branch to `latest`,
- * otherwise we'll check updates on the `stable` branch.
- *
- * No update required for the dev version.
- *
- * Note: due to hardcoded URL and lack of dependency injection, this is not unit testable.
- *
- * FIXME! This needs to be removed when we switch to first digit major version
- * instead of the second one since the versionning process will change.
- */
- public function updateMethodCheckUpdateRemoteBranch()
- {
- if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
- return true;
- }
-
- // Get latest branch major version digit
- $latestVersion = ApplicationUtils::getLatestGitVersionCode(
- 'https://raw.githubusercontent.com/shaarli/Shaarli/latest/shaarli_version.php',
- 5
- );
- if (preg_match('/(\d+)\.\d+$/', $latestVersion, $matches) === false) {
- return false;
- }
- $latestMajor = $matches[1];
-
- // Get current major version digit
- preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
- $currentMajor = $matches[1];
-
- if ($currentMajor === $latestMajor) {
- $branch = 'latest';
- } else {
- $branch = 'stable';
- }
- $this->conf->set('updates.check_updates_branch', $branch);
- $this->conf->write($this->isLoggedIn);
- return true;
- }
-
- /**
- * Reset history store file due to date format change.
- */
- public function updateMethodResetHistoryFile()
- {
- if (is_file($this->conf->get('resource.history'))) {
- unlink($this->conf->get('resource.history'));
- }
- return true;
- }
-
- /**
- * Save the datastore -> the link order is now applied when links are saved.
- */
- public function updateMethodReorderDatastore()
- {
- $this->linkDB->save($this->conf->get('resource.page_cache'));
- return true;
- }
-
- /**
- * Change privateonly session key to visibility.
- */
- public function updateMethodVisibilitySession()
- {
- if (isset($_SESSION['privateonly'])) {
- unset($_SESSION['privateonly']);
- $_SESSION['visibility'] = 'private';
- }
- return true;
- }
-
- /**
- * Add download size and timeout to the configuration file
- *
- * @return bool true if the update is successful, false otherwise.
- */
- public function updateMethodDownloadSizeAndTimeoutConf()
- {
- if ($this->conf->exists('general.download_max_size')
- && $this->conf->exists('general.download_timeout')
- ) {
- return true;
- }
-
- if (!$this->conf->exists('general.download_max_size')) {
- $this->conf->set('general.download_max_size', 1024 * 1024 * 4);
- }
-
- if (!$this->conf->exists('general.download_timeout')) {
- $this->conf->set('general.download_timeout', 30);
- }
-
- $this->conf->write($this->isLoggedIn);
- return true;
- }
-
- /**
- * * Move thumbnails management to WebThumbnailer, coming with new settings.
- */
- public function updateMethodWebThumbnailer()
- {
- if ($this->conf->exists('thumbnails.mode')) {
- return true;
- }
-
- $thumbnailsEnabled = extension_loaded('gd') && $this->conf->get('thumbnail.enable_thumbnails', true);
- $this->conf->set('thumbnails.mode', $thumbnailsEnabled ? Thumbnailer::MODE_ALL : Thumbnailer::MODE_NONE);
- $this->conf->set('thumbnails.width', 125);
- $this->conf->set('thumbnails.height', 90);
- $this->conf->remove('thumbnail');
- $this->conf->write(true);
-
- if ($thumbnailsEnabled) {
- $this->session['warnings'][] = t(
- 'You have enabled or changed thumbnails mode. Please synchronize them.'
- );
+ if ($updated) {
+ $this->bookmarkService->save();
}
return true;
}
- /**
- * Set sticky = false on all links
- *
- * @return bool true if the update is successful, false otherwise.
- */
- public function updateMethodSetSticky()
+ public function setBasePath(string $basePath): self
{
- foreach ($this->linkDB as $key => $link) {
- if (isset($link['sticky'])) {
- return true;
- }
- $link['sticky'] = false;
- $this->linkDB[$key] = $link;
- }
+ $this->basePath = $basePath;
- $this->linkDB->save($this->conf->get('resource.page_cache'));
-
- return true;
- }
-
- /**
- * Remove redirector settings.
- */
- public function updateMethodRemoveRedirector()
- {
- $this->conf->remove('redirector');
- $this->conf->write(true);
- return true;
+ return $this;
}
}
diff --git a/application/updater/UpdaterUtils.php b/application/updater/UpdaterUtils.php
index 34d4f422..206f826e 100644
--- a/application/updater/UpdaterUtils.php
+++ b/application/updater/UpdaterUtils.php
@@ -1,39 +1,44 @@
+ * whom created the CSS which this file is based on.
+ * License: Unlicense
+ */
+
+.markdown p{
+ margin:0.75em 0;
+}
+
+.markdown img{
+ max-width:100%;
+}
+
+.markdown h1, .markdown h2, .markdown h3, .markdown h4, .markdown h5, .markdown h6{
+ font-weight:normal;
+ font-style:normal;
+ line-height:1em;
+ margin:0.75em 0;
+}
+.markdown h4, .markdown h5, .markdown h6{ font-weight: bold; }
+.markdown h1{ font-size:2.5em; }
+.markdown h2{ font-size:2em; }
+.markdown h3{ font-size:1.5em; }
+.markdown h4{ font-size:1.2em; }
+.markdown h5{ font-size:1em; }
+.markdown h6{ font-size:0.9em; }
+
+.markdown blockquote{
+ color:#666666;
+ padding-left: 3em;
+ border-left: 0.5em #EEE solid;
+ margin:0.75em 0;
+}
+.markdown hr { display: block; height: 2px; border: 0; border-top: 1px solid #aaa;border-bottom: 1px solid #eee; margin: 1em 0; padding: 0; }
+.markdown pre, .markdown code, .markdown kbd, .markdown samp {
+ font-family: monospace, 'courier new';
+ font-size: 0.98em;
+}
+.markdown pre { white-space: pre; white-space: pre-wrap; word-wrap: break-word; }
+
+.markdown b, .markdown strong { font-weight: bold; }
+
+.markdown dfn, .markdown em { font-style: italic; }
+
+.markdown ins { background: #ff9; color: #000; text-decoration: none; }
+
+.markdown mark { background: #ff0; color: #000; font-style: italic; font-weight: bold; }
+
+.markdown sub, .markdown sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
+.markdown sup { top: -0.5em; }
+.markdown sub { bottom: -0.25em; }
+
+.markdown ul, .markdown ol { margin: 1em 0; padding: 0 0 0 2em; }
+.markdown li p:last-child { margin:0 }
+.markdown dd { margin: 0 0 0 2em; }
+
+.markdown img { border: 0; -ms-interpolation-mode: bicubic; vertical-align: middle; }
+
+.markdown table { border-collapse: collapse; border-spacing: 0; }
+.markdown td { vertical-align: top; }
+
+@media only screen and (min-width: 480px) {
+ .markdown {font-size:0.9em;}
+}
+
+@media only screen and (min-width: 768px) {
+ .markdown {font-size:1em;}
+}
+
+#linklist .markdown li {
+ padding: 0;
+ border: none;
+ background: none;
+}
+
+#linklist .markdown ul li {
+ list-style: circle;
+}
+
+#linklist .markdown ol li {
+ list-style: decimal;
+}
+
+.markdown table {
+ padding: 0;
+}
+.markdown table tr {
+ border-top: 1px solid #cccccc;
+ background-color: white;
+ margin: 0;
+ padding: 0;
+}
+.markdown table tr:nth-child(2n) {
+ background-color: #f8f8f8;
+}
+.markdown table tr th {
+ font-weight: bold;
+ border: 1px solid #cccccc;
+ text-align: left;
+ margin: 0;
+ padding: 6px 13px;
+}
+.markdown table tr td {
+ border: 1px solid #cccccc;
+ text-align: left;
+ margin: 0;
+ padding: 6px 13px;
+}
+.markdown table tr th :first-child, .markdown table tr td :first-child {
+ margin-top: 0;
+}
+.markdown table tr th :last-child, table tr td :last-child {
+ margin-bottom: 0;
+}
+
+.markdown pre {
+ background-color: #eee;
+ padding: 4px 9px;
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+ border-radius: 5px;
+ overflow: auto;
+ box-shadow: 0 -1px 0 #e5e5e5,0 0 1px rgba(0,0,0,0.12),0 1px 2px rgba(0,0,0,0.24);
+}
+
+.markdown pre code {
+ color: black;
+ font-family: 'Consolas', 'Monaco', 'Andale Mono', monospace;
+ direction: ltr;
+ text-align: left;
+ white-space: pre;
+ word-spacing: normal;
+ word-break: normal;
+ line-height: 1.7;
+ font-size: 11.5px;
+ -moz-tab-size: 4;
+ -o-tab-size: 4;
+ tab-size: 4;
+ -webkit-hyphens: none;
+ -moz-hyphens: none;
+ -ms-hyphens: none;
+ hyphens: none;
+}
+
+.markdown :not(pre) code {
+ background-color: #eee;
+ padding: 1px 3px;
+ border-radius: 1px;
+ box-shadow: 0 -1px 0 #e5e5e5,0 0 1px rgba(0,0,0,0.12),0 1px 1px rgba(0,0,0,0.24);
+}
+
+#pageheader .md_help {
+ color: white;
+}
+
+/*
+ Remove header bookmarks style
+ */
+#pageheader .md_help a {
+ color: lightgray;
+ font-weight: bold;
+ text-decoration: underline;
+
+ background: none;
+ box-shadow: none;
+ padding: 0;
+ margin: 0;
+}
+
+#pageheader .md_help a:hover {
+ color: white;
+}
diff --git a/assets/common/js/metadata.js b/assets/common/js/metadata.js
new file mode 100644
index 00000000..d5a28a35
--- /dev/null
+++ b/assets/common/js/metadata.js
@@ -0,0 +1,107 @@
+import he from 'he';
+
+/**
+ * This script is used to retrieve bookmarks metadata asynchronously:
+ * - title, description and keywords while creating a new bookmark
+ * - thumbnails while visiting the bookmark list
+ *
+ * Note: it should only be included if the user is logged in
+ * and the setting general.enable_async_metadata is enabled.
+ */
+
+/**
+ * Removes given input loaders - used in edit link template.
+ *
+ * @param {object} loaders List of input DOM element that need to be cleared
+ */
+function clearLoaders(loaders) {
+ if (loaders != null && loaders.length > 0) {
+ [...loaders].forEach((loader) => {
+ loader.classList.remove('loading-input');
+ });
+ }
+}
+
+/**
+ * AJAX request to update the thumbnail of a bookmark with the provided ID.
+ * If a thumbnail is retrieved, it updates the divElement with the image src, and displays it.
+ *
+ * @param {string} basePath Shaarli subfolder for XHR requests
+ * @param {object} divElement Main DOM element containing the thumbnail placeholder
+ * @param {int} id Bookmark ID to update
+ */
+function updateThumb(basePath, divElement, id) {
+ const xhr = new XMLHttpRequest();
+ xhr.open('PATCH', `${basePath}/admin/shaare/${id}/update-thumbnail`);
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+ xhr.responseType = 'json';
+ xhr.onload = () => {
+ if (xhr.status !== 200) {
+ alert(`An error occurred. Return code: ${xhr.status}`);
+ } else {
+ const { response } = xhr;
+
+ if (response.thumbnail !== false) {
+ const imgElement = divElement.querySelector('img');
+
+ imgElement.src = response.thumbnail;
+ imgElement.dataset.src = response.thumbnail;
+ imgElement.style.opacity = '1';
+ divElement.classList.remove('hidden');
+ }
+ }
+ };
+ xhr.send();
+}
+
+(() => {
+ const basePath = document.querySelector('input[name="js_base_path"]').value;
+
+ /*
+ * METADATA FOR EDIT BOOKMARK PAGE
+ */
+ const inputTitles = document.querySelectorAll('input[name="lf_title"]');
+ if (inputTitles != null) {
+ [...inputTitles].forEach((inputTitle) => {
+ const form = inputTitle.closest('form[name="linkform"]');
+ const loaders = form.querySelectorAll('.loading-input');
+
+ if (inputTitle.value.length > 0) {
+ clearLoaders(loaders);
+ return;
+ }
+
+ const url = form.querySelector('input[name="lf_url"]').value;
+
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', `${basePath}/admin/metadata?url=${encodeURI(url)}`, true);
+ xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
+ xhr.onload = () => {
+ const result = JSON.parse(xhr.response);
+ Object.keys(result).forEach((key) => {
+ if (result[key] !== null && result[key].length) {
+ const element = form.querySelector(`input[name="lf_${key}"], textarea[name="lf_${key}"]`);
+ if (element != null && element.value.length === 0) {
+ element.value = he.decode(result[key]);
+ }
+ }
+ });
+ clearLoaders(loaders);
+ };
+
+ xhr.send();
+ });
+ }
+
+ /*
+ * METADATA FOR THUMBNAIL RETRIEVAL
+ */
+ const thumbsToLoad = document.querySelectorAll('div[data-async-thumbnail]');
+ if (thumbsToLoad != null) {
+ [...thumbsToLoad].forEach((divElement) => {
+ const { id } = divElement.closest('[data-id]').dataset;
+
+ updateThumb(basePath, divElement, id);
+ });
+ }
+})();
diff --git a/assets/common/js/shaare-batch.js b/assets/common/js/shaare-batch.js
new file mode 100644
index 00000000..6fc16faf
--- /dev/null
+++ b/assets/common/js/shaare-batch.js
@@ -0,0 +1,125 @@
+const sendBookmarkForm = (basePath, formElement) => {
+ const inputs = formElement
+ .querySelectorAll('input[type="text"], textarea, input[type="checkbox"], input[type="hidden"]');
+
+ const formData = new FormData();
+ [...inputs].forEach((input) => {
+ 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) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', `${basePath}/admin/shaare`);
+ xhr.onload = () => {
+ if (xhr.status !== 200) {
+ alert(`An error occurred. Return code: ${xhr.status}`);
+ reject();
+ } else {
+ formElement.closest('.edit-link-container').remove();
+ resolve();
+ }
+ };
+ xhr.send(formData);
+ });
+};
+
+const sendBookmarkDelete = (buttonElement, formElement) => (
+ new Promise((resolve, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', `${buttonElement.href}&source=batch`);
+ xhr.onload = () => {
+ if (xhr.status !== 204) {
+ alert(`An error occurred. Return code: ${xhr.status}`);
+ reject();
+ } else {
+ formElement.closest('.edit-link-container').remove();
+ resolve();
+ }
+ };
+ xhr.send();
+ })
+);
+
+const redirectIfEmptyBatch = (basePath, formElements, path) => {
+ if (formElements == null || formElements.length === 0) {
+ window.location.href = `${basePath}${path}`;
+ }
+};
+
+(() => {
+ const basePath = document.querySelector('input[name="js_base_path"]').value;
+ const getForms = () => document.querySelectorAll('form[name="linkform"]');
+
+ const cancelButtons = document.querySelectorAll('[name="cancel-batch-link"]');
+ if (cancelButtons != null) {
+ [...cancelButtons].forEach((cancelButton) => {
+ cancelButton.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.target.closest('form[name="linkform"]').remove();
+ redirectIfEmptyBatch(basePath, getForms(), '/admin/add-shaare');
+ });
+ });
+ }
+
+ const saveButtons = document.querySelectorAll('[name="save_edit"]');
+ if (saveButtons != null) {
+ [...saveButtons].forEach((saveButton) => {
+ saveButton.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const formElement = e.target.closest('form[name="linkform"]');
+ sendBookmarkForm(basePath, formElement)
+ .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
+ });
+ });
+ }
+
+ const saveAllButtons = document.querySelectorAll('[name="save_edit_batch"]');
+ if (saveAllButtons != null) {
+ [...saveAllButtons].forEach((saveAllButton) => {
+ saveAllButton.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const forms = [...getForms()];
+ const nbForm = forms.length;
+ let current = 0;
+ const progressBar = document.querySelector('.progressbar > div');
+ const progressBarCurrent = document.querySelector('.progressbar-current');
+
+ document.querySelector('.dark-layer').style.display = 'block';
+ document.querySelector('.progressbar-max').innerHTML = nbForm;
+ progressBarCurrent.innerHTML = current;
+
+ const promises = [];
+ forms.forEach((formElement) => {
+ promises.push(sendBookmarkForm(basePath, formElement).then(() => {
+ current += 1;
+ progressBar.style.width = `${(current * 100) / nbForm}%`;
+ progressBarCurrent.innerHTML = current;
+ }));
+ });
+
+ Promise.all(promises).then(() => {
+ window.location.href = `${basePath}/`;
+ });
+ });
+ });
+ }
+
+ const deleteButtons = document.querySelectorAll('[name="delete_link"]');
+ if (deleteButtons != null) {
+ [...deleteButtons].forEach((deleteButton) => {
+ deleteButton.addEventListener('click', (e) => {
+ e.preventDefault();
+
+ const formElement = e.target.closest('form[name="linkform"]');
+ sendBookmarkDelete(e.target, formElement)
+ .then(() => redirectIfEmptyBatch(basePath, getForms(), '/'));
+ });
+ });
+ }
+})();
diff --git a/assets/common/js/thumbnails-update.js b/assets/common/js/thumbnails-update.js
index b66ca3ae..3cd4c2a7 100644
--- a/assets/common/js/thumbnails-update.js
+++ b/assets/common/js/thumbnails-update.js
@@ -10,13 +10,14 @@
* It contains a recursive call to retrieve the thumb of the next link when it succeed.
* It also update the progress bar and other visual feedback elements.
*
+ * @param {string} basePath Shaarli subfolder for XHR requests
* @param {array} ids List of LinkID to update
* @param {int} i Current index in ids
* @param {object} elements List of DOM element to avoid retrieving them at each iteration
*/
-function updateThumb(ids, i, elements) {
+function updateThumb(basePath, ids, i, elements) {
const xhr = new XMLHttpRequest();
- xhr.open('POST', '?do=ajax_thumb_update');
+ xhr.open('PATCH', `${basePath}/admin/shaare/${ids[i]}/update-thumbnail`);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.responseType = 'json';
xhr.onload = () => {
@@ -29,17 +30,18 @@ function updateThumb(ids, i, elements) {
elements.current.innerHTML = i;
elements.title.innerHTML = response.title;
if (response.thumbnail !== false) {
- elements.thumbnail.innerHTML = ``;
+ elements.thumbnail.innerHTML = ``;
}
if (i < ids.length) {
- updateThumb(ids, i, elements);
+ updateThumb(basePath, ids, i, elements);
}
}
};
- xhr.send(`id=${ids[i]}`);
+ xhr.send();
}
(() => {
+ const basePath = document.querySelector('input[name="js_base_path"]').value;
const ids = document.getElementsByName('ids')[0].value.split(',');
const elements = {
progressBar: document.querySelector('.progressbar > div'),
@@ -47,5 +49,5 @@ function updateThumb(ids, i, elements) {
thumbnail: document.querySelector('.thumbnail-placeholder'),
title: document.querySelector('.thumbnail-link-title'),
};
- updateThumb(ids, 0, elements);
+ updateThumb(basePath, ids, 0, elements);
})();
diff --git a/assets/default/js/base.js b/assets/default/js/base.js
index d5c29c69..48291d52 100644
--- a/assets/default/js/base.js
+++ b/assets/default/js/base.js
@@ -1,4 +1,5 @@
import Awesomplete from 'awesomplete';
+import he from 'he';
/**
* Find a parent element according to its tag and its attributes
@@ -10,7 +11,7 @@ import Awesomplete from 'awesomplete';
* @returns Found element or null.
*/
function findParent(element, tagName, attributes) {
- const parentMatch = key => attributes[key] !== '' && element.getAttribute(key).indexOf(attributes[key]) !== -1;
+ const parentMatch = (key) => attributes[key] !== '' && element.getAttribute(key).indexOf(attributes[key]) !== -1;
while (element) {
if (element.tagName.toLowerCase() === tagName) {
if (Object.keys(attributes).find(parentMatch)) {
@@ -25,29 +26,48 @@ function findParent(element, tagName, attributes) {
/**
* Ajax request to refresh the CSRF token.
*/
-function refreshToken() {
+function refreshToken(basePath, callback) {
const xhr = new XMLHttpRequest();
- xhr.open('GET', '?do=token');
+ xhr.open('GET', `${basePath}/admin/token`);
xhr.onload = () => {
- const token = document.getElementById('token');
- token.setAttribute('value', xhr.responseText);
+ const elements = document.querySelectorAll('input[name="token"]');
+ [...elements].forEach((element) => {
+ element.setAttribute('value', xhr.responseText);
+ });
+
+ if (callback) {
+ callback(xhr.response);
+ }
};
xhr.send();
}
-function createAwesompleteInstance(element, tags = []) {
+function createAwesompleteInstance(element, separator, tags = []) {
const awesome = new Awesomplete(Awesomplete.$(element));
- // Tags are separated by a space
- awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]);
+
+ // Tags are separated by separator. Ignore leading search flags
+ awesome.filter = (text, input) => {
+ let filterFunc = Awesomplete.FILTER_CONTAINS;
+ let term = input.match(new RegExp(`[^${separator}]*$`))[0];
+ const termFlagged = term.replace(/^[-~+]/, '');
+ if (term !== termFlagged) {
+ term = termFlagged;
+ filterFunc = Awesomplete.FILTER_STARTSWITH;
+ }
+
+ return filterFunc(text, term);
+ };
+
// Insert new selected tag in the input
awesome.replace = (text) => {
- const before = awesome.input.value.match(/^.+ \s*|/)[0];
- awesome.input.value = `${before}${text} `;
+ const before = awesome.input.value.match(new RegExp(`^(.+${separator}+)?[-~+]?|`))[0];
+ awesome.input.value = `${before}${text}${separator}`;
};
// Highlight found items
- awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(/[^ ]*$/)[0]);
+ awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${separator}]*$`))[0]);
// Don't display already selected items
- const reg = /(\w+) /g;
+ // WARNING: pseudo classes does not seem to work with string litterals...
+ const reg = new RegExp(`([^${separator}]+)${separator}`, 'g');
let match;
awesome.data = (item, input) => {
while ((match = reg.exec(input))) {
@@ -71,13 +91,14 @@ function createAwesompleteInstance(element, tags = []) {
* @param selector CSS selector
* @param tags Array of tags
* @param instances List of existing awesomplete instances
+ * @param separator Tags separator character
*/
-function updateAwesompleteList(selector, tags, instances) {
+function updateAwesompleteList(selector, tags, instances, separator) {
if (instances.length === 0) {
// First load: create Awesomplete instances
const elements = document.querySelectorAll(selector);
[...elements].forEach((element) => {
- instances.push(createAwesompleteInstance(element, tags));
+ instances.push(createAwesompleteInstance(element, separator, tags));
});
} else {
// Update awesomplete tag list
@@ -89,15 +110,6 @@ function updateAwesompleteList(selector, tags, instances) {
return instances;
}
-/**
- * html_entities in JS
- *
- * @see http://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
- */
-function htmlEntities(str) {
- return str.replace(/[\u00A0-\u9999<>&]/gim, i => `${i.charCodeAt(0)};`);
-}
-
/**
* Add the class 'hidden' to city options not attached to the current selected continent.
*
@@ -188,8 +200,8 @@ function removeClass(element, classname) {
function init(description) {
function resize() {
/* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
- const scrollTop = window.pageYOffset ||
- (document.documentElement || document.body.parentNode || document.body).scrollTop;
+ const scrollTop = window.pageYOffset
+ || (document.documentElement || document.body.parentNode || document.body).scrollTop;
description.style.height = 'auto';
description.style.height = `${description.scrollHeight + 10}px`;
@@ -215,6 +227,10 @@ function init(description) {
}
(() => {
+ const basePath = document.querySelector('input[name="js_base_path"]').value;
+ const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
+ const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' ';
+
/**
* Handle responsive menu.
* Source: http://purecss.io/layouts/tucked-menu-vertical/
@@ -294,7 +310,8 @@ function init(description) {
const deleteLinks = document.querySelectorAll('.confirm-delete');
[...deleteLinks].forEach((deleteLink) => {
deleteLink.addEventListener('click', (event) => {
- if (!confirm(document.getElementById('translation-delete-link').innerHTML)) {
+ const type = event.currentTarget.getAttribute('data-type') || 'link';
+ if (!confirm(document.getElementById(`translation-delete-${type}`).innerHTML)) {
event.preventDefault();
}
});
@@ -366,6 +383,10 @@ function init(description) {
});
sub.classList.toggle('open');
+ const autofocus = sub.querySelector('.autofocus');
+ if (autofocus) {
+ autofocus.focus();
+ }
}
});
});
@@ -461,7 +482,7 @@ function init(description) {
});
if (window.confirm(message)) {
- window.location = `?delete_link&lf_linkdate=${ids.join('+')}&token=${token.value}`;
+ window.location = `${basePath}/admin/shaare/delete?id=${ids.join('+')}&token=${token.value}`;
}
});
}
@@ -482,12 +503,45 @@ function init(description) {
});
});
- const ids = links.map(item => item.id);
- window.location = `?change_visibility&token=${token.value}&newVisibility=${visibility}&ids=${ids.join('+')}`;
+ const ids = links.map((item) => item.id);
+ window.location = (
+ `${basePath}/admin/shaare/visibility?token=${token.value}&newVisibility=${visibility}&id=${ids.join('+')}`
+ );
});
});
}
+ ['add', 'delete'].forEach((action) => {
+ const subHeader = document.getElementById(`bulk-tag-action-${action}`);
+
+ if (subHeader) {
+ subHeader.querySelectorAll('a.button').forEach((link) => {
+ if (!link.classList.contains('action')) {
+ return;
+ }
+
+ subHeader.querySelector('input[name="tag"]').addEventListener('keypress', (event) => {
+ if (event.keyCode === 13) { // enter
+ link.click();
+ }
+ });
+
+ link.addEventListener('click', (event) => {
+ event.preventDefault();
+
+ const ids = [];
+ const linkCheckedCheckboxes = document.querySelectorAll('.link-checkbox:checked');
+ [...linkCheckedCheckboxes].forEach((checkbox) => {
+ ids.push(checkbox.value);
+ });
+
+ subHeader.querySelector('input[name="id"]').value = ids.join(' ');
+ subHeader.querySelector('form').submit();
+ });
+ });
+ }
+ });
+
/**
* Select all button
*/
@@ -545,8 +599,9 @@ function init(description) {
}
const refreshedToken = document.getElementById('token').value;
const fromtag = block.getAttribute('data-tag');
+ const fromtagUrl = block.getAttribute('data-tag-url');
const xhr = new XMLHttpRequest();
- xhr.open('POST', '?do=changetag');
+ xhr.open('POST', `${basePath}/admin/tags`);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = () => {
if (xhr.status !== 200) {
@@ -554,20 +609,28 @@ function init(description) {
location.reload();
} else {
block.setAttribute('data-tag', totag);
+ block.setAttribute('data-tag-url', encodeURIComponent(totag));
input.setAttribute('name', totag);
input.setAttribute('value', totag);
findParent(input, 'div', { class: 'rename-tag-form' }).style.display = 'none';
- block.querySelector('a.tag-link').innerHTML = htmlEntities(totag);
- block.querySelector('a.tag-link').setAttribute('href', `?searchtags=${encodeURIComponent(totag)}`);
- block.querySelector('a.rename-tag').setAttribute('href', `?do=changetag&fromtag=${encodeURIComponent(totag)}`);
+ block.querySelector('a.tag-link').innerHTML = he.encode(totag);
+ block
+ .querySelector('a.tag-link')
+ .setAttribute('href', `${basePath}/?searchtags=${encodeURIComponent(totag)}`);
+ block
+ .querySelector('a.count')
+ .setAttribute('href', `${basePath}/add-tag/${encodeURIComponent(totag)}`);
+ block
+ .querySelector('a.rename-tag')
+ .setAttribute('href', `${basePath}/admin/tags?fromtag=${encodeURIComponent(totag)}`);
// Refresh awesomplete values
- existingTags = existingTags.map(tag => (tag === fromtag ? totag : tag));
- awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
+ existingTags = existingTags.map((tag) => (tag === fromtag ? totag : tag));
+ awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
}
};
- xhr.send(`renametag=1&fromtag=${encodeURIComponent(fromtag)}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
- refreshToken();
+ xhr.send(`renametag=1&fromtag=${fromtagUrl}&totag=${encodeURIComponent(totag)}&token=${refreshedToken}`);
+ refreshToken(basePath);
});
});
@@ -589,26 +652,67 @@ function init(description) {
event.preventDefault();
const block = findParent(event.target, 'div', { class: 'tag-list-item' });
const tag = block.getAttribute('data-tag');
+ const tagUrl = block.getAttribute('data-tag-url');
const refreshedToken = document.getElementById('token').value;
if (confirm(`Are you sure you want to delete the tag "${tag}"?`)) {
const xhr = new XMLHttpRequest();
- xhr.open('POST', '?do=changetag');
+ xhr.open('POST', `${basePath}/admin/tags`);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = () => {
block.remove();
};
- xhr.send(encodeURI(`deletetag=1&fromtag=${tag}&token=${refreshedToken}`));
- refreshToken();
+ xhr.send(`deletetag=1&fromtag=${tagUrl}&token=${refreshedToken}`);
+ refreshToken(basePath);
- existingTags = existingTags.filter(tagItem => tagItem !== tag);
- awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes);
+ existingTags = existingTags.filter((tagItem) => tagItem !== tag);
+ awesomepletes = updateAwesompleteList('.rename-tag-input', existingTags, awesomepletes, tagsSeparator);
}
});
});
const autocompleteFields = document.querySelectorAll('input[data-multiple]');
[...autocompleteFields].forEach((autocompleteField) => {
- awesomepletes.push(createAwesompleteInstance(autocompleteField));
+ awesomepletes.push(createAwesompleteInstance(autocompleteField, tagsSeparator));
});
+
+ const exportForm = document.querySelector('#exportform');
+ if (exportForm != null) {
+ exportForm.addEventListener('submit', (event) => {
+ event.preventDefault();
+
+ refreshToken(basePath, () => {
+ event.target.submit();
+ });
+ });
+ }
+
+ const bulkCreationButton = document.querySelector('.addlink-batch-show-more-block');
+ if (bulkCreationButton != null) {
+ const toggleBulkCreationVisibility = (showMoreBlockElement, formElement) => {
+ if (bulkCreationButton.classList.contains('pure-u-0')) {
+ showMoreBlockElement.classList.remove('pure-u-0');
+ formElement.classList.add('pure-u-0');
+ } else {
+ showMoreBlockElement.classList.add('pure-u-0');
+ formElement.classList.remove('pure-u-0');
+ }
+ };
+
+ const bulkCreationForm = document.querySelector('.addlink-batch-form-block');
+
+ toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
+ bulkCreationButton.querySelector('a').addEventListener('click', (e) => {
+ e.preventDefault();
+ toggleBulkCreationVisibility(bulkCreationButton, bulkCreationForm);
+ });
+
+ // Force to send falsy value if the checkbox is not checked.
+ const privateButton = bulkCreationForm.querySelector('input[type="checkbox"][name="private"]');
+ const privateHiddenButton = bulkCreationForm.querySelector('input[type="hidden"][name="private"]');
+ privateButton.addEventListener('click', () => {
+ privateHiddenButton.disabled = !privateHiddenButton.disabled;
+ });
+ privateHiddenButton.disabled = privateButton.checked;
+ }
})();
diff --git a/assets/default/scss/shaarli.scss b/assets/default/scss/shaarli.scss
index 61e382b6..51ddff23 100644
--- a/assets/default/scss/shaarli.scss
+++ b/assets/default/scss/shaarli.scss
@@ -69,20 +69,20 @@ pre {
font-family: 'Roboto';
font-weight: 400;
font-style: normal;
- src: local('Roboto'),
- local('Roboto-Regular'),
- url('../fonts/Roboto-Regular.woff2') format('woff2'),
- url('../fonts/Roboto-Regular.woff') format('woff');
+ src:
+ local('Roboto-Regular'),
+ url('../fonts/Roboto-Regular.woff2') format('woff2'),
+ url('../fonts/Roboto-Regular.woff') format('woff');
}
@font-face {
font-family: 'Roboto';
font-weight: 700;
font-style: normal;
- src: local('Roboto'),
- local('Roboto-Bold'),
- url('../fonts/Roboto-Bold.woff2') format('woff2'),
- url('../fonts/Roboto-Bold.woff') format('woff');
+ src:
+ local('Roboto-Bold'),
+ url('../fonts/Roboto-Bold.woff2') format('woff2'),
+ url('../fonts/Roboto-Bold.woff') format('woff');
}
body,
@@ -137,6 +137,16 @@ body,
}
}
+.page-form,
+.pure-alert {
+ code {
+ display: inline-block;
+ padding: 0 2px;
+ color: $dark-grey;
+ background-color: var(--background-color);
+ }
+}
+
// Make pure-extras alert closable.
.pure-alert-closable {
.fa-times {
@@ -375,7 +385,7 @@ body,
}
@media screen and (max-width: 64em) {
- .header-search ,
+ .header-search,
.header-search * {
visibility: hidden;
}
@@ -490,6 +500,10 @@ body,
}
}
+.header-alert-message {
+ text-align: center;
+}
+
// CONTENT - GENERAL
.container {
position: relative;
@@ -550,7 +564,6 @@ body,
color: $dark-grey;
font-size: .9em;
-
a {
display: inline-block;
margin: 3px 0;
@@ -612,6 +625,11 @@ body,
padding: 5px;
text-decoration: none;
color: $dark-grey;
+
+ &.selected {
+ background: var(--main-color);
+ color: $white;
+ }
}
input {
@@ -661,6 +679,10 @@ body,
content: '';
}
}
+
+ .search-highlight {
+ background-color: yellow;
+ }
}
.linklist-item-buttons {
@@ -1009,6 +1031,10 @@ body,
&.button-red {
background: $red;
}
+
+ &.button-grey {
+ background: $light-grey;
+ }
}
.submit-buttons {
@@ -1033,7 +1059,7 @@ body,
}
table {
- margin: auto;
+ margin: 10px auto 25px auto;
width: 90%;
.order {
@@ -1069,6 +1095,11 @@ body,
position: absolute;
right: 5%;
}
+
+ &.button-grey {
+ position: absolute;
+ left: 5%;
+ }
}
}
}
@@ -1236,8 +1267,23 @@ form {
color: $dark-grey;
}
-.page404-container {
+.page-error-container {
color: $dark-grey;
+
+ h2 {
+ margin: 70px 0 25px;
+ }
+
+ a {
+ color: var(--main-color);
+ }
+
+ pre {
+ margin: 0 20%;
+ padding: 20px 0;
+ text-align: left;
+ line-height: 1em;
+ }
}
// EDIT LINK
@@ -1248,6 +1294,57 @@ form {
}
}
+.loading-input {
+ position: relative;
+
+ @keyframes around {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+ }
+
+ .icon-container {
+ position: absolute;
+ right: 60px;
+ top: calc(50% - 10px);
+ }
+
+ .loader {
+ position: relative;
+ height: 20px;
+ width: 20px;
+ display: inline-block;
+ animation: around 5.4s infinite;
+
+ &::after,
+ &::before {
+ content: "";
+ background: $form-input-background;
+ position: absolute;
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ border-width: 2px;
+ border-color: #333 #333 transparent transparent;
+ border-style: solid;
+ border-radius: 20px;
+ box-sizing: border-box;
+ top: 0;
+ left: 0;
+ animation: around 0.7s ease-in-out infinite;
+ }
+
+ &::after {
+ animation: around 0.7s ease-in-out 0.1s infinite;
+ background: transparent;
+ }
+ }
+}
+
// LOGIN
.login-form-container {
.remember-me {
@@ -1436,6 +1533,8 @@ form {
-webkit-transition: opacity 500ms ease-in-out;
-moz-transition: opacity 500ms ease-in-out;
-o-transition: opacity 500ms ease-in-out;
+ min-width: 1px;
+ min-height: 1px;
&.b-loaded {
opacity: 1;
@@ -1535,11 +1634,11 @@ form {
text-align: center;
a {
+ background: $almost-white;
display: inline-block;
- margin: 0 15px;
+ padding: 5px;
text-decoration: none;
- color: $white;
- font-weight: bold;
+ color: $dark-grey;
}
}
@@ -1587,13 +1686,14 @@ form {
> div {
border-radius: 10px;
- background: repeating-linear-gradient(
- -45deg,
- $almost-white,
- $almost-white 6px,
- var(--background-color) 6px,
- var(--background-color) 12px
- );
+ background:
+ repeating-linear-gradient(
+ -45deg,
+ $almost-white,
+ $almost-white 6px,
+ var(--background-color) 6px,
+ var(--background-color) 12px
+ );
width: 0%;
height: 10px;
}
@@ -1617,6 +1717,123 @@ form {
}
}
+// SERVER PAGE
+
+.server-tables-page,
+.server-tables {
+ .window-subtitle {
+ &::before {
+ display: block;
+ margin: 8px auto;
+ background: linear-gradient(to right, var(--background-color), $dark-grey, var(--background-color));
+ width: 50%;
+ height: 1px;
+ content: '';
+ }
+ }
+
+ .server-row {
+ p {
+ height: 25px;
+ padding: 0 10px;
+ }
+ }
+
+ .server-label {
+ text-align: right;
+ font-weight: bold;
+ }
+
+ i {
+ &.fa-color-green {
+ color: $main-green;
+ }
+
+ &.fa-color-orange {
+ color: $orange;
+ }
+
+ &.fa-color-red {
+ color: $red;
+ }
+ }
+
+ @media screen and (max-width: 64em) {
+ .server-label {
+ text-align: center;
+ }
+
+ .server-row {
+ p {
+ text-align: center;
+ }
+ }
+ }
+}
+
+// Batch creation
+input[name='save_edit_batch'] {
+ @extend %page-form-button;
+}
+
+.addlink-batch-show-more {
+ display: flex;
+ align-items: center;
+ margin: 20px 0 8px;
+
+ a {
+ color: var(--main-color);
+ text-decoration: none;
+ }
+
+ &::before,
+ &::after {
+ content: "";
+ flex-grow: 1;
+ background: rgba(0, 0, 0, 0.35);
+ height: 1px;
+ font-size: 0;
+ line-height: 0;
+ }
+
+ &::before {
+ margin: 0 16px 0 0;
+ }
+
+ &::after {
+ margin: 0 0 0 16px;
+ }
+}
+
+.dark-layer {
+ display: none;
+ position: fixed;
+ height: 100%;
+ width: 100%;
+ z-index: 998;
+ background-color: rgba(0, 0, 0, .75);
+ color: #fff;
+
+ .screen-center {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ min-height: 100vh;
+ }
+
+ .progressbar {
+ width: 33%;
+ }
+}
+
+.addlink-batch-form-block {
+ .pure-alert {
+ margin: 25px 0 0 0;
+ }
+}
+
// Print rules
@media print {
.shaarli-menu {
diff --git a/assets/vintage/css/shaarli.css b/assets/vintage/css/shaarli.css
index 87c440c8..33e178af 100644
--- a/assets/vintage/css/shaarli.css
+++ b/assets/vintage/css/shaarli.css
@@ -746,8 +746,6 @@ a.bigbutton, #pageheader a.bigbutton {
text-align: left;
background-color: transparent;
background-color: rgba(0, 0, 0, 0.4);
- /* FF3+, Saf3+, Opera 10.10+, Chrome, IE9 */
- filter: progid: DXImageTransform.Microsoft.gradient(startColorstr=#66000000, endColorstr=#66000000);
/* IE6IE9 */
text-shadow: 2px 2px 1px #000000;
}
@@ -1124,6 +1122,16 @@ ul.errors {
float: left;
}
+ul.warnings {
+ color: orange;
+ float: left;
+}
+
+ul.successes {
+ color: green;
+ float: left;
+}
+
#pluginsadmin {
width: 80%;
padding: 20px 0 0 20px;
@@ -1250,3 +1258,54 @@ ul.errors {
width: 0%;
height: 10px;
}
+
+.loading-input {
+ position: relative;
+}
+
+@keyframes around {
+ 0% {
+ transform: rotate(0deg);
+ }
+
+ 100% {
+ transform: rotate(360deg);
+ }
+}
+
+.loading-input .icon-container {
+ position: absolute;
+ right: 60px;
+ top: calc(50% - 10px);
+}
+
+.loading-input .loader {
+ position: relative;
+ height: 20px;
+ width: 20px;
+ display: inline-block;
+ animation: around 5.4s infinite;
+}
+
+.loading-input .loader::after,
+.loading-input .loader::before {
+ content: "";
+ background: #eee;
+ position: absolute;
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+ border-width: 2px;
+ border-color: #333 #333 transparent transparent;
+ border-style: solid;
+ border-radius: 20px;
+ box-sizing: border-box;
+ top: 0;
+ left: 0;
+ animation: around 0.7s ease-in-out infinite;
+}
+
+.loading-input .loader::after {
+ animation: around 0.7s ease-in-out 0.1s infinite;
+ background: transparent;
+}
diff --git a/assets/vintage/js/base.js b/assets/vintage/js/base.js
index 66830b59..55f1c37d 100644
--- a/assets/vintage/js/base.js
+++ b/assets/vintage/js/base.js
@@ -2,29 +2,38 @@ import Awesomplete from 'awesomplete';
import 'awesomplete/awesomplete.css';
(() => {
- const awp = Awesomplete.$;
const autocompleteFields = document.querySelectorAll('input[data-multiple]');
- [...autocompleteFields].forEach((autocompleteField) => {
- const awesomplete = new Awesomplete(awp(autocompleteField));
- awesomplete.filter = (text, input) => Awesomplete.FILTER_CONTAINS(text, input.match(/[^ ]*$/)[0]);
- awesomplete.replace = (text) => {
- const before = awesomplete.input.value.match(/^.+ \s*|/)[0];
- awesomplete.input.value = `${before}${text} `;
- };
- awesomplete.minChars = 1;
+ const tagsSeparatorElement = document.querySelector('input[name="tags_separator"]');
+ const tagsSeparator = tagsSeparatorElement ? tagsSeparatorElement.value || ' ' : ' ';
- autocompleteField.addEventListener('input', () => {
- const proposedTags = autocompleteField.getAttribute('data-list').replace(/,/g, '').split(' ');
- const reg = /(\w+) /g;
- let match;
- while ((match = reg.exec(autocompleteField.value)) !== null) {
- const id = proposedTags.indexOf(match[1]);
- if (id !== -1) {
- proposedTags.splice(id, 1);
+ [...autocompleteFields].forEach((autocompleteField) => {
+ const awesome = new Awesomplete(Awesomplete.$(autocompleteField));
+
+ // Tags are separated by separator
+ awesome.filter = (text, input) => Awesomplete.FILTER_CONTAINS(
+ text,
+ input.match(new RegExp(`[^${tagsSeparator}]*$`))[0],
+ );
+ // Insert new selected tag in the input
+ awesome.replace = (text) => {
+ const before = awesome.input.value.match(new RegExp(`^.+${tagsSeparator}+|`))[0];
+ awesome.input.value = `${before}${text}${tagsSeparator}`;
+ };
+ // Highlight found items
+ awesome.item = (text, input) => Awesomplete.ITEM(text, input.match(new RegExp(`[^${tagsSeparator}]*$`))[0]);
+
+ // Don't display already selected items
+ // WARNING: pseudo classes does not seem to work with string litterals...
+ const reg = new RegExp(`([^${tagsSeparator}]+)${tagsSeparator}`, 'g');
+ let match;
+ awesome.data = (item, input) => {
+ while ((match = reg.exec(input))) {
+ if (item === match[1]) {
+ return '';
}
}
-
- awesomplete.list = proposedTags;
- });
+ return item;
+ };
+ awesome.minChars = 1;
});
})();
diff --git a/composer.json b/composer.json
index c23b8252..3015d8eb 100644
--- a/composer.json
+++ b/composer.json
@@ -10,26 +10,29 @@
},
"keywords": ["bookmark", "link", "share", "web"],
"config": {
+ "sort-packages": true,
"platform": {
- "php": "5.6.31"
+ "php": "7.1.29"
}
},
"require": {
- "php": ">=5.6",
+ "php": ">=7.1",
"ext-json": "*",
"ext-zlib": "*",
- "shaarli/netscape-bookmark-parser": "^2.1",
+ "arthurhoaro/web-thumbnailer": "^2.0",
"erusev/parsedown": "^1.6",
- "slim/slim": "^3.0",
- "arthurhoaro/web-thumbnailer": "^1.1",
+ "erusev/parsedown-extra": "^0.8.1",
+ "gettext/gettext": "^4.4",
+ "katzgrau/klogger": "^1.2",
+ "malkusch/lock": "^2.1",
"pubsubhubbub/publisher": "dev-master",
- "gettext/gettext": "^4.4"
+ "shaarli/netscape-bookmark-parser": "^4.0",
+ "slim/slim": "^3.0"
},
"require-dev": {
"roave/security-advisories": "dev-master",
- "phpunit/phpcov": "*",
- "phpunit/phpunit": "^5.0",
- "squizlabs/php_codesniffer": "2.*"
+ "squizlabs/php_codesniffer": "3.*",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"suggest": {
"ext-curl": "Allows fetching web pages and thumbnails in a more robust way",
@@ -48,17 +51,32 @@
"Shaarli\\Bookmark\\Exception\\": "application/bookmark/exception",
"Shaarli\\Config\\": "application/config/",
"Shaarli\\Config\\Exception\\": "application/config/exception",
+ "Shaarli\\Container\\": "application/container",
"Shaarli\\Exceptions\\": "application/exceptions",
"Shaarli\\Feed\\": "application/feed",
+ "Shaarli\\Formatter\\": "application/formatter",
+ "Shaarli\\Front\\": "application/front",
+ "Shaarli\\Front\\Controller\\Admin\\": "application/front/controller/admin",
+ "Shaarli\\Front\\Controller\\Visitor\\": "application/front/controller/visitor",
+ "Shaarli\\Front\\Exception\\": "application/front/exceptions",
+ "Shaarli\\Helper\\": "application/helper",
"Shaarli\\Http\\": "application/http",
+ "Shaarli\\Legacy\\": "application/legacy",
"Shaarli\\Netscape\\": "application/netscape",
"Shaarli\\Plugin\\": "application/plugin",
"Shaarli\\Plugin\\Exception\\": "application/plugin/exception",
"Shaarli\\Plugin\\Wallabag\\": "plugins/wallabag",
+ "Shaarli\\Plugin\\ReadItLater\\": "plugins/readitlater",
"Shaarli\\Render\\": "application/render",
"Shaarli\\Security\\": "application/security",
"Shaarli\\Updater\\": "application/updater",
"Shaarli\\Updater\\Exception\\": "application/updater/exception"
}
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Shaarli\\Tests\\": "tests",
+ "Shaarli\\Tests\\Utils\\": "tests/utils"
+ }
}
}
diff --git a/composer.lock b/composer.lock
index 97b5e3bd..7b14f599 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,41 +4,37 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "432005c9db3e890f42fde27036d2a70f",
+ "content-hash": "326e743376bd043cd7de28c02b5ac1d5",
"packages": [
{
"name": "arthurhoaro/web-thumbnailer",
- "version": "v1.3.1",
+ "version": "v2.1.0",
"source": {
"type": "git",
"url": "https://github.com/ArthurHoaro/web-thumbnailer.git",
- "reference": "7142bd94ec93719a756a7012ebb8e1c5813c6860"
+ "reference": "47675fc58f6fd1dfd63f911b6e86ee5f31e8efd1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/7142bd94ec93719a756a7012ebb8e1c5813c6860",
- "reference": "7142bd94ec93719a756a7012ebb8e1c5813c6860",
+ "url": "https://api.github.com/repos/ArthurHoaro/web-thumbnailer/zipball/47675fc58f6fd1dfd63f911b6e86ee5f31e8efd1",
+ "reference": "47675fc58f6fd1dfd63f911b6e86ee5f31e8efd1",
"shasum": ""
},
"require": {
- "php": ">=5.6",
- "phpunit/php-text-template": "^1.2"
- },
- "conflict": {
- "phpunit/php-timer": ">=2"
+ "php": ">=7.1",
+ "phpunit/php-text-template": "^1.2 || ^2.0"
},
"require-dev": {
+ "gskema/phpcs-type-sniff": "^0.13.1",
"php-coveralls/php-coveralls": "^2.0",
- "phpunit/phpunit": "5.2.*",
- "squizlabs/php_codesniffer": "^3.2"
+ "phpstan/phpstan": "^0.12.9",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0 || dev-master",
+ "squizlabs/php_codesniffer": "^3.0"
},
"type": "library",
"autoload": {
- "psr-0": {
- "WebThumbnailer\\": [
- "src/",
- "tests/"
- ]
+ "psr-4": {
+ "WebThumbnailer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -48,55 +44,28 @@
"authors": [
{
"name": "Arthur Hoaro",
- "homepage": "http://hoa.ro"
+ "homepage": "https://hoa.ro"
}
],
"description": "PHP library which will retrieve a thumbnail for any given URL",
- "time": "2018-08-11T12:21:52+00:00"
- },
- {
- "name": "container-interop/container-interop",
- "version": "1.2.0",
- "source": {
- "type": "git",
- "url": "https://github.com/container-interop/container-interop.git",
- "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8"
+ "support": {
+ "issues": "https://github.com/ArthurHoaro/web-thumbnailer/issues",
+ "source": "https://github.com/ArthurHoaro/web-thumbnailer/tree/v2.1.0"
},
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8",
- "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8",
- "shasum": ""
- },
- "require": {
- "psr/container": "^1.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "Interop\\Container\\": "src/Interop/Container/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "Promoting the interoperability of container objects (DIC, SL, etc.)",
- "homepage": "https://github.com/container-interop/container-interop",
- "time": "2017-02-14T19:40:03+00:00"
+ "time": "2021-05-08T11:20:56+00:00"
},
{
"name": "erusev/parsedown",
- "version": "1.7.1",
+ "version": "1.7.4",
"source": {
"type": "git",
"url": "https://github.com/erusev/parsedown.git",
- "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1"
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/erusev/parsedown/zipball/92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
- "reference": "92e9c27ba0e74b8b028b111d1b6f956a15c01fc1",
+ "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
+ "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3",
"shasum": ""
},
"require": {
@@ -129,20 +98,75 @@
"markdown",
"parser"
],
- "time": "2018-03-08T01:11:30+00:00"
+ "support": {
+ "issues": "https://github.com/erusev/parsedown/issues",
+ "source": "https://github.com/erusev/parsedown/tree/1.7.x"
+ },
+ "time": "2019-12-30T22:54:17+00:00"
},
{
- "name": "gettext/gettext",
- "version": "v4.6.2",
+ "name": "erusev/parsedown-extra",
+ "version": "0.8.1",
"source": {
"type": "git",
- "url": "https://github.com/oscarotero/Gettext.git",
- "reference": "93176b272d61fb58a9767be71c50d19149cb1e48"
+ "url": "https://github.com/erusev/parsedown-extra.git",
+ "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/oscarotero/Gettext/zipball/93176b272d61fb58a9767be71c50d19149cb1e48",
- "reference": "93176b272d61fb58a9767be71c50d19149cb1e48",
+ "url": "https://api.github.com/repos/erusev/parsedown-extra/zipball/91ac3ff98f0cea243bdccc688df43810f044dcef",
+ "reference": "91ac3ff98f0cea243bdccc688df43810f044dcef",
+ "shasum": ""
+ },
+ "require": {
+ "erusev/parsedown": "^1.7.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "ParsedownExtra": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Emanuil Rusev",
+ "email": "hello@erusev.com",
+ "homepage": "http://erusev.com"
+ }
+ ],
+ "description": "An extension of Parsedown that adds support for Markdown Extra.",
+ "homepage": "https://github.com/erusev/parsedown-extra",
+ "keywords": [
+ "markdown",
+ "markdown extra",
+ "parsedown",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/erusev/parsedown-extra/issues",
+ "source": "https://github.com/erusev/parsedown-extra/tree/0.8.x"
+ },
+ "time": "2019-12-30T23:20:37+00:00"
+ },
+ {
+ "name": "gettext/gettext",
+ "version": "v4.8.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-gettext/Gettext.git",
+ "reference": "3f7bc5ef23302a9059e64934f3d59e454516bec0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-gettext/Gettext/zipball/3f7bc5ef23302a9059e64934f3d59e454516bec0",
+ "reference": "3f7bc5ef23302a9059e64934f3d59e454516bec0",
"shasum": ""
},
"require": {
@@ -150,7 +174,7 @@
"php": ">=5.4.0"
},
"require-dev": {
- "illuminate/view": "*",
+ "illuminate/view": "^5.0.x-dev",
"phpunit/phpunit": "^4.8|^5.7|^6.5",
"squizlabs/php_codesniffer": "^3.0",
"symfony/yaml": "~2",
@@ -191,31 +215,49 @@
"po",
"translation"
],
- "time": "2019-01-12T18:40:56+00:00"
+ "support": {
+ "email": "oom@oscarotero.com",
+ "issues": "https://github.com/oscarotero/Gettext/issues",
+ "source": "https://github.com/php-gettext/Gettext/tree/v4.8.7"
+ },
+ "funding": [
+ {
+ "url": "https://paypal.me/oscarotero",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/oscarotero",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/misteroom",
+ "type": "patreon"
+ }
+ ],
+ "time": "2022-08-02T09:42:10+00:00"
},
{
"name": "gettext/languages",
- "version": "2.5.0",
+ "version": "2.9.0",
"source": {
"type": "git",
- "url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git",
- "reference": "78db2d17933f0765a102f368a6663f057162ddbd"
+ "url": "https://github.com/php-gettext/Languages.git",
+ "reference": "ed56dd2c7f4024cc953ed180d25f02f2640e3ffa"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/78db2d17933f0765a102f368a6663f057162ddbd",
- "reference": "78db2d17933f0765a102f368a6663f057162ddbd",
+ "url": "https://api.github.com/repos/php-gettext/Languages/zipball/ed56dd2c7f4024cc953ed180d25f02f2640e3ffa",
+ "reference": "ed56dd2c7f4024cc953ed180d25f02f2640e3ffa",
"shasum": ""
},
"require": {
"php": ">=5.3"
},
"require-dev": {
- "phpunit/phpunit": "^4"
+ "phpunit/phpunit": "^4.8 || ^5.7 || ^6.5 || ^7.5 || ^8.4"
},
"bin": [
- "bin/export-plural-rules",
- "bin/export-plural-rules.php"
+ "bin/export-plural-rules"
],
"type": "library",
"autoload": {
@@ -235,7 +277,7 @@
}
],
"description": "gettext languages with plural rules",
- "homepage": "https://github.com/mlocati/cldr-to-gettext-plural-rules",
+ "homepage": "https://github.com/php-gettext/Languages",
"keywords": [
"cldr",
"i18n",
@@ -252,20 +294,34 @@
"translations",
"unicode"
],
- "time": "2018-11-13T22:06:07+00:00"
+ "support": {
+ "issues": "https://github.com/php-gettext/Languages/issues",
+ "source": "https://github.com/php-gettext/Languages/tree/2.9.0"
+ },
+ "funding": [
+ {
+ "url": "https://paypal.me/mlocati",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/mlocati",
+ "type": "github"
+ }
+ ],
+ "time": "2021-11-11T17:30:39+00:00"
},
{
"name": "katzgrau/klogger",
- "version": "1.2.1",
+ "version": "1.2.2",
"source": {
"type": "git",
"url": "https://github.com/katzgrau/KLogger.git",
- "reference": "a4ed373fa8a214aa4ae7aa4f221fe2c6ce862ef1"
+ "reference": "36481c69db9305169a2ceadead25c2acaabd567c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/katzgrau/KLogger/zipball/a4ed373fa8a214aa4ae7aa4f221fe2c6ce862ef1",
- "reference": "a4ed373fa8a214aa4ae7aa4f221fe2c6ce862ef1",
+ "url": "https://api.github.com/repos/katzgrau/KLogger/zipball/36481c69db9305169a2ceadead25c2acaabd567c",
+ "reference": "36481c69db9305169a2ceadead25c2acaabd567c",
"shasum": ""
},
"require": {
@@ -273,7 +329,7 @@
"psr/log": "^1.0.0"
},
"require-dev": {
- "phpunit/phpunit": "4.0.*"
+ "phpunit/phpunit": "^6.0.0"
},
"type": "library",
"autoload": {
@@ -302,7 +358,96 @@
"keywords": [
"logging"
],
- "time": "2016-11-07T19:29:14+00:00"
+ "support": {
+ "issues": "https://github.com/katzgrau/KLogger/issues",
+ "source": "https://github.com/katzgrau/KLogger/tree/1.2.2"
+ },
+ "time": "2022-07-29T20:41:14+00:00"
+ },
+ {
+ "name": "malkusch/lock",
+ "version": "v2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-lock/lock.git",
+ "reference": "093f389ec2f38fc8686d2f70e23378182fce7714"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-lock/lock/zipball/093f389ec2f38fc8686d2f70e23378182fce7714",
+ "reference": "093f389ec2f38fc8686d2f70e23378182fce7714",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/log": "^1"
+ },
+ "require-dev": {
+ "eloquent/liberator": "^2.0",
+ "ext-memcached": "*",
+ "ext-pcntl": "*",
+ "ext-pdo_mysql": "*",
+ "ext-pdo_sqlite": "*",
+ "ext-redis": "*",
+ "ext-sysvsem": "*",
+ "johnkary/phpunit-speedtrap": "^3.0",
+ "kriswallsmith/spork": "^0.3",
+ "mikey179/vfsstream": "^1.6",
+ "php-mock/php-mock-phpunit": "^2.1",
+ "phpunit/phpunit": "^7.4",
+ "predis/predis": "^1.1",
+ "squizlabs/php_codesniffer": "^3.3"
+ },
+ "suggest": {
+ "ext-pnctl": "Enables locking with flock without busy waiting in CLI scripts.",
+ "ext-redis": "To use this library with the PHP Redis extension.",
+ "ext-sysvsem": "Enables locking using semaphores.",
+ "predis/predis": "To use this library with predis."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "malkusch\\lock\\": "classes/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "WTFPL"
+ ],
+ "authors": [
+ {
+ "name": "Markus Malkusch",
+ "email": "markus@malkusch.de",
+ "homepage": "http://markus.malkusch.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Willem Stuursma-Ruwen",
+ "email": "willem@stuursma.name",
+ "role": "Developer"
+ }
+ ],
+ "description": "Mutex library for exclusive code execution.",
+ "homepage": "https://github.com/malkusch/lock",
+ "keywords": [
+ "advisory-locks",
+ "cas",
+ "flock",
+ "lock",
+ "locking",
+ "memcache",
+ "mutex",
+ "mysql",
+ "postgresql",
+ "redis",
+ "redlock",
+ "semaphore"
+ ],
+ "support": {
+ "issues": "https://github.com/php-lock/lock/issues",
+ "source": "https://github.com/php-lock/lock/tree/v2.1"
+ },
+ "time": "2018-12-12T19:53:29+00:00"
},
{
"name": "nikic/fast-route",
@@ -326,12 +471,12 @@
},
"type": "library",
"autoload": {
- "psr-4": {
- "FastRoute\\": "src/"
- },
"files": [
"src/functions.php"
- ]
+ ],
+ "psr-4": {
+ "FastRoute\\": "src/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -348,6 +493,10 @@
"router",
"routing"
],
+ "support": {
+ "issues": "https://github.com/nikic/FastRoute/issues",
+ "source": "https://github.com/nikic/FastRoute/tree/master"
+ },
"time": "2018-02-13T20:26:39+00:00"
},
{
@@ -389,6 +538,10 @@
"keywords": [
"template"
],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
+ },
"time": "2015-06-21T13:50:34+00:00"
},
{
@@ -439,6 +592,10 @@
"container",
"dependency injection"
],
+ "support": {
+ "issues": "https://github.com/silexphp/Pimple/issues",
+ "source": "https://github.com/silexphp/Pimple/tree/master"
+ },
"time": "2018-01-21T07:42:36+00:00"
},
{
@@ -488,6 +645,10 @@
"container-interop",
"psr"
],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/master"
+ },
"time": "2017-02-14T16:28:37+00:00"
},
{
@@ -538,20 +699,23 @@
"request",
"response"
],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/master"
+ },
"time": "2016-08-06T14:39:51+00:00"
},
{
"name": "psr/log",
- "version": "1.1.0",
+ "version": "1.1.4",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
- "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd"
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
- "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
+ "reference": "d49695b909c3b7628b6289db5479a1c204601f11",
"shasum": ""
},
"require": {
@@ -560,7 +724,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "1.1.x-dev"
}
},
"autoload": {
@@ -575,7 +739,7 @@
"authors": [
{
"name": "PHP-FIG",
- "homepage": "http://www.php-fig.org/"
+ "homepage": "https://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
@@ -585,7 +749,10 @@
"psr",
"psr-3"
],
- "time": "2018-11-20T15:27:04+00:00"
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/1.1.4"
+ },
+ "time": "2021-05-03T11:20:27+00:00"
},
{
"name": "pubsubhubbub/publisher",
@@ -593,18 +760,19 @@
"source": {
"type": "git",
"url": "https://github.com/pubsubhubbub/php-publisher.git",
- "reference": "047b0faf6219071527a45942d6fef4dbc6d1d884"
+ "reference": "60c7aa753b2908c7a924d3be8686eafcbeebbfd6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/047b0faf6219071527a45942d6fef4dbc6d1d884",
- "reference": "047b0faf6219071527a45942d6fef4dbc6d1d884",
+ "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/60c7aa753b2908c7a924d3be8686eafcbeebbfd6",
+ "reference": "60c7aa753b2908c7a924d3be8686eafcbeebbfd6",
"shasum": ""
},
"require": {
"ext-curl": "*",
- "php": "~5.4 || ~7.0"
+ "php": "~5.4 || ~7.0 || ~8.0"
},
+ "default-branch": true,
"type": "library",
"autoload": {
"psr-4": {
@@ -630,34 +798,39 @@
"pubsubhubbub",
"websub"
],
- "time": "2018-10-09T05:20:28+00:00"
+ "support": {
+ "issues": "https://github.com/pubsubhubbub/php-publisher/issues",
+ "source": "https://github.com/pubsubhubbub/php-publisher/tree/master"
+ },
+ "time": "2021-12-07T05:38:17+00:00"
},
{
"name": "shaarli/netscape-bookmark-parser",
- "version": "v2.1.0",
+ "version": "v4.0.0",
"source": {
"type": "git",
"url": "https://github.com/shaarli/netscape-bookmark-parser.git",
- "reference": "819008ee42c4dd7e45d988176a4a22d6ed689577"
+ "reference": "aa024e5731959966660d98fcefe27deada40d88e"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/819008ee42c4dd7e45d988176a4a22d6ed689577",
- "reference": "819008ee42c4dd7e45d988176a4a22d6ed689577",
+ "url": "https://api.github.com/repos/shaarli/netscape-bookmark-parser/zipball/aa024e5731959966660d98fcefe27deada40d88e",
+ "reference": "aa024e5731959966660d98fcefe27deada40d88e",
"shasum": ""
},
"require": {
- "katzgrau/klogger": "~1.0",
- "php": ">=5.6"
+ "php": ">=7.1",
+ "psr/log": "^1.1"
},
"require-dev": {
- "phpunit/phpunit": "^5.0"
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "squizlabs/php_codesniffer": "^3.5"
},
"type": "library",
"autoload": {
- "files": [
- "NetscapeBookmarkParser.php"
- ]
+ "psr-4": {
+ "Shaarli\\NetscapeBookmarkParser\\": "src/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -675,34 +848,48 @@
"email": "virtualtam@flibidi.net",
"homepage": "https://github.com/virtualtam",
"role": "Developer"
+ },
+ {
+ "name": "Matthias Morin",
+ "email": "mat@tangoman.io",
+ "homepage": "https://github.com/TangoMan75",
+ "role": "Developer"
}
],
"description": "Generic Netscape bookmark parser",
"homepage": "https://github.com/shaarli/netscape-bookmark-parser",
"keywords": [
"bookmark",
+ "decoder",
+ "encoder",
"link",
"netscape",
- "parser"
+ "parse"
],
- "time": "2018-10-06T14:43:38+00:00"
+ "support": {
+ "issues": "https://github.com/shaarli/netscape-bookmark-parser/issues",
+ "source": "https://github.com/shaarli/netscape-bookmark-parser/tree/v4.0.0"
+ },
+ "time": "2022-08-13T09:57:26+00:00"
},
{
"name": "slim/slim",
- "version": "3.12.0",
+ "version": "3.12.4",
"source": {
"type": "git",
"url": "https://github.com/slimphp/Slim.git",
- "reference": "f4947cc900b6e51cbfda58b9f1247bca2f76f9f0"
+ "reference": "ce3cb65a06325fc9fe3d0223f2ae23113a767304"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/slimphp/Slim/zipball/f4947cc900b6e51cbfda58b9f1247bca2f76f9f0",
- "reference": "f4947cc900b6e51cbfda58b9f1247bca2f76f9f0",
+ "url": "https://api.github.com/repos/slimphp/Slim/zipball/ce3cb65a06325fc9fe3d0223f2ae23113a767304",
+ "reference": "ce3cb65a06325fc9fe3d0223f2ae23113a767304",
"shasum": ""
},
"require": {
- "container-interop/container-interop": "^1.2",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-simplexml": "*",
"nikic/fast-route": "^1.0",
"php": ">=5.5.0",
"pimple/pimple": "^3.0",
@@ -714,7 +901,7 @@
},
"require-dev": {
"phpunit/phpunit": "^4.0",
- "squizlabs/php_codesniffer": "^2.5"
+ "squizlabs/php_codesniffer": "^3.6.0"
},
"type": "library",
"autoload": {
@@ -727,25 +914,25 @@
"MIT"
],
"authors": [
- {
- "name": "Rob Allen",
- "email": "rob@akrabat.com",
- "homepage": "http://akrabat.com"
- },
{
"name": "Josh Lockhart",
"email": "hello@joshlockhart.com",
"homepage": "https://joshlockhart.com"
},
- {
- "name": "Gabriel Manricks",
- "email": "gmanricks@me.com",
- "homepage": "http://gabrielmanricks.com"
- },
{
"name": "Andrew Smith",
"email": "a.smith@silentworks.co.uk",
"homepage": "http://silentworks.co.uk"
+ },
+ {
+ "name": "Rob Allen",
+ "email": "rob@akrabat.com",
+ "homepage": "http://akrabat.com"
+ },
+ {
+ "name": "Gabriel Manricks",
+ "email": "gmanricks@me.com",
+ "homepage": "http://gabrielmanricks.com"
}
],
"description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs",
@@ -756,40 +943,52 @@
"micro",
"router"
],
- "time": "2019-01-15T13:21:25+00:00"
+ "support": {
+ "issues": "https://github.com/slimphp/Slim/issues",
+ "source": "https://github.com/slimphp/Slim/tree/3.12.4"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/slimphp",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/slim/slim",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-10-02T19:38:22+00:00"
}
],
"packages-dev": [
{
"name": "doctrine/instantiator",
- "version": "1.0.5",
+ "version": "1.4.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
- "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
+ "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
- "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
+ "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc",
+ "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc",
"shasum": ""
},
"require": {
- "php": ">=5.3,<8.0-DEV"
+ "php": "^7.1 || ^8.0"
},
"require-dev": {
- "athletic/athletic": "~0.1.8",
+ "doctrine/coding-standard": "^9",
"ext-pdo": "*",
"ext-phar": "*",
- "phpunit/phpunit": "~4.0",
- "squizlabs/php_codesniffer": "~2.0"
+ "phpbench/phpbench": "^0.16 || ^1",
+ "phpstan/phpstan": "^1.4",
+ "phpstan/phpstan-phpunit": "^1",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "vimeo/psalm": "^4.22"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
"autoload": {
"psr-4": {
"Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/"
@@ -803,47 +1002,69 @@
{
"name": "Marco Pivetta",
"email": "ocramius@gmail.com",
- "homepage": "http://ocramius.github.com/"
+ "homepage": "https://ocramius.github.io/"
}
],
"description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors",
- "homepage": "https://github.com/doctrine/instantiator",
+ "homepage": "https://www.doctrine-project.org/projects/instantiator.html",
"keywords": [
"constructor",
"instantiate"
],
- "time": "2015-06-14T21:17:01+00:00"
+ "support": {
+ "issues": "https://github.com/doctrine/instantiator/issues",
+ "source": "https://github.com/doctrine/instantiator/tree/1.4.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-03-03T08:28:38+00:00"
},
{
"name": "myclabs/deep-copy",
- "version": "1.7.0",
+ "version": "1.11.0",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
- "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e"
+ "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
- "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/14daed4296fae74d9e3201d2c4925d1acb7aa614",
+ "reference": "14daed4296fae74d9e3201d2c4925d1acb7aa614",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3,<3.2.2"
},
"require-dev": {
- "doctrine/collections": "^1.0",
- "doctrine/common": "^2.6",
- "phpunit/phpunit": "^4.1"
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
},
"type": "library",
"autoload": {
- "psr-4": {
- "DeepCopy\\": "src/DeepCopy/"
- },
"files": [
"src/DeepCopy/deep_copy.php"
- ]
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
@@ -857,27 +1078,37 @@
"object",
"object graph"
],
- "time": "2017-10-19T19:58:43+00:00"
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.11.0"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-03-03T13:19:32+00:00"
},
{
- "name": "phpdocumentor/reflection-common",
- "version": "1.0.1",
+ "name": "phar-io/manifest",
+ "version": "1.0.3",
"source": {
"type": "git",
- "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
- "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
- "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
+ "reference": "7761fcacf03b4d4f16e7ccb606d4879ca431fcf4",
"shasum": ""
},
"require": {
- "php": ">=5.5"
- },
- "require-dev": {
- "phpunit/phpunit": "^4.6"
+ "ext-dom": "*",
+ "ext-phar": "*",
+ "phar-io/version": "^2.0",
+ "php": "^5.6 || ^7.0"
},
"type": "library",
"extra": {
@@ -885,11 +1116,116 @@
"dev-master": "1.0.x-dev"
}
},
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/master"
+ },
+ "time": "2018-07-08T19:23:20+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+ "reference": "45a2ec53a73c70ce41d55cedef9063630abaf1b6",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/master"
+ },
+ "time": "2018-07-08T19:19:57+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
+ "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
"autoload": {
"psr-4": {
- "phpDocumentor\\Reflection\\": [
- "src"
- ]
+ "phpDocumentor\\Reflection\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -911,33 +1247,44 @@
"reflection",
"static analysis"
],
- "time": "2017-09-11T18:02:19+00:00"
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/master"
+ },
+ "time": "2020-04-27T09:25:28+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "3.3.2",
+ "version": "4.3.4",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2"
+ "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bf329f6c1aadea3299f08ee804682b7c45b326a2",
- "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/da3fd972d6bafd628114f7e7e036f45944b62e9c",
+ "reference": "da3fd972d6bafd628114f7e7e036f45944b62e9c",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0",
- "phpdocumentor/reflection-common": "^1.0.0",
- "phpdocumentor/type-resolver": "^0.4.0",
+ "php": "^7.0",
+ "phpdocumentor/reflection-common": "^1.0.0 || ^2.0.0",
+ "phpdocumentor/type-resolver": "~0.4 || ^1.0.0",
"webmozart/assert": "^1.0"
},
"require-dev": {
- "mockery/mockery": "^0.9.4",
- "phpunit/phpunit": "^4.4"
+ "doctrine/instantiator": "^1.0.5",
+ "mockery/mockery": "^1.0",
+ "phpdocumentor/type-resolver": "0.4.*",
+ "phpunit/phpunit": "^6.4"
},
"type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.x-dev"
+ }
+ },
"autoload": {
"psr-4": {
"phpDocumentor\\Reflection\\": [
@@ -956,41 +1303,44 @@
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
- "time": "2017-11-10T14:09:06+00:00"
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/4.x"
+ },
+ "time": "2019-12-28T18:55:12+00:00"
},
{
"name": "phpdocumentor/type-resolver",
- "version": "0.4.0",
+ "version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
- "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
+ "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
- "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
+ "reference": "2e32a6d48972b2c1976ed5d8967145b6cec4a4a9",
"shasum": ""
},
"require": {
- "php": "^5.5 || ^7.0",
- "phpdocumentor/reflection-common": "^1.0"
+ "php": "^7.1",
+ "phpdocumentor/reflection-common": "^2.0"
},
"require-dev": {
- "mockery/mockery": "^0.9.4",
- "phpunit/phpunit": "^5.2||^4.8.24"
+ "ext-tokenizer": "^7.1",
+ "mockery/mockery": "~1",
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "1.x-dev"
}
},
"autoload": {
"psr-4": {
- "phpDocumentor\\Reflection\\": [
- "src/"
- ]
+ "phpDocumentor\\Reflection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -1003,42 +1353,47 @@
"email": "me@mikevanriel.com"
}
],
- "time": "2017-07-14T14:27:02+00:00"
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/0.7.2"
+ },
+ "time": "2019-08-22T18:11:29+00:00"
},
{
"name": "phpspec/prophecy",
- "version": "1.8.0",
+ "version": "v1.10.3",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06"
+ "reference": "451c3cd1418cf640de218914901e51b064abb093"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
- "reference": "4ba436b55987b4bf311cb7c6ba82aa528aac0a06",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093",
+ "reference": "451c3cd1418cf640de218914901e51b064abb093",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
- "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
- "sebastian/comparator": "^1.1|^2.0|^3.0",
- "sebastian/recursion-context": "^1.0|^2.0|^3.0"
+ "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0",
+ "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0",
+ "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0"
},
"require-dev": {
- "phpspec/phpspec": "^2.5|^3.2",
+ "phpspec/phpspec": "^2.5 || ^3.2",
"phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.8.x-dev"
+ "dev-master": "1.10.x-dev"
}
},
"autoload": {
- "psr-0": {
- "Prophecy\\": "src/"
+ "psr-4": {
+ "Prophecy\\": "src/Prophecy"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -1066,44 +1421,48 @@
"spy",
"stub"
],
- "time": "2018-08-05T17:53:17+00:00"
+ "support": {
+ "issues": "https://github.com/phpspec/prophecy/issues",
+ "source": "https://github.com/phpspec/prophecy/tree/v1.10.3"
+ },
+ "time": "2020-03-05T15:02:03+00:00"
},
{
"name": "phpunit/php-code-coverage",
- "version": "4.0.8",
+ "version": "6.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
- "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d"
+ "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
- "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
+ "reference": "807e6013b00af69b6c5d9ceb4282d0393dbb9d8d",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-xmlwriter": "*",
- "php": "^5.6 || ^7.0",
- "phpunit/php-file-iterator": "^1.3",
- "phpunit/php-text-template": "^1.2",
- "phpunit/php-token-stream": "^1.4.2 || ^2.0",
- "sebastian/code-unit-reverse-lookup": "^1.0",
- "sebastian/environment": "^1.3.2 || ^2.0",
- "sebastian/version": "^1.0 || ^2.0"
+ "php": "^7.1",
+ "phpunit/php-file-iterator": "^2.0",
+ "phpunit/php-text-template": "^1.2.1",
+ "phpunit/php-token-stream": "^3.0",
+ "sebastian/code-unit-reverse-lookup": "^1.0.1",
+ "sebastian/environment": "^3.1 || ^4.0",
+ "sebastian/version": "^2.0.1",
+ "theseer/tokenizer": "^1.1"
},
"require-dev": {
- "ext-xdebug": "^2.1.4",
- "phpunit/phpunit": "^5.7"
+ "phpunit/phpunit": "^7.0"
},
"suggest": {
- "ext-xdebug": "^2.5.1"
+ "ext-xdebug": "^2.6.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "4.0.x-dev"
+ "dev-master": "6.1-dev"
}
},
"autoload": {
@@ -1118,7 +1477,7 @@
"authors": [
{
"name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
+ "email": "sebastian@phpunit.de",
"role": "lead"
}
],
@@ -1129,29 +1488,36 @@
"testing",
"xunit"
],
- "time": "2017-04-02T07:44:40+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/master"
+ },
+ "time": "2018-10-31T16:06:48+00:00"
},
{
"name": "phpunit/php-file-iterator",
- "version": "1.4.5",
+ "version": "2.0.5",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-file-iterator.git",
- "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4"
+ "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4",
- "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5",
+ "reference": "42c5ba5220e6904cbfe8b1a1bda7c0cfdc8c12f5",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.4.x-dev"
+ "dev-master": "2.0.x-dev"
}
},
"autoload": {
@@ -1166,7 +1532,7 @@
"authors": [
{
"name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
+ "email": "sebastian@phpunit.de",
"role": "lead"
}
],
@@ -1176,32 +1542,42 @@
"filesystem",
"iterator"
],
- "time": "2017-11-27T13:52:08+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2021-12-02T12:42:26+00:00"
},
{
"name": "phpunit/php-timer",
- "version": "1.0.9",
+ "version": "2.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-timer.git",
- "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f"
+ "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
- "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662",
+ "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662",
"shasum": ""
},
"require": {
- "php": "^5.3.3 || ^7.0"
+ "php": ">=7.1"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+ "phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-master": "2.1-dev"
}
},
"autoload": {
@@ -1216,7 +1592,7 @@
"authors": [
{
"name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
+ "email": "sebastian@phpunit.de",
"role": "lead"
}
],
@@ -1225,33 +1601,43 @@
"keywords": [
"timer"
],
- "time": "2017-02-26T11:10:40+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T08:20:02+00:00"
},
{
"name": "phpunit/php-token-stream",
- "version": "1.4.12",
+ "version": "3.1.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
- "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16"
+ "reference": "9c1da83261628cb24b6a6df371b6e312b3954768"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16",
- "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9c1da83261628cb24b6a6df371b6e312b3954768",
+ "reference": "9c1da83261628cb24b6a6df371b6e312b3954768",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
- "php": ">=5.3.3"
+ "php": ">=7.1"
},
"require-dev": {
- "phpunit/phpunit": "~4.2"
+ "phpunit/phpunit": "^7.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.4-dev"
+ "dev-master": "3.1-dev"
}
},
"autoload": {
@@ -1274,107 +1660,68 @@
"keywords": [
"tokenizer"
],
- "time": "2017-12-04T08:55:13+00:00"
- },
- {
- "name": "phpunit/phpcov",
- "version": "3.1.0",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/phpcov.git",
- "reference": "2005bd90c2c8aae6d93ec82d9cda9d55dca96c3d"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
+ "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.3"
},
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpcov/zipball/2005bd90c2c8aae6d93ec82d9cda9d55dca96c3d",
- "reference": "2005bd90c2c8aae6d93ec82d9cda9d55dca96c3d",
- "shasum": ""
- },
- "require": {
- "php": "^5.6 || ^7.0",
- "phpunit/php-code-coverage": "^4.0",
- "phpunit/phpunit": "^5.0",
- "sebastian/diff": "^1.1",
- "sebastian/finder-facade": "^1.1",
- "sebastian/version": "^1.0|^2.0",
- "symfony/console": "^2|^3"
- },
- "bin": [
- "phpcov"
- ],
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.1.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
+ "funding": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
}
],
- "description": "CLI frontend for PHP_CodeCoverage",
- "homepage": "https://github.com/sebastianbergmann/phpcov",
- "time": "2016-06-03T07:01:55+00:00"
+ "abandoned": true,
+ "time": "2021-07-26T12:15:06+00:00"
},
{
"name": "phpunit/phpunit",
- "version": "5.7.27",
+ "version": "7.5.20",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
- "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c"
+ "reference": "9467db479d1b0487c99733bb1e7944d32deded2c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c",
- "reference": "b7803aeca3ccb99ad0a506fa80b64cd6a56bbc0c",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9467db479d1b0487c99733bb1e7944d32deded2c",
+ "reference": "9467db479d1b0487c99733bb1e7944d32deded2c",
"shasum": ""
},
"require": {
+ "doctrine/instantiator": "^1.1",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-xml": "*",
- "myclabs/deep-copy": "~1.3",
- "php": "^5.6 || ^7.0",
- "phpspec/prophecy": "^1.6.2",
- "phpunit/php-code-coverage": "^4.0.4",
- "phpunit/php-file-iterator": "~1.4",
- "phpunit/php-text-template": "~1.2",
- "phpunit/php-timer": "^1.0.6",
- "phpunit/phpunit-mock-objects": "^3.2",
- "sebastian/comparator": "^1.2.4",
- "sebastian/diff": "^1.4.3",
- "sebastian/environment": "^1.3.4 || ^2.0",
- "sebastian/exporter": "~2.0",
- "sebastian/global-state": "^1.1",
- "sebastian/object-enumerator": "~2.0",
- "sebastian/resource-operations": "~1.0",
- "sebastian/version": "^1.0.6|^2.0.1",
- "symfony/yaml": "~2.1|~3.0|~4.0"
+ "myclabs/deep-copy": "^1.7",
+ "phar-io/manifest": "^1.0.2",
+ "phar-io/version": "^2.0",
+ "php": "^7.1",
+ "phpspec/prophecy": "^1.7",
+ "phpunit/php-code-coverage": "^6.0.7",
+ "phpunit/php-file-iterator": "^2.0.1",
+ "phpunit/php-text-template": "^1.2.1",
+ "phpunit/php-timer": "^2.1",
+ "sebastian/comparator": "^3.0",
+ "sebastian/diff": "^3.0",
+ "sebastian/environment": "^4.0",
+ "sebastian/exporter": "^3.1",
+ "sebastian/global-state": "^2.0",
+ "sebastian/object-enumerator": "^3.0.3",
+ "sebastian/resource-operations": "^2.0",
+ "sebastian/version": "^2.0.1"
},
"conflict": {
- "phpdocumentor/reflection-docblock": "3.0.2"
+ "phpunit/phpunit-mock-objects": "*"
},
"require-dev": {
"ext-pdo": "*"
},
"suggest": {
+ "ext-soap": "*",
"ext-xdebug": "*",
- "phpunit/php-invoker": "~1.1"
+ "phpunit/php-invoker": "^2.0"
},
"bin": [
"phpunit"
@@ -1382,7 +1729,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "5.7.x-dev"
+ "dev-master": "7.5-dev"
}
},
"autoload": {
@@ -1408,67 +1755,11 @@
"testing",
"xunit"
],
- "time": "2018-02-01T05:50:59+00:00"
- },
- {
- "name": "phpunit/phpunit-mock-objects",
- "version": "3.4.4",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
- "reference": "a23b761686d50a560cc56233b9ecf49597cc9118"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/7.5.20"
},
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/a23b761686d50a560cc56233b9ecf49597cc9118",
- "reference": "a23b761686d50a560cc56233b9ecf49597cc9118",
- "shasum": ""
- },
- "require": {
- "doctrine/instantiator": "^1.0.2",
- "php": "^5.6 || ^7.0",
- "phpunit/php-text-template": "^1.2",
- "sebastian/exporter": "^1.2 || ^2.0"
- },
- "conflict": {
- "phpunit/phpunit": "<5.4.0"
- },
- "require-dev": {
- "phpunit/phpunit": "^5.4"
- },
- "suggest": {
- "ext-soap": "*"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.2.x-dev"
- }
- },
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
- {
- "name": "Sebastian Bergmann",
- "email": "sb@sebastian-bergmann.de",
- "role": "lead"
- }
- ],
- "description": "Mock Object library for PHPUnit",
- "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/",
- "keywords": [
- "mock",
- "xunit"
- ],
- "abandoned": true,
- "time": "2017-06-30T09:13:00+00:00"
+ "time": "2020-01-08T08:45:45+00:00"
},
{
"name": "roave/security-advisories",
@@ -1476,172 +1767,463 @@
"source": {
"type": "git",
"url": "https://github.com/Roave/SecurityAdvisories.git",
- "reference": "d155baccb43ba2542941fbcba258b85ce7786419"
+ "reference": "773292d413a97c357a0b49635afd5fdb1d4f314a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/d155baccb43ba2542941fbcba258b85ce7786419",
- "reference": "d155baccb43ba2542941fbcba258b85ce7786419",
+ "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/773292d413a97c357a0b49635afd5fdb1d4f314a",
+ "reference": "773292d413a97c357a0b49635afd5fdb1d4f314a",
"shasum": ""
},
"conflict": {
"3f/pygmentize": "<1.2",
- "adodb/adodb-php": "<5.20.12",
+ "admidio/admidio": "<4.1.9",
+ "adodb/adodb-php": "<=5.20.20|>=5.21,<=5.21.3",
+ "akaunting/akaunting": "<2.1.13",
+ "alextselegidis/easyappointments": "<=1.4.3",
"alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1",
+ "amazing/media2click": ">=1,<1.3.3",
"amphp/artax": "<1.0.6|>=2,<2.0.6",
"amphp/http": "<1.0.1",
+ "amphp/http-client": ">=4,<4.4",
+ "anchorcms/anchor-cms": "<=0.12.7",
+ "andreapollastri/cipi": "<=3.1.15",
"api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6",
+ "appwrite/server-ce": "<0.11.1|>=0.12,<0.12.2",
+ "area17/twill": "<1.2.5|>=2,<2.5.3",
"asymmetricrypt/asymmetricrypt": ">=0,<9.9.99",
"aws/aws-sdk-php": ">=3,<3.2.1",
+ "bagisto/bagisto": "<0.1.5",
+ "barrelstrength/sprout-base-email": "<1.2.7",
+ "barrelstrength/sprout-forms": "<3.9",
+ "barryvdh/laravel-translation-manager": "<0.6.2",
+ "baserproject/basercms": "<4.5.4",
+ "billz/raspap-webgui": "<=2.6.6",
+ "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3",
+ "bmarshall511/wordpress_zero_spam": "<5.2.13",
+ "bolt/bolt": "<3.7.2",
+ "bolt/core": "<=4.2",
+ "bottelet/flarepoint": "<2.2.1",
"brightlocal/phpwhois": "<=4.2.5",
+ "brotkrueml/codehighlight": "<2.7",
+ "brotkrueml/schema": "<1.13.1|>=2,<2.5.1",
+ "brotkrueml/typo3-matomo-integration": "<1.3.2",
+ "buddypress/buddypress": "<7.2.1",
"bugsnag/bugsnag-laravel": ">=2,<2.0.2",
- "cakephp/cakephp": ">=1.3,<1.3.18|>=2,<2.4.99|>=2.5,<2.5.99|>=2.6,<2.6.12|>=2.7,<2.7.6|>=3,<3.0.15|>=3.1,<3.1.4|>=3.4,<3.4.14|>=3.5,<3.5.17|>=3.6,<3.6.4",
+ "bytefury/crater": "<6.0.2",
+ "cachethq/cachet": "<2.5.1",
+ "cakephp/cakephp": "<3.10.3|>=4,<4.0.6",
+ "cardgate/magento2": "<2.0.33",
"cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4",
"cartalyst/sentry": "<=2.1.6",
+ "catfan/medoo": "<1.7.5",
+ "centreon/centreon": "<20.10.7",
+ "cesnet/simplesamlphp-module-proxystatistics": "<3.1",
+ "codeception/codeception": "<3.1.3|>=4,<4.1.22",
"codeigniter/framework": "<=3.0.6",
- "composer/composer": "<=1.0.0-alpha11",
+ "codeigniter4/framework": "<4.1.9",
+ "codiad/codiad": "<=2.8.4",
+ "composer/composer": "<1.10.26|>=2-alpha.1,<2.2.12|>=2.3,<2.3.5",
+ "concrete5/concrete5": "<9",
+ "concrete5/core": "<8.5.8|>=9,<9.1",
"contao-components/mediaelement": ">=2.14.2,<2.21.1",
- "contao/core": ">=2,<3.5.35",
- "contao/core-bundle": ">=4,<4.4.18|>=4.5,<4.5.8",
+ "contao/contao": ">=4,<4.4.56|>=4.5,<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3",
+ "contao/core": ">=2,<3.5.39",
+ "contao/core-bundle": "<4.9.18|>=4.10,<4.11.7|>=4.13,<4.13.3|= 4.10.0",
"contao/listing-bundle": ">=4,<4.4.8",
- "contao/newsletter-bundle": ">=4,<4.1",
+ "contao/managed-edition": "<=1.5",
+ "craftcms/cms": "<3.7.36",
+ "croogo/croogo": "<3.0.7",
+ "cuyz/valinor": "<0.12",
+ "czproject/git-php": "<4.0.3",
+ "darylldoyle/safe-svg": "<1.9.10",
+ "datadog/dd-trace": ">=0.30,<0.30.2",
"david-garcia/phpwhois": "<=4.3.1",
+ "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1",
+ "directmailteam/direct-mail": "<5.2.4",
"doctrine/annotations": ">=1,<1.2.7",
"doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2",
"doctrine/common": ">=2,<2.4.3|>=2.5,<2.5.1",
- "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2",
+ "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2|>=3,<3.1.4",
"doctrine/doctrine-bundle": "<1.5.2",
"doctrine/doctrine-module": "<=0.7.1",
"doctrine/mongodb-odm": ">=1,<1.0.2",
"doctrine/mongodb-odm-bundle": ">=2,<3.0.1",
- "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1",
- "dompdf/dompdf": ">=0.6,<0.6.2",
- "drupal/core": ">=7,<7.60|>=8,<8.5.8|>=8.6,<8.6.2",
- "drupal/drupal": ">=7,<7.60|>=8,<8.5.8|>=8.6,<8.6.2",
- "erusev/parsedown": "<1.7",
- "ezsystems/ezpublish-kernel": ">=5.3,<5.3.12.1|>=5.4,<5.4.13.1|>=6,<6.7.9.1|>=6.8,<6.13.5.1|>=7,<7.2.4.1|>=7.3,<7.3.2.1",
- "ezsystems/ezpublish-legacy": ">=5.3,<5.3.12.6|>=5.4,<5.4.12.3|>=2011,<2017.12.4.3|>=2018.6,<2018.6.1.4|>=2018.9,<2018.9.1.3",
+ "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4",
+ "dolibarr/dolibarr": "<16|= 12.0.5|>= 3.3.beta1, < 13.0.2",
+ "dompdf/dompdf": "<2",
+ "drupal/core": ">=7,<7.91|>=8,<9.3.19|>=9.4,<9.4.3",
+ "drupal/drupal": ">=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4",
+ "dweeves/magmi": "<=0.7.24",
+ "ecodev/newsletter": "<=4",
+ "ectouch/ectouch": "<=2.7.2",
+ "elefant/cms": "<1.3.13",
+ "elgg/elgg": "<3.3.24|>=4,<4.0.5",
+ "endroid/qr-code-bundle": "<3.4.2",
+ "enshrined/svg-sanitize": "<0.15",
+ "erusev/parsedown": "<1.7.2",
+ "ether/logs": "<3.0.4",
+ "ezsystems/demobundle": ">=5.4,<5.4.6.1",
+ "ezsystems/ez-support-tools": ">=2.2,<2.2.3",
+ "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1",
+ "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1|>=5.4,<5.4.11.1|>=2017.12,<2017.12.0.1",
+ "ezsystems/ezplatform": "<=1.13.6|>=2,<=2.5.24",
+ "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.27",
+ "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1",
+ "ezsystems/ezplatform-kernel": "<=1.2.5|>=1.3,<1.3.19",
+ "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8",
+ "ezsystems/ezplatform-richtext": ">=2.3,<=2.3.7",
+ "ezsystems/ezplatform-user": ">=1,<1.0.1",
+ "ezsystems/ezpublish-kernel": "<=6.13.8.1|>=7,<7.5.29",
+ "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.3.5.1",
+ "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3",
"ezsystems/repository-forms": ">=2.3,<2.3.2.1",
"ezyang/htmlpurifier": "<4.1.1",
+ "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2",
+ "facturascripts/facturascripts": "<=2022.8",
+ "feehi/cms": "<=2.1.1",
+ "feehi/feehicms": "<=0.1.3",
+ "fenom/fenom": "<=2.12.1",
+ "filegator/filegator": "<7.8",
"firebase/php-jwt": "<2",
+ "flarum/core": ">=1,<=1.0.1",
+ "flarum/sticky": ">=0.1-beta.14,<=0.1-beta.15",
+ "flarum/tags": "<=0.1-beta.13",
+ "fluidtypo3/vhs": "<5.1.1",
+ "fof/byobu": ">=0.3-beta.2,<1.1.7",
+ "fof/upload": "<1.2.3",
"fooman/tcpdf": "<6.2.22",
+ "forkcms/forkcms": "<5.11.1",
"fossar/tcpdf-parser": "<6.2.22",
+ "francoisjacquet/rosariosis": "<9.1",
+ "friendsofsymfony/oauth2-php": "<1.3",
"friendsofsymfony/rest-bundle": ">=1.2,<1.2.2",
"friendsofsymfony/user-bundle": ">=1.2,<1.3.5",
+ "friendsoftypo3/mediace": ">=7.6.2,<7.6.5",
+ "froala/wysiwyg-editor": "<3.2.7",
+ "froxlor/froxlor": "<=0.10.22",
"fuel/core": "<1.8.1",
+ "gaoming13/wechat-php-sdk": "<=1.10.2",
+ "genix/cms": "<=1.1.11",
+ "getgrav/grav": "<1.7.34",
+ "getkirby/cms": "<3.5.8",
+ "getkirby/panel": "<2.5.14",
+ "gilacms/gila": "<=1.11.4",
+ "globalpayments/php-sdk": "<2",
+ "google/protobuf": "<3.15",
+ "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3",
"gree/jose": "<=2.2",
"gregwar/rst": "<1.0.3",
- "guzzlehttp/guzzle": ">=6,<6.2.1|>=4.0.0-rc2,<4.2.4|>=5,<5.3.1",
+ "grumpydictator/firefly-iii": "<5.6.5",
+ "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5",
+ "guzzlehttp/psr7": "<1.8.4|>=2,<2.1.1",
+ "helloxz/imgurl": "= 2.31|<=2.31",
+ "hillelcoren/invoice-ninja": "<5.3.35",
+ "hjue/justwriting": "<=1",
+ "hov/jobfair": "<1.0.13|>=2,<2.0.2",
+ "hyn/multi-tenant": ">=5.6,<5.7.2",
+ "ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4",
+ "ibexa/post-install": "<=1.0.4",
+ "icecoder/icecoder": "<=8.1",
+ "idno/known": "<=1.3.1",
"illuminate/auth": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.10",
- "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30",
- "illuminate/database": ">=4,<4.0.99|>=4.1,<4.1.29",
+ "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.99999|>=4.2,<=4.2.99999|>=5,<=5.0.99999|>=5.1,<=5.1.99999|>=5.2,<=5.2.99999|>=5.3,<=5.3.99999|>=5.4,<=5.4.99999|>=5.5,<=5.5.49|>=5.6,<=5.6.99999|>=5.7,<=5.7.99999|>=5.8,<=5.8.99999|>=6,<6.18.31|>=7,<7.22.4",
+ "illuminate/database": "<6.20.26|>=7,<7.30.5|>=8,<8.40",
"illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15",
+ "illuminate/view": "<6.20.42|>=7,<7.30.6|>=8,<8.75",
+ "impresscms/impresscms": "<=1.4.3",
+ "in2code/femanager": "<5.5.1|>=6,<6.3.1",
+ "in2code/lux": "<17.6.1|>=18,<24.0.2",
+ "intelliants/subrion": "<=4.2.1",
+ "islandora/islandora": ">=2,<2.4.1",
"ivankristianto/phpwhois": "<=4.3",
- "james-heinrich/getid3": "<1.9.9",
+ "jackalope/jackalope-doctrine-dbal": "<1.7.4",
+ "james-heinrich/getid3": "<1.9.21",
+ "joomla/archive": "<1.1.12|>=2,<2.0.1",
+ "joomla/filesystem": "<1.6.2|>=2,<2.0.1",
+ "joomla/filter": "<1.4.4|>=2,<2.0.1",
+ "joomla/input": ">=2,<2.0.2",
"joomla/session": "<1.3.1",
+ "jsdecena/laracom": "<2.0.9",
"jsmitty12/phpwhois": "<5.1",
"kazist/phpwhois": "<=4.2.6",
+ "kevinpapst/kimai2": "<1.16.7",
+ "kitodo/presentation": "<3.1.2",
+ "klaviyo/magento2-extension": ">=1,<3",
+ "krayin/laravel-crm": "<1.2.2",
"kreait/firebase-php": ">=3.2,<3.8.1",
"la-haute-societe/tcpdf": "<6.2.22",
- "laravel/framework": ">=4,<4.0.99|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.42|>=5.6,<5.6.30",
+ "laminas/laminas-diactoros": "<2.11.1",
+ "laminas/laminas-form": "<2.17.1|>=3,<3.0.2|>=3.1,<3.1.1",
+ "laminas/laminas-http": "<2.14.2",
+ "laravel/fortify": "<1.11.1",
+ "laravel/framework": "<6.20.42|>=7,<7.30.6|>=8,<8.75",
+ "laravel/laravel": "<=9.1.8",
"laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10",
- "league/commonmark": ">=0.15.6,<0.18.1",
- "magento/magento1ce": "<1.9.4",
- "magento/magento1ee": ">=1.9,<1.14.4",
- "magento/product-community-edition": ">=2,<2.2.7",
+ "latte/latte": "<2.10.8",
+ "lavalite/cms": "<=5.8",
+ "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5",
+ "league/commonmark": "<0.18.3",
+ "league/flysystem": "<1.1.4|>=2,<2.1.1",
+ "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3",
+ "librenms/librenms": "<22.4",
+ "limesurvey/limesurvey": "<3.27.19",
+ "livehelperchat/livehelperchat": "<=3.91",
+ "livewire/livewire": ">2.2.4,<2.2.6",
+ "lms/routes": "<2.1.1",
+ "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2",
+ "luyadev/yii-helpers": "<1.2.1",
+ "magento/community-edition": ">=2,<2.2.10|>=2.3,<2.3.3",
+ "magento/magento1ce": "<1.9.4.3",
+ "magento/magento1ee": ">=1,<1.14.4.3",
+ "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2-p.2",
+ "marcwillmann/turn": "<0.3.3",
+ "matyhtf/framework": "<3.0.6",
+ "mautic/core": "<4.3|= 2.13.1",
+ "mediawiki/core": ">=1.27,<1.27.6|>=1.29,<1.29.3|>=1.30,<1.30.2|>=1.31,<1.31.9|>=1.32,<1.32.6|>=1.32.99,<1.33.3|>=1.33.99,<1.34.3|>=1.34.99,<1.35",
+ "mezzio/mezzio-swoole": "<3.7|>=4,<4.3",
+ "microweber/microweber": "<1.3.1",
+ "miniorange/miniorange-saml": "<1.4.3",
+ "mittwald/typo3_forum": "<1.2.1",
+ "modx/revolution": "<= 2.8.3-pl|<2.8",
+ "mojo42/jirafeau": "<4.4",
"monolog/monolog": ">=1.8,<1.12",
+ "moodle/moodle": "<4.0.1",
+ "mustache/mustache": ">=2,<2.14.1",
"namshi/jose": "<2.2",
+ "neoan3-apps/template": "<1.1.1",
+ "neorazorx/facturascripts": "<2022.4",
+ "neos/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6",
+ "neos/form": ">=1.2,<4.3.3|>=5,<5.0.9|>=5.1,<5.1.3",
+ "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.9.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<5.3.10|>=7,<7.0.9|>=7.1,<7.1.7|>=7.2,<7.2.6|>=7.3,<7.3.4|>=8,<8.0.2",
+ "neos/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5",
+ "netgen/tagsbundle": ">=3.4,<3.4.11|>=4,<4.0.15",
+ "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6",
+ "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13",
+ "nilsteampassnet/teampass": "<=2.1.27.36",
+ "noumo/easyii": "<=0.9",
+ "nukeviet/nukeviet": "<4.5.2",
+ "nystudio107/craft-seomatic": "<3.4.12",
+ "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1",
+ "october/backend": "<1.1.2",
+ "october/cms": "= 1.1.1|= 1.0.471|= 1.0.469|>=1.0.319,<1.0.469",
+ "october/october": ">=1.0.319,<1.0.466|>=2.1,<2.1.12",
+ "october/rain": "<1.0.472|>=1.1,<1.1.2",
+ "october/system": "<1.0.476|>=1.1,<1.1.12|>=2,<2.2.15",
"onelogin/php-saml": "<2.10.4",
+ "oneup/uploader-bundle": "<1.9.3|>=2,<2.1.5",
+ "open-web-analytics/open-web-analytics": "<1.7.4",
+ "opencart/opencart": "<=3.0.3.2",
"openid/php-openid": "<2.3",
- "oro/crm": ">=1.7,<1.7.4",
- "oro/platform": ">=1.7,<1.7.4",
+ "openmage/magento-lts": "<19.4.15|>=20,<20.0.13",
+ "orchid/platform": ">=9,<9.4.4",
+ "oro/commerce": ">=5,<5.0.4",
+ "oro/crm": ">=1.7,<1.7.4|>=3.1,<4.1.17|>=4.2,<4.2.7",
+ "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<4.2.8",
+ "packbackbooks/lti-1-3-php-library": "<5",
"padraic/humbug_get_contents": "<1.1.2",
"pagarme/pagarme-php": ">=0,<3",
+ "pagekit/pagekit": "<=1.0.18",
"paragonie/random_compat": "<2",
+ "passbolt/passbolt_api": "<2.11",
"paypal/merchant-sdk-php": "<3.12",
- "pear/archive_tar": "<1.4.4",
- "phpmailer/phpmailer": ">=5,<5.2.27|>=6,<6.0.6",
- "phpoffice/phpexcel": "<=1.8.1",
- "phpoffice/phpspreadsheet": "<=1.5",
- "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3",
+ "pear/archive_tar": "<1.4.14",
+ "pear/crypt_gpg": "<1.6.7",
+ "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1",
+ "personnummer/personnummer": "<3.0.2",
+ "phanan/koel": "<5.1.4",
+ "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7",
+ "phpmailer/phpmailer": "<6.5",
+ "phpmussel/phpmussel": ">=1,<1.6",
+ "phpmyadmin/phpmyadmin": "<5.1.3",
+ "phpoffice/phpexcel": "<1.8",
+ "phpoffice/phpspreadsheet": "<1.16",
+ "phpseclib/phpseclib": "<2.0.31|>=3,<3.0.7",
+ "phpservermon/phpservermon": "<=3.5.2",
+ "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5,<5.6.3",
"phpwhois/phpwhois": "<=4.2.5",
"phpxmlrpc/extras": "<0.6.1",
- "propel/propel": ">=2.0.0-alpha1,<=2.0.0-alpha7",
+ "pimcore/data-hub": "<1.2.4",
+ "pimcore/pimcore": "<10.4.4",
+ "pocketmine/bedrock-protocol": "<8.0.2",
+ "pocketmine/pocketmine-mp": ">= 4.0.0-BETA5, < 4.4.2|<4.2.10",
+ "pressbooks/pressbooks": "<5.18",
+ "prestashop/autoupgrade": ">=4,<4.10.1",
+ "prestashop/blockwishlist": ">=2,<2.1.1",
+ "prestashop/contactform": ">1.0.1,<4.3",
+ "prestashop/gamification": "<2.3.2",
+ "prestashop/prestashop": ">=1.6.0.10,<1.7.8.7",
+ "prestashop/productcomments": ">=4,<4.2.1",
+ "prestashop/ps_emailsubscription": "<2.6.1",
+ "prestashop/ps_facetedsearch": "<3.4.1",
+ "prestashop/ps_linklist": "<3.1",
+ "privatebin/privatebin": "<1.4",
+ "propel/propel": ">=2-alpha.1,<=2-alpha.7",
"propel/propel1": ">=1,<=1.7.1",
+ "pterodactyl/panel": "<1.7",
+ "ptrofimov/beanstalk_console": "<1.7.14",
"pusher/pusher-php-server": "<2.2.1",
- "robrichards/xmlseclibs": ">=1,<3.0.2",
+ "pwweb/laravel-core": "<=0.3.6-beta",
+ "rainlab/debugbar-plugin": "<3.1",
+ "remdex/livehelperchat": "<3.99",
+ "rmccue/requests": ">=1.6,<1.8",
+ "robrichards/xmlseclibs": "<3.0.4",
+ "rudloff/alltube": "<3.0.3",
+ "s-cart/core": "<6.9",
+ "s-cart/s-cart": "<6.9",
+ "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1",
"sabre/dav": ">=1.6,<1.6.99|>=1.7,<1.7.11|>=1.8,<1.8.9",
+ "scheb/two-factor-bundle": ">=0,<3.26|>=4,<4.11",
"sensiolabs/connect": "<4.2.3",
"serluck/phpwhois": "<=4.2.6",
- "shopware/shopware": "<5.3.7",
- "silverstripe/cms": ">=3,<=3.0.11|>=3.1,<3.1.11",
+ "shopware/core": "<=6.4.9",
+ "shopware/platform": "<=6.4.9",
+ "shopware/production": "<=6.3.5.2",
+ "shopware/shopware": "<=5.7.13",
+ "shopware/storefront": "<=6.4.8.1",
+ "shopxo/shopxo": "<2.2.6",
+ "showdoc/showdoc": "<2.10.4",
+ "silverstripe/admin": ">=1,<1.8.1",
+ "silverstripe/assets": ">=1,<1.10.1",
+ "silverstripe/cms": "<4.3.6|>=4.4,<4.4.4",
+ "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1",
"silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3",
- "silverstripe/framework": ">=3,<3.3",
+ "silverstripe/framework": "<4.10.9",
+ "silverstripe/graphql": "<3.5.2|>=4-alpha.1,<4-alpha.2|= 4.0.0-alpha1",
+ "silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1",
+ "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1",
+ "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4",
+ "silverstripe/silverstripe-omnipay": "<2.5.2|>=3,<3.0.2|>=3.1,<3.1.4|>=3.2,<3.2.1",
+ "silverstripe/subsites": ">=2,<2.1.1",
+ "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1",
"silverstripe/userforms": "<3",
"simple-updates/phpwhois": "<=1",
"simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4",
- "simplesamlphp/simplesamlphp": "<1.16.3",
+ "simplesamlphp/simplesamlphp": "<1.18.6",
"simplesamlphp/simplesamlphp-module-infocard": "<1.0.1",
+ "simplito/elliptic-php": "<1.0.6",
"slim/slim": "<2.6",
- "smarty/smarty": "<3.1.33",
+ "smarty/smarty": "<3.1.45|>=4,<4.1.1",
+ "snipe/snipe-it": "<=6.0.2|>= 6.0.0-RC-1, <= 6.0.0-RC-5",
"socalnick/scn-social-auth": "<1.15.2",
+ "socialiteproviders/steam": "<1.1",
+ "spipu/html2pdf": "<5.2.4",
"spoonity/tcpdf": "<6.2.22",
"squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1",
+ "ssddanbrown/bookstack": "<22.2.3",
+ "statamic/cms": "<3.2.39|>=3.3,<3.3.2",
"stormpath/sdk": ">=0,<9.9.99",
+ "studio-42/elfinder": "<2.1.59",
+ "subrion/cms": "<=4.2.1",
+ "sulu/sulu": "= 2.4.0-RC1|<1.6.44|>=2,<2.2.18|>=2.3,<2.3.8",
"swiftmailer/swiftmailer": ">=4,<5.4.5",
"sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
- "sylius/sylius": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2",
- "symfony/dependency-injection": ">=2,<2.0.17",
+ "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1",
+ "sylius/grid-bundle": "<1.10.1",
+ "sylius/paypal-plugin": ">=1,<1.2.4|>=1.3,<1.3.1",
+ "sylius/resource-bundle": "<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4",
+ "sylius/sylius": "<1.9.10|>=1.10,<1.10.11|>=1.11,<1.11.2",
+ "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99",
+ "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4",
+ "symbiote/silverstripe-versionedfiles": "<=2.0.3",
+ "symfont/process": ">=0,<4",
+ "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8",
+ "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
+ "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4",
"symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
- "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2",
- "symfony/http-foundation": ">=2,<2.7.49|>=2.8,<2.8.44|>=3,<3.3.18|>=3.4,<3.4.14|>=4,<4.0.14|>=4.1,<4.1.3",
- "symfony/http-kernel": ">=2,<2.3.29|>=2.4,<2.5.12|>=2.6,<2.6.8",
+ "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<=5.3.14|>=5.4.3,<=5.4.3|>=6.0.3,<=6.0.3|= 6.0.3|= 5.4.3|= 5.3.14",
+ "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7",
+ "symfony/http-kernel": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.4.13|>=5,<5.1.5|>=5.2,<5.3.12",
"symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13",
+ "symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1",
+ "symfony/mime": ">=4.3,<4.3.8",
+ "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
"symfony/polyfill": ">=1,<1.10",
"symfony/polyfill-php55": ">=1,<1.10",
+ "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7",
"symfony/routing": ">=2,<2.0.19",
- "symfony/security": ">=2,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.19|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
- "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
- "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<2.8.37|>=3,<3.3.17|>=3.4,<3.4.7|>=4,<4.0.7",
+ "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8",
+ "symfony/security-bundle": ">=2,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11|>=5.3,<5.3.12",
+ "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9",
"symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
- "symfony/security-guard": ">=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11",
- "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
- "symfony/serializer": ">=2,<2.0.11",
- "symfony/symfony": ">=2,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1",
+ "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8",
+ "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.3.2",
+ "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12",
+ "symfony/symfony": ">=2,<3.4.49|>=4,<4.4.35|>=5,<5.3.12|>=5.3.14,<=5.3.14|>=5.4.3,<=5.4.3|>=6.0.3,<=6.0.3",
"symfony/translation": ">=2,<2.0.17",
"symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3",
+ "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8",
"symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4",
"symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7",
+ "t3/dce": ">=2.2,<2.6.2",
+ "t3g/svg-sanitizer": "<1.0.3",
+ "tastyigniter/tastyigniter": "<3.3",
"tecnickcom/tcpdf": "<6.2.22",
+ "terminal42/contao-tablelookupwizard": "<3.3.5",
"thelia/backoffice-default-template": ">=2.1,<2.1.2",
- "thelia/thelia": ">=2.1.0-beta1,<2.1.3|>=2.1,<2.1.2",
+ "thelia/thelia": ">=2.1-beta.1,<2.1.3",
"theonedemon/phpwhois": "<=4.2.5",
+ "thinkcmf/thinkcmf": "<=5.1.7",
+ "tinymce/tinymce": "<5.10",
"titon/framework": ">=0,<9.9.99",
+ "topthink/framework": "<=6.0.12",
+ "topthink/think": "<=6.0.9",
+ "topthink/thinkphp": "<=3.2.3",
+ "tribalsystems/zenario": "<9.2.55826",
"truckersmp/phpwhois": "<=4.3.1",
- "twig/twig": "<1.20",
- "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.21|>=9,<9.5.2",
- "typo3/cms-core": ">=8,<8.7.21|>=9,<9.5.2",
- "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.10|>=3.1,<3.1.7|>=3.2,<3.2.7|>=3.3,<3.3.5",
- "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4",
+ "twig/twig": "<1.38|>=2,<2.14.11|>=3,<3.3.8",
+ "typo3/cms": ">=6.2,<6.2.30|>=7,<7.6.32|>=8,<8.7.38|>=9,<9.5.29|>=10,<10.4.29|>=11,<11.5.11",
+ "typo3/cms-backend": ">=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1",
+ "typo3/cms-core": ">=6.2,<=6.2.56|>=7,<7.6.57|>=8,<8.7.47|>=9,<9.5.35|>=10,<10.4.29|>=11,<11.5.11",
+ "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1",
+ "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6",
+ "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3",
+ "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1",
+ "typo3/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5",
+ "typo3fluid/fluid": ">=2,<2.0.8|>=2.1,<2.1.7|>=2.2,<2.2.4|>=2.3,<2.3.7|>=2.4,<2.4.4|>=2.5,<2.5.11|>=2.6,<2.6.10",
"ua-parser/uap-php": "<3.8",
+ "unisharp/laravel-filemanager": "<=2.3",
+ "userfrosting/userfrosting": ">=0.3.1,<4.6.3",
+ "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2",
+ "vanilla/safecurl": "<0.9.2",
+ "verot/class.upload.php": "<=1.0.3|>=2,<=2.0.4",
+ "vrana/adminer": "<4.8.1",
"wallabag/tcpdf": "<6.2.22",
+ "wanglelecc/laracms": "<=1.0.3",
+ "web-auth/webauthn-framework": ">=3.3,<3.3.4",
+ "webcoast/deferred-image-processing": "<1.0.2",
+ "wikimedia/parsoid": "<0.12.2",
"willdurand/js-translation-bundle": "<2.1.1",
+ "wintercms/winter": "<1.0.475|>=1.1,<1.1.9",
+ "woocommerce/woocommerce": "<6.6",
+ "wp-cli/wp-cli": "<2.5",
+ "wp-graphql/wp-graphql": "<0.3.5",
+ "wpanel/wpanel4-cms": "<=4.3.1",
+ "wwbn/avideo": "<=11.6",
+ "yeswiki/yeswiki": "<4.1",
+ "yetiforce/yetiforce-crm": "<6.4",
+ "yidashi/yii2cmf": "<=2",
+ "yii2mod/yii2-cms": "<1.9.2",
"yiisoft/yii": ">=1.1.14,<1.1.15",
- "yiisoft/yii2": "<2.0.15",
+ "yiisoft/yii2": "<2.0.38",
"yiisoft/yii2-bootstrap": "<2.0.4",
- "yiisoft/yii2-dev": "<2.0.15",
+ "yiisoft/yii2-dev": "<2.0.43",
"yiisoft/yii2-elasticsearch": "<2.0.5",
"yiisoft/yii2-gii": "<2.0.4",
"yiisoft/yii2-jui": "<2.0.4",
"yiisoft/yii2-redis": "<2.0.8",
+ "yoast-seo-for-typo3/yoast_seo": "<7.2.3",
+ "yourls/yourls": "<=1.8.2",
+ "zendesk/zendesk_api_client_php": "<2.2.11",
"zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3",
"zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2",
"zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2",
"zendframework/zend-db": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.10|>=2.3,<2.3.5",
- "zendframework/zend-diactoros": ">=1,<1.8.4",
- "zendframework/zend-feed": ">=1,<2.10.3",
+ "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3",
+ "zendframework/zend-diactoros": "<1.8.4",
+ "zendframework/zend-feed": "<2.10.3",
"zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1",
- "zendframework/zend-http": ">=1,<2.8.1",
+ "zendframework/zend-http": "<2.8.1",
"zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6",
"zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3",
"zendframework/zend-mail": ">=2,<2.4.11|>=2.5,<2.7.2",
@@ -1650,14 +2232,15 @@
"zendframework/zend-validator": ">=2.3,<2.3.6",
"zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1",
"zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6",
- "zendframework/zendframework": "<2.5.1",
+ "zendframework/zendframework": "<=3",
"zendframework/zendframework1": "<1.12.20",
"zendframework/zendopenid": ">=2,<2.0.2",
"zendframework/zendxml": ">=1,<1.0.1",
"zetacomponents/mail": "<1.8.2",
"zf-commons/zfc-user": "<1.2.2",
"zfcampus/zf-apigility-doctrine": ">=1,<1.0.3",
- "zfr/zfr-oauth2-server-module": "<0.1.2"
+ "zfr/zfr-oauth2-server-module": "<0.1.2",
+ "zoujingli/thinkadmin": "<6.0.22"
},
"type": "metapackage",
"notification-url": "https://packagist.org/downloads/",
@@ -1669,30 +2252,49 @@
"name": "Marco Pivetta",
"email": "ocramius@gmail.com",
"role": "maintainer"
+ },
+ {
+ "name": "Ilya Tribusean",
+ "email": "slash3b@gmail.com",
+ "role": "maintainer"
}
],
"description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it",
- "time": "2019-01-15T19:39:37+00:00"
+ "support": {
+ "issues": "https://github.com/Roave/SecurityAdvisories/issues",
+ "source": "https://github.com/Roave/SecurityAdvisories/tree/latest"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Ocramius",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-08-12T16:04:45+00:00"
},
{
"name": "sebastian/code-unit-reverse-lookup",
- "version": "1.0.1",
+ "version": "1.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git",
- "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18"
+ "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
- "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18",
+ "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619",
+ "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": ">=5.6"
},
"require-dev": {
- "phpunit/phpunit": "^5.7 || ^6.0"
+ "phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
@@ -1717,34 +2319,44 @@
],
"description": "Looks up which function or method a line of code belongs to",
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
- "time": "2017-03-04T06:30:41+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
+ "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T08:15:22+00:00"
},
{
"name": "sebastian/comparator",
- "version": "1.2.4",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/comparator.git",
- "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be"
+ "reference": "1071dfcef776a57013124ff35e1fc41ccd294758"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
- "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758",
+ "reference": "1071dfcef776a57013124ff35e1fc41ccd294758",
"shasum": ""
},
"require": {
- "php": ">=5.3.3",
- "sebastian/diff": "~1.2",
- "sebastian/exporter": "~1.2 || ~2.0"
+ "php": ">=7.1",
+ "sebastian/diff": "^3.0",
+ "sebastian/exporter": "^3.1"
},
"require-dev": {
- "phpunit/phpunit": "~4.4"
+ "phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.2.x-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -1757,6 +2369,10 @@
"BSD-3-Clause"
],
"authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
@@ -1768,45 +2384,52 @@
{
"name": "Bernhard Schussek",
"email": "bschussek@2bepublished.at"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
}
],
"description": "Provides the functionality to compare PHP values for equality",
- "homepage": "http://www.github.com/sebastianbergmann/comparator",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
"keywords": [
"comparator",
"compare",
"equality"
],
- "time": "2017-01-29T09:50:25+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T08:04:30+00:00"
},
{
"name": "sebastian/diff",
- "version": "1.4.3",
+ "version": "3.0.3",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/diff.git",
- "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4"
+ "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4",
- "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
+ "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211",
"shasum": ""
},
"require": {
- "php": "^5.3.3 || ^7.0"
+ "php": ">=7.1"
},
"require-dev": {
- "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0"
+ "phpunit/phpunit": "^7.5 || ^8.0",
+ "symfony/process": "^2 || ^3.3 || ^4"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.4-dev"
+ "dev-master": "3.0-dev"
}
},
"autoload": {
@@ -1819,46 +2442,62 @@
"BSD-3-Clause"
],
"authors": [
- {
- "name": "Kore Nordmann",
- "email": "mail@kore-nordmann.de"
- },
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
}
],
"description": "Diff implementation",
"homepage": "https://github.com/sebastianbergmann/diff",
"keywords": [
- "diff"
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
],
- "time": "2017-05-22T07:24:03+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:59:04+00:00"
},
{
"name": "sebastian/environment",
- "version": "2.0.0",
+ "version": "4.2.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/environment.git",
- "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac"
+ "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac",
- "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
+ "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0",
"shasum": ""
},
"require": {
- "php": "^5.6 || ^7.0"
+ "php": ">=7.1"
},
"require-dev": {
- "phpunit/phpunit": "^5.0"
+ "phpunit/phpunit": "^7.5"
+ },
+ "suggest": {
+ "ext-posix": "*"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "4.2-dev"
}
},
"autoload": {
@@ -1883,34 +2522,44 @@
"environment",
"hhvm"
],
- "time": "2016-11-26T07:53:53+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:53:42+00:00"
},
{
"name": "sebastian/exporter",
- "version": "2.0.0",
+ "version": "3.1.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/exporter.git",
- "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4"
+ "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4",
- "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0c32ea2e40dbf59de29f3b49bf375176ce7dd8db",
+ "reference": "0c32ea2e40dbf59de29f3b49bf375176ce7dd8db",
"shasum": ""
},
"require": {
- "php": ">=5.3.3",
- "sebastian/recursion-context": "~2.0"
+ "php": ">=7.0",
+ "sebastian/recursion-context": "^3.0"
},
"require-dev": {
"ext-mbstring": "*",
- "phpunit/phpunit": "~4.4"
+ "phpunit/phpunit": "^8.5"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "3.1.x-dev"
}
},
"autoload": {
@@ -1923,6 +2572,10 @@
"BSD-3-Clause"
],
"authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
{
"name": "Jeff Welch",
"email": "whatthejeff@gmail.com"
@@ -1931,17 +2584,13 @@
"name": "Volker Dusch",
"email": "github@wallbash.com"
},
- {
- "name": "Bernhard Schussek",
- "email": "bschussek@2bepublished.at"
- },
- {
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de"
- },
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
}
],
"description": "Provides the functionality to export PHP variables for visualization",
@@ -1950,66 +2599,37 @@
"export",
"exporter"
],
- "time": "2016-11-19T08:54:04+00:00"
- },
- {
- "name": "sebastian/finder-facade",
- "version": "1.2.2",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianbergmann/finder-facade.git",
- "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.4"
},
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/finder-facade/zipball/4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f",
- "reference": "4a3174709c2dc565fe5fb26fcf827f6a1fc7b09f",
- "shasum": ""
- },
- "require": {
- "symfony/finder": "~2.3|~3.0|~4.0",
- "theseer/fdomdocument": "~1.3"
- },
- "type": "library",
- "autoload": {
- "classmap": [
- "src/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "BSD-3-Clause"
- ],
- "authors": [
+ "funding": [
{
- "name": "Sebastian Bergmann",
- "email": "sebastian@phpunit.de",
- "role": "lead"
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
}
],
- "description": "FinderFacade is a convenience wrapper for Symfony's Finder component.",
- "homepage": "https://github.com/sebastianbergmann/finder-facade",
- "time": "2017-11-18T17:31:49+00:00"
+ "time": "2021-11-11T13:51:24+00:00"
},
{
"name": "sebastian/global-state",
- "version": "1.1.1",
+ "version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/global-state.git",
- "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4"
+ "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4",
- "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
+ "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": "^7.0"
},
"require-dev": {
- "phpunit/phpunit": "~4.2"
+ "phpunit/phpunit": "^6.0"
},
"suggest": {
"ext-uopz": "*"
@@ -2017,7 +2637,7 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0-dev"
+ "dev-master": "2.0-dev"
}
},
"autoload": {
@@ -2040,33 +2660,38 @@
"keywords": [
"global state"
],
- "time": "2015-10-12T03:26:01+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0"
+ },
+ "time": "2017-04-27T15:39:26+00:00"
},
{
"name": "sebastian/object-enumerator",
- "version": "2.0.1",
+ "version": "3.0.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/object-enumerator.git",
- "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7"
+ "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7",
- "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
+ "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2",
"shasum": ""
},
"require": {
- "php": ">=5.6",
- "sebastian/recursion-context": "~2.0"
+ "php": ">=7.0",
+ "sebastian/object-reflector": "^1.1.1",
+ "sebastian/recursion-context": "^3.0"
},
"require-dev": {
- "phpunit/phpunit": "~5"
+ "phpunit/phpunit": "^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "3.0.x-dev"
}
},
"autoload": {
@@ -2086,32 +2711,42 @@
],
"description": "Traverses array structures and object graphs to enumerate all referenced objects",
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
- "time": "2017-02-18T15:18:39+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:40:27+00:00"
},
{
- "name": "sebastian/recursion-context",
- "version": "2.0.0",
+ "name": "sebastian/object-reflector",
+ "version": "1.1.2",
"source": {
"type": "git",
- "url": "https://github.com/sebastianbergmann/recursion-context.git",
- "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a"
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a",
- "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
+ "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.0"
},
"require-dev": {
- "phpunit/phpunit": "~4.4"
+ "phpunit/phpunit": "^6.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.0.x-dev"
+ "dev-master": "1.1-dev"
}
},
"autoload": {
@@ -2125,13 +2760,68 @@
],
"authors": [
{
- "name": "Jeff Welch",
- "email": "whatthejeff@gmail.com"
- },
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:37:18+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb",
+ "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^6.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
{
"name": "Sebastian Bergmann",
"email": "sebastian@phpunit.de"
},
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
{
"name": "Adam Harvey",
"email": "aharvey@php.net"
@@ -2139,29 +2829,39 @@
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
- "time": "2016-11-19T07:33:16+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:34:24+00:00"
},
{
"name": "sebastian/resource-operations",
- "version": "1.0.0",
+ "version": "2.0.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/resource-operations.git",
- "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52"
+ "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
- "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52",
+ "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3",
+ "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3",
"shasum": ""
},
"require": {
- "php": ">=5.6.0"
+ "php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.0.x-dev"
+ "dev-master": "2.0-dev"
}
},
"autoload": {
@@ -2181,7 +2881,17 @@
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
- "time": "2015-07-28T20:34:47+00:00"
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/resource-operations/issues",
+ "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2020-11-30T07:30:19+00:00"
},
{
"name": "sebastian/version",
@@ -2224,68 +2934,45 @@
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "source": "https://github.com/sebastianbergmann/version/tree/master"
+ },
"time": "2016-10-03T07:35:21+00:00"
},
{
"name": "squizlabs/php_codesniffer",
- "version": "2.9.2",
+ "version": "3.7.1",
"source": {
"type": "git",
"url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
- "reference": "2acf168de78487db620ab4bc524135a13cfe6745"
+ "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/2acf168de78487db620ab4bc524135a13cfe6745",
- "reference": "2acf168de78487db620ab4bc524135a13cfe6745",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619",
+ "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619",
"shasum": ""
},
"require": {
"ext-simplexml": "*",
"ext-tokenizer": "*",
"ext-xmlwriter": "*",
- "php": ">=5.1.2"
+ "php": ">=5.4.0"
},
"require-dev": {
- "phpunit/phpunit": "~4.0"
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
},
"bin": [
- "scripts/phpcs",
- "scripts/phpcbf"
+ "bin/phpcs",
+ "bin/phpcbf"
],
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "2.x-dev"
+ "dev-master": "3.x-dev"
}
},
- "autoload": {
- "classmap": [
- "CodeSniffer.php",
- "CodeSniffer/CLI.php",
- "CodeSniffer/Exception.php",
- "CodeSniffer/File.php",
- "CodeSniffer/Fixer.php",
- "CodeSniffer/Report.php",
- "CodeSniffer/Reporting.php",
- "CodeSniffer/Sniff.php",
- "CodeSniffer/Tokens.php",
- "CodeSniffer/Reports/",
- "CodeSniffer/Tokenizers/",
- "CodeSniffer/DocGenerators/",
- "CodeSniffer/Standards/AbstractPatternSniff.php",
- "CodeSniffer/Standards/AbstractScopeSniff.php",
- "CodeSniffer/Standards/AbstractVariableSniff.php",
- "CodeSniffer/Standards/IncorrectPatternException.php",
- "CodeSniffer/Standards/Generic/Sniffs/",
- "CodeSniffer/Standards/MySource/Sniffs/",
- "CodeSniffer/Standards/PEAR/Sniffs/",
- "CodeSniffer/Standards/PSR1/Sniffs/",
- "CodeSniffer/Standards/PSR2/Sniffs/",
- "CodeSniffer/Standards/Squiz/Sniffs/",
- "CodeSniffer/Standards/Zend/Sniffs/"
- ]
- },
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
@@ -2297,206 +2984,37 @@
}
],
"description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
- "homepage": "http://www.squizlabs.com/php-codesniffer",
+ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
"keywords": [
"phpcs",
"standards"
],
- "time": "2018-11-07T22:31:41+00:00"
- },
- {
- "name": "symfony/console",
- "version": "v3.4.21",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/console.git",
- "reference": "a700b874d3692bc8342199adfb6d3b99f62cc61a"
+ "support": {
+ "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
+ "source": "https://github.com/squizlabs/PHP_CodeSniffer",
+ "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/a700b874d3692bc8342199adfb6d3b99f62cc61a",
- "reference": "a700b874d3692bc8342199adfb6d3b99f62cc61a",
- "shasum": ""
- },
- "require": {
- "php": "^5.5.9|>=7.0.8",
- "symfony/debug": "~2.8|~3.0|~4.0",
- "symfony/polyfill-mbstring": "~1.0"
- },
- "conflict": {
- "symfony/dependency-injection": "<3.4",
- "symfony/process": "<3.3"
- },
- "provide": {
- "psr/log-implementation": "1.0"
- },
- "require-dev": {
- "psr/log": "~1.0",
- "symfony/config": "~3.3|~4.0",
- "symfony/dependency-injection": "~3.4|~4.0",
- "symfony/event-dispatcher": "~2.8|~3.0|~4.0",
- "symfony/lock": "~3.4|~4.0",
- "symfony/process": "~3.3|~4.0"
- },
- "suggest": {
- "psr/log": "For using the console logger",
- "symfony/event-dispatcher": "",
- "symfony/lock": "",
- "symfony/process": ""
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.4-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Console\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony Console Component",
- "homepage": "https://symfony.com",
- "time": "2019-01-04T04:42:43+00:00"
- },
- {
- "name": "symfony/debug",
- "version": "v3.4.21",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/debug.git",
- "reference": "26d7f23b9bd0b93bee5583e4d6ca5cb1ab31b186"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/debug/zipball/26d7f23b9bd0b93bee5583e4d6ca5cb1ab31b186",
- "reference": "26d7f23b9bd0b93bee5583e4d6ca5cb1ab31b186",
- "shasum": ""
- },
- "require": {
- "php": "^5.5.9|>=7.0.8",
- "psr/log": "~1.0"
- },
- "conflict": {
- "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2"
- },
- "require-dev": {
- "symfony/http-kernel": "~2.8|~3.0|~4.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.4-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Debug\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony Debug Component",
- "homepage": "https://symfony.com",
- "time": "2019-01-01T13:45:19+00:00"
- },
- {
- "name": "symfony/finder",
- "version": "v3.4.21",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/finder.git",
- "reference": "3f2a2ab6315dd7682d4c16dcae1e7b95c8b8555e"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/3f2a2ab6315dd7682d4c16dcae1e7b95c8b8555e",
- "reference": "3f2a2ab6315dd7682d4c16dcae1e7b95c8b8555e",
- "shasum": ""
- },
- "require": {
- "php": "^5.5.9|>=7.0.8"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.4-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Finder\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony Finder Component",
- "homepage": "https://symfony.com",
- "time": "2019-01-01T13:45:19+00:00"
+ "time": "2022-06-18T07:21:10+00:00"
},
{
"name": "symfony/polyfill-ctype",
- "version": "v1.10.0",
+ "version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
- "reference": "e3d826245268269cd66f8326bd8bc066687b4a19"
+ "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e3d826245268269cd66f8326bd8bc066687b4a19",
- "reference": "e3d826245268269cd66f8326bd8bc066687b4a19",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
+ "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"shasum": ""
},
"require": {
- "php": ">=5.3.3"
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
@@ -2504,29 +3022,33 @@
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.9-dev"
+ "dev-main": "1.26-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Ctype\\": ""
- },
"files": [
"bootstrap.php"
- ]
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- },
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
@@ -2537,144 +3059,44 @@
"polyfill",
"portable"
],
- "time": "2018-08-06T14:22:27+00:00"
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-24T11:49:31+00:00"
},
{
- "name": "symfony/polyfill-mbstring",
- "version": "v1.10.0",
+ "name": "theseer/tokenizer",
+ "version": "1.1.3",
"source": {
"type": "git",
- "url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "c79c051f5b3a46be09205c73b80b346e4153e494"
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/c79c051f5b3a46be09205c73b80b346e4153e494",
- "reference": "c79c051f5b3a46be09205c73b80b346e4153e494",
- "shasum": ""
- },
- "require": {
- "php": ">=5.3.3"
- },
- "suggest": {
- "ext-mbstring": "For best performance"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.9-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Polyfill\\Mbstring\\": ""
- },
- "files": [
- "bootstrap.php"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Nicolas Grekas",
- "email": "p@tchwork.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony polyfill for the Mbstring extension",
- "homepage": "https://symfony.com",
- "keywords": [
- "compatibility",
- "mbstring",
- "polyfill",
- "portable",
- "shim"
- ],
- "time": "2018-09-21T13:07:52+00:00"
- },
- {
- "name": "symfony/yaml",
- "version": "v3.4.21",
- "source": {
- "type": "git",
- "url": "https://github.com/symfony/yaml.git",
- "reference": "554a59a1ccbaac238a89b19c8e551a556fd0e2ea"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/554a59a1ccbaac238a89b19c8e551a556fd0e2ea",
- "reference": "554a59a1ccbaac238a89b19c8e551a556fd0e2ea",
- "shasum": ""
- },
- "require": {
- "php": "^5.5.9|>=7.0.8",
- "symfony/polyfill-ctype": "~1.8"
- },
- "conflict": {
- "symfony/console": "<3.4"
- },
- "require-dev": {
- "symfony/console": "~3.4|~4.0"
- },
- "suggest": {
- "symfony/console": "For validating YAML files using the lint command"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.4-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "Symfony\\Component\\Yaml\\": ""
- },
- "exclude-from-classmap": [
- "/Tests/"
- ]
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Fabien Potencier",
- "email": "fabien@symfony.com"
- },
- {
- "name": "Symfony Community",
- "homepage": "https://symfony.com/contributors"
- }
- ],
- "description": "Symfony Yaml Component",
- "homepage": "https://symfony.com",
- "time": "2019-01-01T13:45:19+00:00"
- },
- {
- "name": "theseer/fdomdocument",
- "version": "1.6.6",
- "source": {
- "type": "git",
- "url": "https://github.com/theseer/fDOMDocument.git",
- "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/theseer/fDOMDocument/zipball/6e8203e40a32a9c770bcb62fe37e68b948da6dca",
- "reference": "6e8203e40a32a9c770bcb62fe37e68b948da6dca",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
+ "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9",
"shasum": ""
},
"require": {
"ext-dom": "*",
- "lib-libxml": "*",
- "php": ">=5.3.3"
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^7.0"
},
"type": "library",
"autoload": {
@@ -2690,41 +3112,42 @@
{
"name": "Arne Blankerts",
"email": "arne@blankerts.de",
- "role": "lead"
+ "role": "Developer"
}
],
- "description": "The classes contained within this repository extend the standard DOM to use exceptions at all occasions of errors instead of PHP warnings or notices. They also add various custom methods and shortcuts for convenience and to simplify the usage of DOM.",
- "homepage": "https://github.com/theseer/fDOMDocument",
- "time": "2017-06-30T11:53:12+00:00"
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/master"
+ },
+ "time": "2019-06-13T22:48:21+00:00"
},
{
"name": "webmozart/assert",
- "version": "1.4.0",
+ "version": "1.9.1",
"source": {
"type": "git",
- "url": "https://github.com/webmozart/assert.git",
- "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
+ "url": "https://github.com/webmozarts/assert.git",
+ "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
- "reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389",
+ "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389",
"shasum": ""
},
"require": {
- "php": "^5.3.3 || ^7.0",
+ "php": "^5.3.3 || ^7.0 || ^8.0",
"symfony/polyfill-ctype": "^1.8"
},
+ "conflict": {
+ "phpstan/phpstan": "<0.12.20",
+ "vimeo/psalm": "<3.9.1"
+ },
"require-dev": {
- "phpunit/phpunit": "^4.6",
- "sebastian/version": "^1.0.1"
+ "phpunit/phpunit": "^4.8.36 || ^7.5.13"
},
"type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.3-dev"
- }
- },
"autoload": {
"psr-4": {
"Webmozart\\Assert\\": "src/"
@@ -2746,7 +3169,11 @@
"check",
"validate"
],
- "time": "2018-12-25T11:19:39+00:00"
+ "support": {
+ "issues": "https://github.com/webmozarts/assert/issues",
+ "source": "https://github.com/webmozarts/assert/tree/1.9.1"
+ },
+ "time": "2020-07-08T17:02:28+00:00"
}
],
"aliases": [],
@@ -2758,12 +3185,13 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
- "php": ">=5.6",
+ "php": ">=7.1",
"ext-json": "*",
"ext-zlib": "*"
},
"platform-dev": [],
"platform-overrides": {
- "php": "5.6.31"
- }
+ "php": "7.1.29"
+ },
+ "plugin-api-version": "2.1.0"
}
diff --git a/doc/custom_theme/main.html b/doc/custom_theme/main.html
index cc2a703e..96bb153e 100644
--- a/doc/custom_theme/main.html
+++ b/doc/custom_theme/main.html
@@ -2,7 +2,7 @@
{#
The entry point for the ReadTheDocs Theme.
-
+
Any theme customisations should override this file to redefine blocks defined in
the various templates. The custom theme should only need to define a main.html
which `{% extends "base.html" %}` and defines various blocks which will replace
@@ -14,7 +14,7 @@
-{%- if 'media.readthedocs.org' not in config.extra_css[0] %}
+{%- if config.extra_css|length and 'media.readthedocs.org' not in config.extra_css[0] %}
{%- endif %}
diff --git a/doc/md/dev/images/poedit-1.jpg b/doc/md/dev/images/poedit-1.jpg
new file mode 100644
index 00000000..673ae6d6
Binary files /dev/null and b/doc/md/dev/images/poedit-1.jpg differ
diff --git a/doc/md/images/07-installation.jpg b/doc/md/images/07-installation.jpg
new file mode 100644
index 00000000..42cc9f10
Binary files /dev/null and b/doc/md/images/07-installation.jpg differ
diff --git a/inc/languages/de/LC_MESSAGES/shaarli.po b/inc/languages/de/LC_MESSAGES/shaarli.po
index 34d29ce8..f46f66fa 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 Token."
+
+#: 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 "
+"Lassen Sie Besucher Ihre Shaares 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"
-"strong>:\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"
-"strong>:\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"
+msgstr[0] "Shaare"
+msgstr[1] "Shaares"
#: 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"
-"strong> 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"
+#~ "strong> 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 026d0101..a54fbcdf 100644
--- a/inc/languages/fr/LC_MESSAGES/shaarli.po
+++ b/inc/languages/fr/LC_MESSAGES/shaarli.po
@@ -1,24 +1,626 @@
msgid ""
msgstr ""
"Project-Id-Version: Shaarli\n"
-"POT-Creation-Date: 2019-07-13 10:45+0200\n"
-"PO-Revision-Date: 2019-07-13 10:49+0200\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"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"X-Generator: Poedit 2.2.1\n"
+"X-Generator: Poedit 2.3\n"
"X-Poedit-Basepath: ../../../..\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: t:1,2;t\n"
-"X-Poedit-SearchPath-0: .\n"
-"X-Poedit-SearchPathExcluded-0: node_modules\n"
-"X-Poedit-SearchPathExcluded-1: vendor\n"
+"X-Poedit-SearchPath-0: application\n"
+"X-Poedit-SearchPath-1: tmp\n"
+"X-Poedit-SearchPath-2: index.php\n"
+"X-Poedit-SearchPath-3: init.php\n"
+"X-Poedit-SearchPath-4: plugins\n"
-#: application/ApplicationUtils.php:159
+#: 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:192
+msgid "Could not parse history file"
+msgstr "Format incorrect pour le fichier d'historique"
+
+#: application/Languages.php:184
+msgid "Automatic"
+msgstr "Automatique"
+
+#: application/Languages.php:185
+msgid "German"
+msgstr "Allemand"
+
+#: application/Languages.php:186
+msgid "English"
+msgstr "Anglais"
+
+#: application/Languages.php:187
+msgid "French"
+msgstr "Français"
+
+#: application/Languages.php:188
+msgid "Japanese"
+msgstr "Japonais"
+
+#: application/Thumbnailer.php:62
+msgid ""
+"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
+"disabled. Please reload the page."
+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:405
+msgid "Setting not set"
+msgstr "Paramètre non défini"
+
+#: application/Utils.php:412
+msgid "Unlimited"
+msgstr "Illimité"
+
+#: application/Utils.php:415
+msgid "B"
+msgstr "o"
+
+#: application/Utils.php:415
+msgid "kiB"
+msgstr "ko"
+
+#: application/Utils.php:415
+msgid "MiB"
+msgstr "Mo"
+
+#: application/Utils.php:415
+msgid "GiB"
+msgstr "Go"
+
+#: 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:210
+msgid "This bookmarks already exists"
+msgstr "Ce marque-page existe déjà"
+
+#: application/bookmark/BookmarkInitializer.php:42
+msgid "(private bookmark with thumbnail demo)"
+msgstr "(marque page privé avec une miniature)"
+
+#: 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 récupérera automatiquement la miniature associée au liens pour de "
+"nombreux sites web.\n"
+"\n"
+"Explorez votre nouvelle instance de Shaarli en essayant les différents "
+"contrôles et menus.\n"
+"Visitez le projet sur [Github](https://github.com/shaarli/Shaarli) ou [la "
+"documentation](https://shaarli.readthedocs.io/en/master/) pour en apprendre "
+"plus sur Shaarli.\n"
+"\n"
+"Maintenant, vous pouvez modifier ou supprimer les shaares créés par défaut.\n"
+
+#: application/bookmark/BookmarkInitializer.php:58
+msgid "Note: Shaare descriptions"
+msgstr "Note : Description des Shaares"
+
+#: 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 ""
+"Ajouter un shaare sans préciser d'URL créé une « note » textuelle, telle que "
+"celle-ci.\n"
+"Cette note est privée, donc vous êtes seul à pouvoir la voir lorsque vous "
+"êtes connecté.\n"
+"\n"
+"Vous pouvez utiliser cette fonctionnalité pour prendre des notes, publier "
+"des articles, des extraits de code, et bien plus.\n"
+"\n"
+"L'option du formatage par Markdown vous permet de formater vos description "
+"de notes et marque-pages :\n"
+"\n"
+"### Titre d'en-tête\n"
+"\n"
+"#### Sur plusieurs niveaux\n"
+" * liste à puce\n"
+" * texte en _italique_\n"
+" * texte en **gras**\n"
+" * texte ~~barré~~\n"
+" * blocs de `code`\n"
+" * images\n"
+" * [liens](https://en.wikipedia.org/wiki/Markdown)\n"
+"\n"
+"Markdown supporte aussi les tableaux :\n"
+"\n"
+"| Nom | Type | Couleur | Qte |\n"
+"| ------- | --------- | ------ | ----- |\n"
+"| Orange | Fruit | Orange | 126 |\n"
+"| Pomme | Fruit | Multiple | 62 |\n"
+"| Citron | Fruit | Jaune | 30 |\n"
+"| Carotte | Légume | Orange | 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 ""
+"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de "
+"données"
+
+#: 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 ""
+"Bienvenue sur Shaarli !\n"
+"\n"
+"Shaarli vous permet de sauvegarder des marque-pages de vos pages favorites, "
+"et de les partager avec d'autres, ou de les enregistrer en privé.\n"
+"Vous pouvez ajouter une description à vos marque-pages, comme celle-ci, et y "
+"ajouter des tags.\n"
+"\n"
+"Créez un nouveau shaare en cliquant sur le bouton `+Shaare`, ou en utilisant "
+"l'un des outils recommandés (extension de navigateur, application mobile, "
+"bookmarklet, REST API, etc.).\n"
+"\n"
+"Vous pouvez facilement retrouver vos liens, même parmi des milliers, en "
+"utilisant le moteur de recherche interne, ou en filtrant par tags (par "
+"exemple ce Shaare est taggé avec `shaarli` et `help`).\n"
+"Les hashtags comme #shaarli #help sont aussi supportés.\n"
+"Vous pouvez aussi filtrer les [flux RSS](/feed/atom) et [mur d'images]() par "
+"tag ou par texte brut.\n"
+"\n"
+"Nous espérons que vous apprécierez utiliser Shaarli, maintenu avec ❤️ par la "
+"communauté !\n"
+"N'hésitez pas à ouvrir [un ticket (en)](https://github.com/shaarli/Shaarli/"
+"issues) si vous avez une suggestion ou si vous rencontrez un problème.\n"
+" \n"
+
+#: 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: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 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: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:20
+#, php-format
+msgid "Configuration value is required for %s"
+msgstr "Le paramètre %s est obligatoire"
+
+#: application/config/exception/PluginConfigOrderException.php:15
+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:15
+msgid "You are not authorized to alter config."
+msgstr "Vous n'êtes pas autorisé à modifier la configuration."
+
+#: application/exceptions/IOException.php:23
+msgid "Error accessing"
+msgstr "Une erreur s'est produite en accédant à"
+
+#: application/feed/FeedBuilder.php:180
+msgid "Direct link"
+msgstr "Liens directs"
+
+#: 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:56
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "Configure"
+msgstr "Configurer"
+
+#: 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: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: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:128
+msgid "Configuration was saved."
+msgstr "La configuration a été sauvegardée."
+
+#: application/front/controller/admin/ExportController.php:26
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
+msgid "Export"
+msgstr "Exporter"
+
+#: application/front/controller/admin/ExportController.php:42
+msgid "Please select an export mode."
+msgstr "Merci de choisir un mode d'export."
+
+#: application/front/controller/admin/ImportController.php:41
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+msgid "Import"
+msgstr "Importer"
+
+#: application/front/controller/admin/ImportController.php:55
+msgid "No import file provided."
+msgstr "Aucun fichier à importer n'a été fourni."
+
+#: 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 ""
+"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
+"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
+"légères."
+
+#: application/front/controller/admin/ManageTagController.php:30
+msgid "whitespace"
+msgstr "espace"
+
+#: application/front/controller/admin/ManageTagController.php:35
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+msgid "Manage tags"
+msgstr "Gérer les tags"
+
+#: application/front/controller/admin/ManageTagController.php:54
+msgid "Invalid tags provided."
+msgstr "Les tags fournis ne sont pas valides."
+
+#: 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] "Le tag a été supprimé du %d lien."
+msgstr[1] "Le tag a été supprimé de %d liens."
+
+#: 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] "Le tag a été renommé dans %d lien."
+msgstr[1] "Le tag a été renommé dans %d liens."
+
+#: application/front/controller/admin/ManageTagController.php:105
+msgid "Tags separator must be a single character."
+msgstr "Un séparateur de tags doit contenir un seul caractère."
+
+#: application/front/controller/admin/ManageTagController.php:111
+msgid "These characters are reserved and can't be used as tags separator: "
+msgstr ""
+"Ces caractères sont réservés et ne peuvent être utilisés comme des "
+"séparateurs de tags : "
+
+#: application/front/controller/admin/PasswordController.php:28
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+msgid "Change password"
+msgstr "Modifier le mot de passe"
+
+#: application/front/controller/admin/PasswordController.php:55
+msgid "You must provide the current and new password to change it."
+msgstr ""
+"Vous devez fournir les mots de passe actuel et nouveau pour pouvoir le "
+"modifier."
+
+#: application/front/controller/admin/PasswordController.php:71
+msgid "The old password is not correct."
+msgstr "L'ancien mot de passe est incorrect."
+
+#: application/front/controller/admin/PasswordController.php:97
+msgid "Your password has been changed"
+msgstr "Votre mot de passe a été modifié"
+
+#: application/front/controller/admin/PluginsController.php:45
+msgid "Plugin Administration"
+msgstr "Administration des plugins"
+
+#: application/front/controller/admin/PluginsController.php:76
+msgid "Setting successfully saved."
+msgstr "Les paramètres ont été sauvegardés avec succès."
+
+#: application/front/controller/admin/PluginsController.php:79
+msgid "Error while saving plugin configuration: "
+msgstr ""
+"Une erreur s'est produite lors de la sauvegarde de la configuration des "
+"plugins : "
+
+#: application/front/controller/admin/ServerController.php:35
+msgid "Check disabled"
+msgstr "Vérification désactivée"
+
+#: application/front/controller/admin/ServerController.php:57
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Server administration"
+msgstr "Administration serveur"
+
+#: application/front/controller/admin/ServerController.php:74
+msgid "Thumbnails cache has been cleared."
+msgstr "Le cache des miniatures a été vidé."
+
+#: 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é !"
+
+#: application/front/controller/admin/ShaareAddController.php:26
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+msgid "Shaare a new link"
+msgstr "Partagez un nouveau lien"
+
+#: application/front/controller/admin/ShaareManageController.php:35
+#: application/front/controller/admin/ShaareManageController.php:93
+msgid "Invalid bookmark ID provided."
+msgstr "L'ID du marque-page fourni n'est pas valide."
+
+#: 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 "Le lien avec l'identifiant %s n'a pas pu être trouvé."
+
+#: application/front/controller/admin/ShaareManageController.php:101
+msgid "Invalid visibility provided."
+msgstr "Visibilité du lien non valide."
+
+#: application/front/controller/admin/ShaarePublishController.php:173
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
+msgid "Edit"
+msgstr "Modifier"
+
+#: 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:208
+msgid "Note: "
+msgstr "Note : "
+
+#: application/front/controller/admin/ThumbnailsController.php:37
+#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Thumbnails update"
+msgstr "Mise à jour des miniatures"
+
+#: application/front/controller/admin/ToolsController.php:31
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33
+msgid "Tools"
+msgstr "Outils"
+
+#: application/front/controller/visitor/BookmarkListController.php:121
+msgid "Search: "
+msgstr "Recherche : "
+
+#: application/front/controller/visitor/DailyController.php:200
+msgid "day"
+msgstr "jour"
+
+#: application/front/controller/visitor/DailyController.php:200
+#: application/front/controller/visitor/DailyController.php:203
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
+msgid "Daily"
+msgstr "Quotidien"
+
+#: application/front/controller/visitor/DailyController.php:201
+msgid "week"
+msgstr "semaine"
+
+#: application/front/controller/visitor/DailyController.php:201
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Weekly"
+msgstr "Hebdomadaire"
+
+#: application/front/controller/visitor/DailyController.php:202
+msgid "month"
+msgstr "mois"
+
+#: application/front/controller/visitor/DailyController.php:202
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "Monthly"
+msgstr "Mensuel"
+
+#: application/front/controller/visitor/ErrorController.php:30
+msgid "Error: "
+msgstr "Erreur : "
+
+#: application/front/controller/visitor/ErrorController.php:34
+msgid "Please report it on Github."
+msgstr "Merci de la rapporter sur Github."
+
+#: application/front/controller/visitor/ErrorController.php:39
+msgid "An unexpected error occurred."
+msgstr "Une erreur inattendue s'est produite."
+
+#: application/front/controller/visitor/ErrorNotFoundController.php:25
+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:65
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+msgid "Install Shaarli"
+msgstr "Installation de Shaarli"
+
+#: application/front/controller/visitor/InstallController.php:85
+#, 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 ""
+"Les sesssions ne semblent pas fonctionner sur ce serveur.
Assurez "
+"vous que la variable « session.save_path » est correctement définie dans "
+"votre fichier de configuration PHP, et que vous avez les droits d'écriture "
+"dessus.
Ce paramètre pointe actuellement sur %s.
Sur certains "
+"navigateurs, accéder à votre serveur depuis un nom d'hôte comme « localhost "
+"» ou autre nom personnalisé sans point '.' entraine l'échec de la sauvegarde "
+"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: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:171
+msgid "Insufficient permissions:"
+msgstr "Permissions insuffisantes :"
+
+#: 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 "Connexion"
+
+#: application/front/controller/visitor/LoginController.php:78
+msgid "Wrong login/password."
+msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
+
+#: 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 "Mur d'images"
+
+#: application/front/controller/visitor/TagCloudController.php:90
+msgid "Tag "
+msgstr "Tag "
+
+#: application/front/exceptions/AlreadyInstalledException.php:11
+msgid "Shaarli has already been installed. Login to edit the configuration."
+msgstr ""
+"Shaarli est déjà installé. Connectez-vous pour modifier la configuration."
+
+#: application/front/exceptions/LoginBannedException.php:11
+msgid ""
+"You have been banned after too many failed login attempts. Try again later."
+msgstr ""
+"Vous avez été banni après trop d'échecs d'authentification. Merci de "
+"réessayer plus tard."
+
+#: application/front/exceptions/OpenShaarliPasswordException.php:16
+msgid "You are not supposed to change a password on an Open Shaarli."
+msgstr ""
+"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
+
+#: application/front/exceptions/ThumbnailsDisabledException.php:11
+msgid "Picture wall unavailable (thumbnails are disabled)."
+msgstr ""
+"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
+
+#: application/front/exceptions/WrongTokenException.php:16
+msgid "Wrong token."
+msgstr "Jeton invalide."
+
+#: application/helper/ApplicationUtils.php:165
#, php-format
msgid ""
"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
@@ -29,109 +631,100 @@ 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/ApplicationUtils.php:189 application/ApplicationUtils.php:201
+#: 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/ApplicationUtils.php:204
+#: application/helper/ApplicationUtils.php:223
msgid "directory is not writable"
msgstr "le répertoire n'est pas accessible en écriture"
-#: application/ApplicationUtils.php:222
+#: application/helper/ApplicationUtils.php:247
msgid "file is not readable"
msgstr "le fichier n'est pas accessible en lecture"
-#: application/ApplicationUtils.php:225
+#: application/helper/ApplicationUtils.php:250
msgid "file is not writable"
msgstr "le fichier n'est pas accessible en écriture"
-#: application/History.php:178
-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:189
-msgid "Could not parse history file"
-msgstr "Format incorrect pour le fichier d'historique"
-
-#: application/Languages.php:181
-msgid "Automatic"
-msgstr "Automatique"
-
-#: application/Languages.php:182
-msgid "English"
-msgstr "Anglais"
-
-#: application/Languages.php:183
-msgid "French"
-msgstr "Français"
-
-#: application/Languages.php:184
-msgid "German"
-msgstr "Allemand"
-
-#: application/Thumbnailer.php:62
+#: application/helper/ApplicationUtils.php:260
msgid ""
-"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
-"disabled. Please reload the page."
+"Lock can not be acquired on the datastore. You might encounter concurrent "
+"access issues."
msgstr ""
-"l'extension php-gd doit être chargée pour utiliser les miniatures. Les "
-"miniatures sont désormais désactivées. Rechargez la page."
+"Le fichier datastore ne peut pas être verrouillé. Vous pourriez rencontrer "
+"des problèmes d'accès concurrents."
-#: application/Utils.php:379 tests/UtilsTest.php:343
-msgid "Setting not set"
-msgstr "Paramètre non défini"
+#: application/helper/ApplicationUtils.php:293
+msgid "Configuration parsing"
+msgstr "Chargement de la configuration"
-#: application/Utils.php:386 tests/UtilsTest.php:341 tests/UtilsTest.php:342
-msgid "Unlimited"
-msgstr "Illimité"
+#: application/helper/ApplicationUtils.php:294
+msgid "Slim Framework (routing, etc.)"
+msgstr "Slim Framwork (routage, etc.)"
-#: application/Utils.php:389 tests/UtilsTest.php:338 tests/UtilsTest.php:339
-#: tests/UtilsTest.php:353
-msgid "B"
-msgstr "o"
+#: application/helper/ApplicationUtils.php:295
+msgid "Multibyte (Unicode) string support"
+msgstr "Support des chaînes de caractère multibytes (Unicode)"
-#: application/Utils.php:389 tests/UtilsTest.php:332 tests/UtilsTest.php:333
-#: tests/UtilsTest.php:340
-msgid "kiB"
-msgstr "ko"
+#: application/helper/ApplicationUtils.php:296
+msgid "Required to use thumbnails"
+msgstr "Obligatoire pour utiliser les miniatures"
-#: application/Utils.php:389 tests/UtilsTest.php:334 tests/UtilsTest.php:335
-#: tests/UtilsTest.php:351 tests/UtilsTest.php:352
-msgid "MiB"
-msgstr "Mo"
+#: application/helper/ApplicationUtils.php:297
+msgid "Localized text sorting (e.g. e->è->f)"
+msgstr "Tri des textes traduits (ex : e->è->f)"
-#: application/Utils.php:389 tests/UtilsTest.php:336 tests/UtilsTest.php:337
-msgid "GiB"
-msgstr "Go"
+#: 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 miniatures"
-#: application/bookmark/LinkDB.php:128
+#: 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:300
+msgid "Login using LDAP server"
+msgstr "Authentification via un serveur LDAP"
+
+#: application/helper/DailyPageHelper.php:172
+msgid "Week"
+msgstr "Semaine"
+
+#: application/helper/DailyPageHelper.php:176
+msgid "Today"
+msgstr "Aujourd'hui"
+
+#: application/helper/DailyPageHelper.php:178
+msgid "Yesterday"
+msgstr "Hier"
+
+#: application/helper/FileUtils.php:100
+msgid "Provided path is not a directory."
+msgstr "Le chemin fourni n'est pas un dossier."
+
+#: application/helper/FileUtils.php:104
+msgid "Trying to delete a folder outside of Shaarli path."
+msgstr "Tentative de supprimer un dossier en dehors du chemin de Shaarli."
+
+#: application/legacy/LegacyLinkDB.php:131
msgid "You are not authorized to add a link."
msgstr "Vous n'êtes pas autorisé à ajouter un lien."
-#: application/bookmark/LinkDB.php:131
+#: application/legacy/LegacyLinkDB.php:134
msgid "Internal Error: A link should always have an id and URL."
msgstr "Erreur interne : un lien devrait toujours avoir un ID et une URL."
-#: application/bookmark/LinkDB.php:134
+#: application/legacy/LegacyLinkDB.php:137
msgid "You must specify an integer as a key."
msgstr "Vous devez utiliser un entier comme clé."
-#: application/bookmark/LinkDB.php:137
+#: application/legacy/LegacyLinkDB.php:140
msgid "Array offset and link ID must be equal."
msgstr "La clé du tableau et l'ID du lien doivent être identiques."
-#: application/bookmark/LinkDB.php:243
-#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
-#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
-#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
-#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:49
-msgid ""
-"The personal, minimalist, super-fast, database free, bookmarking service"
-msgstr ""
-"Le gestionnaire de marque-pages personnel, minimaliste, et sans base de "
-"données"
-
-#: application/bookmark/LinkDB.php:246
+#: 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"
@@ -151,314 +744,68 @@ msgstr ""
"Vous utilisez la version supportée par la communauté du projet original "
"Shaarli de Sébastien Sauvage."
-#: application/bookmark/LinkDB.php:263
+#: application/legacy/LegacyLinkDB.php:266
msgid "My secret stuff... - Pastebin.com"
msgstr "Mes trucs secrets... - Pastebin.com"
-#: application/bookmark/LinkDB.php:265
+#: application/legacy/LegacyLinkDB.php:268
msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
msgstr ""
"Pssst ! Je suis un lien privé que VOUS êtes le seul à voir. Vous pouvez me "
"supprimer aussi."
-#: application/bookmark/exception/LinkNotFoundException.php:13
-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/legacy/LegacyUpdater.php:104
+msgid "Couldn't retrieve updater class methods."
+msgstr "Impossible de récupérer les méthodes de la classe Updater."
-#: application/config/ConfigJson.php:52 application/config/ConfigPhp.php:129
-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 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/legacy/LegacyUpdater.php:540
+msgid ""
+msgstr ""
-#: application/config/ConfigManager.php:135
-#: application/config/ConfigManager.php:162
-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
-#, php-format
-msgid "Configuration value is required for %s"
-msgstr "Le paramètre %s est obligatoire"
-
-#: application/config/exception/PluginConfigOrderException.php:15
-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
-msgid "You are not authorized to alter config."
-msgstr "Vous n'êtes pas autorisé à modifier la configuration."
-
-#: application/exceptions/IOException.php:22
-msgid "Error accessing"
-msgstr "Une erreur s'est produite en accédant à"
-
-#: application/feed/Cache.php:16
-#, php-format
-msgid "Cannot purge %s: no directory"
-msgstr "Impossible de purger %s : le répertoire n'existe pas"
-
-#: application/feed/FeedBuilder.php:155
-msgid "Direct link"
-msgstr "Liens directs"
-
-#: application/feed/FeedBuilder.php:157
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
-msgid "Permalink"
-msgstr "Permalien"
-
-#: application/netscape/NetscapeBookmarkUtils.php:42
+#: application/netscape/NetscapeBookmarkUtils.php:63
msgid "Invalid export selection:"
msgstr "Sélection d'export invalide :"
-#: application/netscape/NetscapeBookmarkUtils.php:87
+#: application/netscape/NetscapeBookmarkUtils.php:215
#, php-format
msgid "File %s (%d bytes) "
msgstr "Le fichier %s (%d octets) "
-#: application/netscape/NetscapeBookmarkUtils.php:89
+#: application/netscape/NetscapeBookmarkUtils.php:217
msgid "has an unknown file format. Nothing was imported."
msgstr "a un format inconnu. Rien n'a été importé."
-#: application/netscape/NetscapeBookmarkUtils.php:93
+#: 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 ""
"a été importé avec succès en %d secondes : %d liens importés, %d liens "
"écrasés, %d liens ignorés."
-#: application/plugin/exception/PluginFileNotFoundException.php:21
+#: application/plugin/PluginManager.php:125
+msgid " [plugin incompatibility]: "
+msgstr " [incompatibilité de l'extension] : "
+
+#: application/plugin/exception/PluginFileNotFoundException.php:22
#, php-format
msgid "Plugin \"%s\" files not found."
msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
-#: application/render/PageBuilder.php:209
-msgid "The page you are trying to reach does not exist or has been deleted."
-msgstr "La page que vous essayez de consulter n'existe pas ou a été supprimée."
-
-#: application/render/PageBuilder.php:211
-msgid "404 Not Found"
-msgstr "404 Introuvable"
-
-#: application/updater/Updater.php:99
-#, fuzzy
-#| msgid "Couldn't retrieve Updater class methods."
-msgid "Couldn't retrieve updater class methods."
-msgstr "Impossible de récupérer les méthodes de la classe Updater."
-
-#: application/updater/Updater.php:526 index.php:1034
-msgid ""
-"You have enabled or changed thumbnails mode. Please synchronize them."
-msgstr ""
-"Vous avez activé ou changé le mode de miniatures. Merci de les synchroniser."
-
-#: application/updater/UpdaterUtils.php:32
-msgid "Updates file path is not set, can't write updates."
-msgstr ""
-"Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
-"d'écrire les mises à jour."
-
-#: application/updater/UpdaterUtils.php:37
-msgid "Unable to write updates in "
-msgstr "Impossible d'écrire les mises à jour dans "
+#: application/render/PageCacheManager.php:32
+#, php-format
+msgid "Cannot purge %s: no directory"
+msgstr "Impossible de purger %s : le répertoire n'existe pas"
#: application/updater/exception/UpdaterException.php:51
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:145
-msgid "Shared links on "
+#: index.php:81
+msgid "Shared bookmarks on "
msgstr "Liens partagés sur "
-#: index.php:167
-msgid "Insufficient permissions:"
-msgstr "Permissions insuffisantes :"
-
-#: index.php:203
-msgid "I said: NO. You are banned for the moment. Go away."
-msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
-
-#: index.php:275
-msgid "Wrong login/password."
-msgstr "Nom d'utilisateur ou mot de passe incorrect(s)."
-
-#: index.php:398 index.php:404
-msgid "Today"
-msgstr "Aujourd'hui"
-
-#: index.php:400
-msgid "Yesterday"
-msgstr "Hier"
-
-#: index.php:484 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:46
-msgid "Daily"
-msgstr "Quotidien"
-
-#: index.php:593 tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
-#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:75
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:99
-msgid "Login"
-msgstr "Connexion"
-
-#: index.php:608 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:41
-msgid "Picture wall"
-msgstr "Mur d'images"
-
-#: index.php:683 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 "Nuage de tags"
-
-#: index.php:715 tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
-msgid "Tag list"
-msgstr "Liste des tags"
-
-#: index.php:944 tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31
-msgid "Tools"
-msgstr "Outils"
-
-#: index.php:952
-msgid "You are not supposed to change a password on an Open Shaarli."
-msgstr ""
-"Vous n'êtes pas censé modifier le mot de passe d'un Shaarli en mode ouvert."
-
-#: index.php:957 index.php:1007 index.php:1094 index.php:1124 index.php:1234
-#: index.php:1281
-msgid "Wrong token."
-msgstr "Jeton invalide."
-
-#: index.php:966
-msgid "The old password is not correct."
-msgstr "L'ancien mot de passe est incorrect."
-
-#: index.php:993
-msgid "Your password has been changed"
-msgstr "Votre mot de passe a été modifié"
-
-#: index.php:997
-#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
-msgid "Change password"
-msgstr "Modifier le mot de passe"
-
-#: index.php:1054
-msgid "Configuration was saved."
-msgstr "La configuration a été sauvegardée."
-
-#: index.php:1078 tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
-msgid "Configure"
-msgstr "Configurer"
-
-#: index.php:1088 tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
-msgid "Manage tags"
-msgstr "Gérer les tags"
-
-#: index.php:1107
-#, php-format
-msgid "The tag was removed from %d link."
-msgid_plural "The tag was removed from %d links."
-msgstr[0] "Le tag a été supprimé de %d lien."
-msgstr[1] "Le tag a été supprimé de %d liens."
-
-#: index.php:1108
-#, php-format
-msgid "The tag was renamed in %d link."
-msgid_plural "The tag was renamed in %d links."
-msgstr[0] "Le tag a été renommé dans %d lien."
-msgstr[1] "Le tag a été renommé dans %d liens."
-
-#: index.php:1115 tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
-msgid "Shaare a new link"
-msgstr "Partager un nouveau lien"
-
-#: index.php:1344 tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
-msgid "Edit"
-msgstr "Modifier"
-
-#: index.php:1344 index.php:1416
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
-msgid "Shaare"
-msgstr "Shaare"
-
-#: index.php:1385
-msgid "Note: "
-msgstr "Note : "
-
-#: index.php:1424
-msgid "Invalid link ID provided"
-msgstr "ID du lien non valide"
-
-#: index.php:1444 tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
-msgid "Export"
-msgstr "Exporter"
-
-#: index.php:1506 tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
-msgid "Import"
-msgstr "Importer"
-
-#: index.php:1516
-#, 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 ""
-"Le fichier que vous essayer d'envoyer est probablement plus lourd que ce que "
-"le serveur web peut accepter (%s). Merci de l'envoyer en parties plus "
-"légères."
-
-#: index.php:1561 tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
-msgid "Plugin administration"
-msgstr "Administration des plugins"
-
-#: index.php:1616 tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
-msgid "Thumbnails update"
-msgstr "Mise à jour des miniatures"
-
-#: index.php:1782
-msgid "Search: "
-msgstr "Recherche : "
-
-#: index.php:1825
-#, 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 ""
-"Les sesssions ne semblent pas fonctionner sur ce serveur.
Assurez "
-"vous que la variable « session.save_path » est correctement définie dans "
-"votre fichier de configuration PHP, et que vous avez les droits d'écriture "
-"dessus.
Ce paramètre pointe actuellement sur %s.
Sur certains "
-"navigateurs, accéder à votre serveur depuis un nom d'hôte comme « localhost "
-"» ou autre nom personnalisé sans point '.' entraine l'échec de la sauvegarde "
-"des cookies. Nous vous recommandons d'accéder à votre serveur depuis son "
-"adresse IP ou un Fully Qualified Domain Name.
"
-
-#: index.php:1835
-msgid "Click to try again."
-msgstr "Cliquer ici pour réessayer."
-
#: plugins/addlink_toolbar/addlink_toolbar.php:31
msgid "URI"
msgstr "URI"
@@ -472,15 +819,15 @@ 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:25
+#: plugins/archiveorg/archiveorg.php:29
msgid "View on archive.org"
msgstr "Voir sur archive.org"
-#: plugins/archiveorg/archiveorg.php:38
+#: 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."
-#: plugins/default_colors/default_colors.php:33
+#: plugins/default_colors/default_colors.php:38
msgid ""
"Default colors plugin error: This plugin is active and no custom color is "
"configured."
@@ -488,25 +835,25 @@ msgstr ""
"Erreur du plugin default colors : ce plugin est actif et aucune couleur "
"n'est configurée."
-#: plugins/default_colors/default_colors.php:107
+#: plugins/default_colors/default_colors.php:113
msgid "Override default theme colors. Use any CSS valid color."
msgstr ""
"Remplacer les couleurs du thème par défaut. Utiliser n'importe quelle "
"couleur CSS valide."
-#: plugins/default_colors/default_colors.php:108
+#: plugins/default_colors/default_colors.php:114
msgid "Main color (navbar green)"
msgstr "Couleur principale (vert de la barre de navigation)"
-#: plugins/default_colors/default_colors.php:109
+#: plugins/default_colors/default_colors.php:115
msgid "Background color (light grey)"
msgstr "Couleur de fond (gris léger)"
-#: plugins/default_colors/default_colors.php:110
+#: plugins/default_colors/default_colors.php:116
msgid "Dark main color (e.g. visited links)"
msgstr "Couleur principale sombre (ex : les liens visités)"
-#: plugins/demo_plugin/demo_plugin.php:482
+#: plugins/demo_plugin/demo_plugin.php:478
msgid ""
"A demo plugin covering all use cases for template designers and plugin "
"developers."
@@ -514,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:483
+#: 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:484
+#: plugins/demo_plugin/demo_plugin.php:480
msgid "Other demo parameter"
msgstr "Un autre paramètre de démo"
@@ -540,37 +887,7 @@ msgstr ""
msgid "Isso server URL (without 'http://')"
msgstr "URL du serveur Isso (sans 'http://')"
-#: plugins/markdown/markdown.php:163
-msgid "Description will be rendered with"
-msgstr "La description sera générée avec"
-
-#: plugins/markdown/markdown.php:164
-msgid "Markdown syntax documentation"
-msgstr "Documentation sur la syntaxe Markdown"
-
-#: plugins/markdown/markdown.php:165
-msgid "Markdown syntax"
-msgstr "la syntaxe Markdown"
-
-#: plugins/markdown/markdown.php:361
-msgid ""
-"Render shaare description with Markdown syntax.
Warning"
-"strong>:\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 ""
-"Utilise la syntaxe Markdown pour la description des liens."
-"
Attention :\n"
-"Si vous aviez des descriptions contenant du HTML avant d'activer cette "
-"extension,\n"
-"l'activer pourrait déformer vos pages.\n"
-"Voir le README."
-
-#: 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."
@@ -578,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."
@@ -626,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:72 plugins/wallabag/wallabag.php:68
+#: 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."
@@ -638,28 +955,18 @@ 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:47
+#: plugins/wallabag/wallabag.php:49
msgid "Save to wallabag"
msgstr "Sauvegarder dans Wallabag"
-#: plugins/wallabag/wallabag.php:69
+#: plugins/wallabag/wallabag.php:73
msgid "Wallabag API URL"
msgstr "URL de l'API Wallabag"
-#: plugins/wallabag/wallabag.php:70
+#: plugins/wallabag/wallabag.php:74
msgid "Wallabag API version (1 or 2)"
msgstr "Version de l'API Wallabag (1 ou 2)"
-#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227
-#: tests/languages/fr/LanguagesFrTest.php:159
-#: tests/languages/fr/LanguagesFrTest.php:172
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:85
-msgid "Search"
-msgid_plural "Search"
-msgstr[0] "Rechercher"
-msgstr[1] "Rechercher"
-
#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
msgid "Sorry, nothing to see here."
msgstr "Désolé, il y a rien à voir ici."
@@ -668,6 +975,48 @@ msgstr "Désolé, il y a rien à voir ici."
msgid "URL or leave empty to post a note"
msgstr "URL ou laisser vide pour créer une note"
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "BULK CREATION"
+msgstr "CRÉATION DE MASSE"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+msgid "Metadata asynchronous retrieval is disabled."
+msgstr "La récupération asynchrone des meta-données est désactivée."
+
+#: 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 ""
+"Nous recommandons d'activer le paramètre general > "
+"enable_async_metadata dans votre fichier de configuration pour utiliser "
+"la création de masse."
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
+msgid "Shaare multiple new links"
+msgstr "Partagez plusieurs nouveaux liens"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
+msgid "Add one URL per line to create multiple bookmarks."
+msgstr "Ajouter une URL par ligne pour créer plusieurs marque-pages."
+
+#: 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 "Privé"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
+msgid "Add links"
+msgstr "Ajouter des liens"
+
#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
msgid "Current password"
msgstr "Mot de passe actuel"
@@ -694,51 +1043,47 @@ msgid "Case sensitive"
msgstr "Sensible à la casse"
#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
-msgid "Rename"
-msgstr "Renommer"
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
+msgid "Rename tag"
+msgstr "Renommer le tag"
#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:145
-msgid "Delete"
-msgstr "Supprimer"
+msgid "Delete tag"
+msgstr "Supprimer le tag"
-#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
msgid "You can also edit tags in the"
msgstr "Vous pouvez aussi modifier les tags dans la"
-#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
msgid "tag list"
msgstr "liste des tags"
-#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:143
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:312
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
-msgid "All"
-msgstr "Tous"
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+msgid "Change tags separator"
+msgstr "Changer le séparateur de tags"
-#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:147
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:316
-msgid "Only common media hosts"
-msgstr "Seulement les hébergeurs de média connus"
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
+msgid "Your current tag separator is"
+msgstr "Votre séparateur actuel est"
-#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:151
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
-msgid "None"
-msgstr "Aucune"
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
+msgid "New separator"
+msgstr "Nouveau séparateur"
-#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:158
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:297
-msgid "You need to enable the extension php-gd
to use thumbnails."
+#: 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 "Enregistrer"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
+msgid "Note that hashtags won't fully work with a non-whitespace separator."
msgstr ""
-"Vous devez activer l'extension php-gd
pour utiliser les "
-"miniatures."
-
-#: tmp/configure.90100d2eaf5d3705e14b9b4f78ecddc9.rtpl.php:162
-msgid "Synchonize thumbnails"
-msgstr "Synchroniser les miniatures"
+"Notez que les hashtags ne sont pas complètement fonctionnels avec un "
+"séparateur qui n'est pas un espace."
#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
msgid "title"
@@ -756,155 +1101,179 @@ msgstr "Valeur par défaut"
msgid "Theme"
msgstr "Thème"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
+msgid "Description formatter"
+msgstr "Format des descriptions"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
msgid "Language"
msgstr "Langue"
-#: 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 "Fuseau horaire"
-#: 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 "Continent"
-#: 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 "Ville"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191
msgid "Disable session cookie hijacking protection"
msgstr "Désactiver la protection contre le détournement de cookies"
-#: 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 ""
"Cocher cette case si vous êtes souvent déconnecté ou si votre adresse IP "
"change souvent"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210
msgid "Private links by default"
msgstr "Liens privés par défaut"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:184
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211
msgid "All new links are private by default"
msgstr "Tous les nouveaux liens sont privés par défaut"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226
msgid "RSS direct links"
msgstr "Liens directs dans le flux RSS"
-#: 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 ""
"Cocher cette case pour utiliser des liens directs au lieu des permaliens "
"dans le flux RSS"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242
msgid "Hide public links"
msgstr "Cacher les liens publics"
-#: 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 "N'afficher aucun lien sans être connecté"
-#: 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 "Vérifier les mises à jour"
-#: 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 "Me notifier lorsqu'une nouvelle version est disponible"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
msgid "Automatically retrieve description for new bookmarks"
msgstr "Récupérer automatiquement la description"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:248
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275
msgid "Shaarli will try to retrieve the description from meta HTML headers"
msgstr ""
"Shaarli essaiera de récupérer la description depuis les balises HTML meta "
"dans les entêtes"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
msgid "Enable REST API"
msgstr "Activer l'API REST"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:264
-#: 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 ""
"Permet aux applications tierces d'utiliser Shaarli, par exemple les "
"applications mobiles"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:279
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306
msgid "API secret"
msgstr "Clé d'API secrète"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:293
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
msgid "Enable thumbnails"
msgstr "Activer les miniatures"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:301
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324
+msgid "You need to enable the extension php-gd
to use thumbnails."
+msgstr ""
+"Vous devez activer l'extension php-gd
pour utiliser les "
+"miniatures."
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
msgid "Synchronize thumbnails"
msgstr "Synchroniser les miniatures"
-#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
-#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
-#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
-msgid "Save"
-msgstr "Enregistrer"
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+msgid "All"
+msgstr "Tous"
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
-msgid "The Daily Shaarli"
-msgstr "Le Quotidien Shaarli"
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
+msgid "Only common media hosts"
+msgstr "Seulement les hébergeurs de média connus"
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
-msgid "1 RSS entry per day"
-msgstr "1 entrée RSS par jour"
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
+msgid "None"
+msgstr "Aucune"
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
-msgid "Previous day"
-msgstr "Jour précédent"
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+msgid "1 RSS entry per :type"
+msgid_plural ""
+msgstr[0] "1 entrée RSS par :type"
+msgstr[1] ""
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
-msgid "All links of one day in a single page."
-msgstr "Tous les liens d'un jour sur une page."
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
+msgid "Previous :type"
+msgid_plural ""
+msgstr[0] ":type précédent"
+msgstr[1] "Jour précédent"
-#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
-msgid "Next day"
-msgstr "Jour suivant"
+#: 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] "Tous les liens d'un :type sur une page."
+msgstr[1] "Tous les liens d'un jour sur une page."
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+msgid "Next :type"
+msgid_plural ""
+msgstr[0] ":type suivant"
+msgstr[1] ""
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
msgid "Edit Shaare"
msgstr "Modifier le Shaare"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
msgid "New Shaare"
msgstr "Nouveau Shaare"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
msgid "Created:"
msgstr "Création :"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
msgid "URL"
msgstr "URL"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
msgid "Title"
msgstr "Titre"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
@@ -912,37 +1281,56 @@ msgstr "Titre"
msgid "Description"
msgstr "Description"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
-msgid "Tags"
-msgstr "Tags"
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89
+msgid "Description will be rendered with"
+msgstr "La description sera générée avec"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:167
-msgid "Private"
-msgstr "Privé"
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
+msgid "Markdown syntax documentation"
+msgstr "Documentation sur la syntaxe Markdown"
-#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
+msgid "Markdown syntax"
+msgstr "la syntaxe Markdown"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115
+msgid "Cancel"
+msgstr "Annuler"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
msgid "Apply Changes"
msgstr "Appliquer les changements"
+#: 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 "Supprimer"
+
+#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
+#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
+msgid "Save all"
+msgstr "Tout enregistrer"
+
#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
msgid "Export Database"
msgstr "Exporter les données"
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
msgid "Selection"
msgstr "Choisir"
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
msgid "Public"
msgstr "Publics"
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
msgid "Prepend note permalinks with this Shaarli instance's URL"
msgstr "Préfixer les liens de note avec l'URL de l'instance de Shaarli"
-#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
msgid "Useful to import bookmarks in a web browser"
msgstr "Utile pour importer les marques-pages dans un navigateur"
@@ -983,42 +1371,42 @@ msgstr "Les doublons s'appuient sur les URL"
msgid "Add default tags"
msgstr "Ajouter des tags par défaut"
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
-msgid "Install Shaarli"
-msgstr "Installation de Shaarli"
-
#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
msgid "It looks like it's the first time you run Shaarli. Please configure it."
msgstr ""
"Il semblerait que ça soit la première fois que vous lancez Shaarli. Merci de "
"le configurer."
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
-#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:165
+#: 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 "Nom d'utilisateur"
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
-#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:166
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:166
+#: 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 "Mot de passe"
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:62
msgid "Shaarli title"
msgstr "Titre du Shaarli"
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
msgid "My links"
msgstr "Mes liens"
-#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
msgid "Install"
msgstr "Installer"
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190
+msgid "Server requirements"
+msgstr "Pré-requis serveur"
+
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
msgid "shaare"
@@ -1034,21 +1422,31 @@ msgstr[0] "lien privé"
msgstr[1] "liens privés"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:121
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123
msgid "Search text"
msgstr "Recherche texte"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:128
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:128
+#: 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 "Filtrer par tag"
+#: 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"
+msgstr "Rechercher"
+
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
msgid "Nothing found."
msgstr "Aucun résultat."
@@ -1069,99 +1467,97 @@ msgid "tagged"
msgstr "taggé"
#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
msgid "Remove tag"
msgstr "Retirer le tag"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:142
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
msgid "with status"
msgstr "avec le statut"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
msgid "without any tag"
msgstr "sans tag"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
-#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
-#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41
msgid "Fold"
msgstr "Replier"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
msgid "Edited: "
msgstr "Modifié : "
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
msgid "permalink"
msgstr "permalien"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
msgid "Add tag"
msgstr "Ajouter un tag"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185
msgid "Toggle sticky"
msgstr "Changer statut épinglé"
-#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187
msgid "Sticky"
msgstr "Épinglé"
-#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
-#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
+msgid "Share a private link"
+msgstr "Partager un lien privé"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5
msgid "Filters"
msgstr "Filtres"
-#: 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 "Afficher uniquement les liens privés"
-#: 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 "Afficher uniquement les liens publics"
-#: 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 "Filtrer par liens privés"
+msgstr "Filtrer par liens sans tag"
#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24
msgid "Select all"
msgstr "Tout sélectionner"
-#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27
-#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
-#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:27
-#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:79
-#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
-#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
+#: 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 "Replier tout"
-#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
-#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:72
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76
msgid "Links per page"
msgstr "Liens par page"
-#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
-msgid ""
-"You have been banned after too many failed login attempts. Try again later."
-msgstr ""
-"Vous avez été banni après trop d'échecs d'authentification. Merci de "
-"réessayer plus tard."
-
-#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171
msgid "Remember me"
msgstr "Rester connecté"
#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
-#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
-#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:49
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
msgid "by the Shaarli community"
msgstr "par la communauté Shaarli"
@@ -1170,77 +1566,84 @@ msgstr "par la communauté Shaarli"
msgid "Documentation"
msgstr "Documentation"
-#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
-#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
msgid "Expand"
msgstr "Déplier"
-#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
-#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
msgid "Expand all"
msgstr "Déplier tout"
-#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
-#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:47
+#: 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 "Êtes-vous sûr de vouloir supprimer ce lien ?"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:90
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:65
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:90
+#: 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 "Êtes-vous sûr de vouloir supprimer ce tag ?"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11
+msgid "Menu"
+msgstr "Menu"
+
+#: 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 "Nuage de tags"
+
+#: 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 "Flux RSS"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:70
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:106
+#: 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 "Déconnexion"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:150
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152
msgid "Set public"
msgstr "Rendre public"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:155
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157
msgid "Set private"
msgstr "Rendre privé"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:187
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189
msgid "is available"
msgstr "est disponible"
-#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:194
-#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:194
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:196
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196
msgid "Error"
msgstr "Erreur"
-#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
-msgid "Picture wall unavailable (thumbnails are disabled)."
-msgstr ""
-"Le mur d'images n'est pas disponible (les miniatures sont désactivées)."
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "There is no cached thumbnail."
+msgstr "Il n'y a aucune miniature dans le cache."
-#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
-#, fuzzy
-#| msgid ""
-#| "You don't have any cached thumbnail. Try to synchronize them."
-msgid ""
-"There is no cached thumbnail. Try to synchronize them."
-msgstr ""
-"Il n'y a aucune miniature en cache. Essayer de les synchroniser."
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
+msgid "Try to synchronize them."
+msgstr "Essayer de les synchroniser."
-#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
msgid "Picture Wall"
msgstr "Mur d'images"
-#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
msgid "pics"
msgstr "images"
@@ -1249,6 +1652,11 @@ msgid "You need to enable Javascript to change plugin loading order."
msgstr ""
"Vous devez activer Javascript pour pouvoir modifier l'ordre des extensions."
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+msgid "Plugin administration"
+msgstr "Administration des plugins"
+
#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
msgid "Enabled Plugins"
msgstr "Extensions activées"
@@ -1304,6 +1712,100 @@ msgstr "Configuration des extensions"
msgid "No parameter available."
msgstr "Aucun paramètre disponible."
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "General"
+msgstr "Général"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
+msgid "Index URL"
+msgstr "URL de l'index"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Base path"
+msgstr "Chemin de base"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Client IP"
+msgstr "IP du client"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+msgid "Trusted reverse proxies"
+msgstr "Reverse proxies de confiance"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "N/A"
+msgstr "N/A"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
+msgid "Visit releases page on Github"
+msgstr "Visiter la page des releases sur Github"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
+msgid "Synchronize all link thumbnails"
+msgstr "Synchroniser toutes les miniatures"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2
+msgid "Permissions"
+msgstr "Permissions"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8
+msgid "There are permissions that need to be fixed."
+msgstr "Il y a des permissions qui doivent être corrigées."
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23
+msgid "All read/write permissions are properly set."
+msgstr "Toutes les permissions de lecture/écriture sont définies correctement."
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32
+msgid "Running PHP"
+msgstr "Fonctionnant avec PHP"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36
+msgid "End of life: "
+msgstr "Fin de vie : "
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48
+msgid "Extension"
+msgstr "Extension"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49
+msgid "Usage"
+msgstr "Utilisation"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50
+msgid "Status"
+msgstr "Statut"
+
+#: 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 "Chargé"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
+msgid "Required"
+msgstr "Obligatoire"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
+msgid "Optional"
+msgstr "Optionnel"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70
+msgid "Not loaded"
+msgstr "Non chargé"
+
#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
msgid "tags"
@@ -1314,6 +1816,10 @@ msgstr "tags"
msgid "List all links with those tags"
msgstr "Lister tous les liens avec ces tags"
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "Tag list"
+msgstr "Liste des tags"
+
#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
msgid "Sort by:"
@@ -1350,51 +1856,43 @@ msgstr "Configurer Shaarli"
msgid "Enable, disable and configure plugins"
msgstr "Activer, désactiver et configurer les extensions"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27
+msgid "Check instance's server configuration"
+msgstr "Vérifier la configuration serveur de l'instance"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
msgid "Change your password"
msgstr "Modifier le mot de passe"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
msgid "Rename or delete a tag in all links"
msgstr "Renommer ou supprimer un tag dans tous les liens"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
-#, fuzzy
-#| msgid ""
-#| "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
-#| "delicious…)"
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
msgid ""
"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
"delicious...)"
msgstr ""
"Importer des marques pages au format Netscape HTML (comme exportés depuis "
-"Firefox, Chrome, Opera, delicious…)"
+"Firefox, Chrome, Opera, delicious...)"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
msgid "Import links"
msgstr "Importer des liens"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
-#, fuzzy
-#| msgid ""
-#| "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
-#| "Opera, delicious…)"
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
msgid ""
"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
"Opera, delicious...)"
msgstr ""
"Exporter les marques pages au format Netscape HTML (comme exportés depuis "
-"Firefox, Chrome, Opera, delicious…)"
+"Firefox, Chrome, Opera, delicious...)"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54
msgid "Export database"
msgstr "Exporter les données"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:55
-msgid "Synchronize all link thumbnails"
-msgstr "Synchroniser toutes les miniatures"
-
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:81
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
msgid ""
"Drag one of these button to your bookmarks toolbar or right-click it and "
"\"Bookmark This Link\""
@@ -1402,13 +1900,13 @@ msgstr ""
"Glisser un de ces boutons dans votre barre de favoris ou cliquer droit "
"dessus et « Ajouter aux favoris »"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:82
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
msgid "then click on the bookmarklet in any page you want to share."
msgstr ""
"puis cliquer sur le marque-page depuis un site que vous souhaitez partager."
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
+#: 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"
@@ -1416,40 +1914,40 @@ msgstr ""
"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
"Ajouter aux favoris »"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
msgid "then click ✚Shaare link button in any page you want to share"
msgstr "puis cliquer sur ✚Shaare depuis un site que vous souhaitez partager"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
msgid "The selected text is too long, it will be truncated."
msgstr "Le texte sélectionné est trop long, il sera tronqué."
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
msgid "Shaare link"
msgstr "Shaare"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
msgid ""
"Then click ✚Add Note button anytime to start composing a private Note (text "
"post) to your Shaarli"
msgstr ""
"Puis cliquer sur ✚Add Note pour commencer à rédiger une Note sur Shaarli"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:127
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
msgid "Add Note"
msgstr "Ajouter une Note"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132
msgid "3rd party"
msgstr "Applications tierces"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140
msgid "plugin"
msgstr "extension"
-#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
msgid ""
"Drag this link to your bookmarks toolbar, or right-click it and choose "
"Bookmark This Link"
@@ -1457,6 +1955,74 @@ msgstr ""
"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
"Ajouter aux favoris »"
+#~ msgid "Display:"
+#~ msgstr "Afficher :"
+
+#~ msgid "The Daily Shaarli"
+#~ msgstr "Le Quotidien Shaarli"
+
+#, fuzzy
+#~| msgid "Selection"
+#~ msgid ".ui-selecting"
+#~ msgstr "Choisir"
+
+#, fuzzy
+#~| msgid "Documentation"
+#~ msgid "document"
+#~ msgstr "Documentation"
+
+#~ msgid "The page you are trying to reach does not exist or has been deleted."
+#~ msgstr ""
+#~ "La page que vous essayez de consulter n'existe pas ou a été supprimée."
+
+#~ msgid "404 Not Found"
+#~ msgstr "404 Introuvable"
+
+#~ msgid "Updates file path is not set, can't write updates."
+#~ msgstr ""
+#~ "Le chemin vers le fichier de mise à jour n'est pas défini, impossible "
+#~ "d'écrire les mises à jour."
+
+#~ msgid "Unable to write updates in "
+#~ msgstr "Impossible d'écrire les mises à jour dans "
+
+#~ msgid "I said: NO. You are banned for the moment. Go away."
+#~ msgstr "NON. Vous êtes banni pour le moment. Revenez plus tard."
+
+#~ msgid "Click to try again."
+#~ msgstr "Cliquer ici pour réessayer."
+
+#~ msgid ""
+#~ "Render shaare description with Markdown syntax.
Warning"
+#~ "strong>:\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 ""
+#~ "Utilise la syntaxe Markdown pour la description des liens."
+#~ "
Attention :\n"
+#~ "Si vous aviez des descriptions contenant du HTML avant d'activer cette "
+#~ "extension,\n"
+#~ "l'activer pourrait déformer vos pages.\n"
+#~ "Voir le README."
+
+#~ msgid "Synchonize thumbnails"
+#~ msgstr "Synchroniser les miniatures"
+
+#, fuzzy
+#~| msgid ""
+#~| "You don't have any cached thumbnail. Try to synchronize them."
+#~ msgid ""
+#~ "There is no cached thumbnail. Try to synchronize them."
+#~ msgstr ""
+#~ "Il n'y a aucune miniature en cache. Essayer de les synchroniser."
+
#~ msgid ""
#~ "You need to browse your Shaarli over HTTPS to use this "
#~ "functionality."
diff --git a/inc/languages/jp/LC_MESSAGES/shaarli.po b/inc/languages/jp/LC_MESSAGES/shaarli.po
new file mode 100644
index 00000000..36164433
--- /dev/null
+++ b/inc/languages/jp/LC_MESSAGES/shaarli.po
@@ -0,0 +1,1441 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Shaarli\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-04-04 11:29+0900\n"
+"PO-Revision-Date: 2021-04-04 13:20+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.4.2\n"
+"X-Poedit-Basepath: ../../../..\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Poedit-KeywordsList: t:1,2;t\n"
+"X-Poedit-SearchPath-0: .\n"
+"X-Poedit-SearchPathExcluded-0: node_modules\n"
+"X-Poedit-SearchPathExcluded-1: vendor\n"
+
+#: application/History.php:181
+msgid "History file isn't readable or writable"
+msgstr "履歴ファイルを読み込む、または書き込むための権限がありません"
+
+#: application/History.php:192
+msgid "Could not parse history file"
+msgstr "履歴ファイルを正常に復元できませんでした"
+
+#: application/Languages.php:184
+msgid "Automatic"
+msgstr "自動"
+
+#: application/Languages.php:185
+msgid "German"
+msgstr "ドイツ語"
+
+#: application/Languages.php:186
+msgid "English"
+msgstr "英語"
+
+#: application/Languages.php:187
+msgid "French"
+msgstr "フランス語"
+
+#: application/Languages.php:188
+msgid "Japanese"
+msgstr "日本語"
+
+#: application/Languages.php:189
+msgid "Russian"
+msgstr "ロシア語"
+
+#: application/Thumbnailer.php:62
+msgid ""
+"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
+"disabled. Please reload the page."
+msgstr ""
+"サムネイルを使用するには、php-gd 拡張機能が読み込まれている必要があります。サ"
+"ムネイルは無効化されました。ページを再読込してください。"
+
+#: application/Utils.php:406 tests/UtilsTest.php:327
+msgid "Setting not set"
+msgstr "未設定"
+
+#: application/Utils.php:413 tests/UtilsTest.php:325 tests/UtilsTest.php:326
+msgid "Unlimited"
+msgstr "無制限"
+
+#: application/Utils.php:416 tests/UtilsTest.php:322 tests/UtilsTest.php:323
+#: tests/UtilsTest.php:337
+msgid "B"
+msgstr "B"
+
+#: application/Utils.php:416 tests/UtilsTest.php:316 tests/UtilsTest.php:317
+#: tests/UtilsTest.php:324
+msgid "kiB"
+msgstr "kiB"
+
+#: application/Utils.php:416 tests/UtilsTest.php:318 tests/UtilsTest.php:319
+#: tests/UtilsTest.php:335 tests/UtilsTest.php:336
+msgid "MiB"
+msgstr "MiB"
+
+#: application/Utils.php:416 tests/UtilsTest.php:320 tests/UtilsTest.php:321
+msgid "GiB"
+msgstr "GiB"
+
+#: application/bookmark/BookmarkFileService.php:203
+#: application/bookmark/BookmarkFileService.php:225
+#: application/bookmark/BookmarkFileService.php:247
+#: application/bookmark/BookmarkFileService.php:261
+msgid "You're not authorized to alter the datastore"
+msgstr "設定を変更する権限がありません"
+
+#: application/bookmark/BookmarkFileService.php:228
+msgid "This bookmarks already exists"
+msgstr "このブックマークは既に存在します"
+
+#: application/bookmark/BookmarkInitializer.php:42
+msgid "(private bookmark with thumbnail demo)"
+msgstr "(サムネイルデモが付属しているプライベートブックマーク)"
+
+#: 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 は自動的に多様なウェブサイトのサムネイルを取得します。\n"
+"\n"
+"あなたの新しい Shaarli インスタンスをコントロールやメニューを試したりして、探"
+"検してください。\n"
+" [Github](https://github.com/shaarli/Shaarli) または [the documentation]"
+"(https://shaarli.readthedocs.io/en/master/) でプロジェクトを訪問して、"
+"Shaarli をもっとよく知ることができます。\n"
+"\n"
+"今から、既定の shaares を編集したり、削除したりすることができます。\n"
+
+#: application/bookmark/BookmarkInitializer.php:58
+msgid "Note: Shaare descriptions"
+msgstr "説明: Shaare の概要"
+
+#: 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 ""
+"URL を追加せずに shaare を作成すると、テキストのみのこのような \"ノート\" が"
+"作成されます。\n"
+"このノートはプライベートなので、ログイン中のあなたしか見ることはできませ"
+"ん。\n"
+"\n"
+"あなたはこれをメモ帳として使ったり、記事を投稿したり、コード スニペットとした"
+"りするなどといったことに使えます。\n"
+"\n"
+"Markdown フォーマットの設定により、ノートやブックマークの概要を以下のように"
+"フォーマットできます:\n"
+"\n"
+"### タイトル ヘッダー\n"
+"\n"
+"#### 複数の見出し\n"
+" * 箇条書きリスト\n"
+" * _イタリック_ 文字\n"
+" * **ボールド** 文字\n"
+" * ~~打ち消し~~ 文字\n"
+" * `コード` ブロック\n"
+" * 画像\n"
+" * [リンク](https://en.wikipedia.org/wiki/Markdown)\n"
+"\n"
+"Markdown は表もサポートします:\n"
+"\n"
+"| 名前 | 種類 | 色 | 数量 |\n"
+"| ------- | --------- | ------ | ----- |\n"
+"| オレンジ | 果物 | 橙 | 126 |\n"
+"| リンゴ | 果物 | 任意 | 62 |\n"
+"| レモン | 果物 | 黄 | 30 |\n"
+"| 人参 | 野菜 | 赤 | 14 |\n"
+
+#: application/bookmark/BookmarkInitializer.php:94
+#: application/legacy/LegacyLinkDB.php:246
+msgid ""
+"The personal, minimalist, super-fast, database free, bookmarking service"
+msgstr ""
+"個人向けの、ミニマムで高速でかつデータベースのいらないブックマークサービス"
+
+#: 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 ""
+"Shaarli へようこそ!\n"
+"\n"
+"Shaarli では、あなたのお気に入りのページをブックマークしたり、それを他の人と"
+"共有するか、またはプライベートなものとして保管することができます。\n"
+"加えて、あなたのブックマークにこの項目のように概要を追加したり、タグ付けした"
+"りすることができます。\n"
+"\n"
+"`+Shaare` ボタンをクリックすることで新しい shaare を作成できます。また、推奨"
+"されたツールを使うこともできます (ブラウザー 拡張機能、モバイル アプリ、ブッ"
+"クマークレット、REST API など...)。\n"
+"\n"
+"また、簡単にあなたのリンクを取得できます。それが何千と登る数であっても、内部"
+"の検索エンジンや、タグを使って検索できます (例えば、この Shaare は `shaarli` "
+"と `help` というタグが付いています)。\n"
+"#shaarli や #help といったハッシュタグもサポートされています。\n"
+"タグやテキスト検索による [RSS フィード](/feed/atom) や ピクチャー ウォール で"
+"項目を絞ることもできます。\n"
+"\n"
+"私たちはあなたが Shaarli を楽しんでくれることを願っています。Shaarli はコミュ"
+"ニティーによって ♡ と共にメンテナンスされています!\n"
+"何か問題に遭遇したり、提案があれば、気軽に [Issue](https://github.com/"
+"shaarli/Shaarli/issues) を開いてください。\n"
+
+#: application/bookmark/exception/BookmarkNotFoundException.php:14
+msgid "The link you are trying to reach does not exist or has been deleted."
+msgstr "開こうとしたリンクは存在しないか、削除されています。"
+
+#: 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 は設定ファイルを作成できませんでした。Shaarli が正しい権限下に置かれ"
+"ていて、インストールされているディレクトリに書き込みできることを確認してくだ"
+"さい。"
+
+#: application/config/ConfigManager.php:137
+#: application/config/ConfigManager.php:164
+msgid "Invalid setting key parameter. String expected, got: "
+msgstr ""
+"不正なキーの値です。文字列が想定されていますが、次のように入力されました: "
+
+#: application/config/exception/MissingFieldConfigException.php:20
+#, php-format
+msgid "Configuration value is required for %s"
+msgstr "%s には設定が必要です"
+
+#: application/config/exception/PluginConfigOrderException.php:15
+msgid "An error occurred while trying to save plugins loading order."
+msgstr "プラグインの読込順を変更する際にエラーが発生しました。"
+
+#: application/config/exception/UnauthorizedConfigException.php:15
+msgid "You are not authorized to alter config."
+msgstr "設定を変更する権限がありません。"
+
+#: application/exceptions/IOException.php:23
+msgid "Error accessing"
+msgstr "読込中にエラーが発生しました"
+
+#: application/feed/FeedBuilder.php:174
+msgid "Direct link"
+msgstr "ダイレクトリンク"
+
+#: application/feed/FeedBuilder.php:176
+msgid "Permalink"
+msgstr "パーマリンク"
+
+#: application/front/controller/admin/ConfigureController.php:56
+msgid "Configure"
+msgstr "設定"
+
+#: application/front/controller/admin/ConfigureController.php:106
+#: application/legacy/LegacyUpdater.php:539
+msgid "You have enabled or changed thumbnails mode."
+msgstr "サムネイルのモードを有効化、または変更しました。"
+
+#: application/front/controller/admin/ConfigureController.php:108
+#: application/front/controller/admin/ServerController.php:81
+#: application/legacy/LegacyUpdater.php:540
+msgid "Please synchronize them."
+msgstr "それらを同期してください。"
+
+#: application/front/controller/admin/ConfigureController.php:119
+#: application/front/controller/visitor/InstallController.php:154
+msgid "Error while writing config file after configuration update."
+msgstr "設定ファイルを更新した後の書き込みに失敗しました。"
+
+#: application/front/controller/admin/ConfigureController.php:128
+msgid "Configuration was saved."
+msgstr "設定は保存されました。"
+
+#: application/front/controller/admin/ExportController.php:26
+msgid "Export"
+msgstr "エクスポート"
+
+#: application/front/controller/admin/ExportController.php:42
+msgid "Please select an export mode."
+msgstr "エクスポート モードを指定してください。"
+
+#: application/front/controller/admin/ImportController.php:41
+msgid "Import"
+msgstr "インポート"
+
+#: application/front/controller/admin/ImportController.php:55
+msgid "No import file provided."
+msgstr "何のインポート元ファイルも指定されませんでした。"
+
+#: 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 ""
+"あなたがアップロードしようとしているファイルは、サーバーが許可しているファイ"
+"ルサイズ (%s) よりも大きいです。もう少し小さいものをアップロードしてくださ"
+"い。"
+
+#: application/front/controller/admin/ManageTagController.php:30
+msgid "whitespace"
+msgstr "空白"
+
+#: application/front/controller/admin/ManageTagController.php:35
+msgid "Manage tags"
+msgstr "タグを設定"
+
+#: application/front/controller/admin/ManageTagController.php:54
+msgid "Invalid tags provided."
+msgstr "不正なタグが入力されました。"
+
+#: application/front/controller/admin/ManageTagController.php:81
+#, php-format
+msgid "The tag was removed from %d bookmark."
+msgid_plural "The tag was removed from %d bookmarks."
+msgstr[0] "%d 件のリンクからタグが削除されました。"
+msgstr[1] "%d 件のリンクからタグが削除されました。"
+
+#: application/front/controller/admin/ManageTagController.php:86
+#, php-format
+msgid "The tag was renamed in %d bookmark."
+msgid_plural "The tag was renamed in %d bookmarks."
+msgstr[0] "このタグを持つ %d 件のリンクにおいて、名前が変更されました。"
+msgstr[1] "このタグを持つ %d 件のリンクにおいて、名前が変更されました。"
+
+#: application/front/controller/admin/ManageTagController.php:108
+msgid "Tags separator must be a single character."
+msgstr "タグを分離する文字は1つでなければいけません。"
+
+#: application/front/controller/admin/ManageTagController.php:114
+msgid "These characters are reserved and can't be used as tags separator: "
+msgstr ""
+"それらの文字は予約文字であり、タグを分離するための文字として使うことはできま"
+"せん: "
+
+#: application/front/controller/admin/PasswordController.php:28
+msgid "Change password"
+msgstr "パスワードを変更"
+
+#: application/front/controller/admin/PasswordController.php:55
+msgid "You must provide the current and new password to change it."
+msgstr ""
+"パスワードを変更するには、現在のパスワードと、新しいパスワードを入力する必要"
+"があります。"
+
+#: application/front/controller/admin/PasswordController.php:71
+msgid "The old password is not correct."
+msgstr "元のパスワードが正しくありません。"
+
+#: application/front/controller/admin/PasswordController.php:97
+msgid "Your password has been changed"
+msgstr "あなたのパスワードは変更されました"
+
+#: application/front/controller/admin/PluginsController.php:45
+msgid "Plugin Administration"
+msgstr "プラグイン管理"
+
+#: application/front/controller/admin/PluginsController.php:76
+msgid "Setting successfully saved."
+msgstr "設定が正常に保存されました。"
+
+#: application/front/controller/admin/PluginsController.php:79
+msgid "Error while saving plugin configuration: "
+msgstr "プラグインの設定ファイルを保存するときにエラーが発生しました: "
+
+#: application/front/controller/admin/ServerController.php:35
+msgid "Check disabled"
+msgstr "無効になっている項目をチェック"
+
+#: application/front/controller/admin/ServerController.php:62
+msgid "Server administration"
+msgstr "サーバー管理"
+
+#: application/front/controller/admin/ServerController.php:79
+msgid "Thumbnails cache has been cleared."
+msgstr "サムネイルのキャッシュがクリアされました。"
+
+#: application/front/controller/admin/ServerController.php:90
+msgid "Shaarli's cache folder has been cleared!"
+msgstr "Shaarli のキャッシュのフォルダーがクリアされました!"
+
+#: application/front/controller/admin/ShaareAddController.php:26
+msgid "Shaare a new link"
+msgstr "新しいリンクを追加"
+
+#: application/front/controller/admin/ShaareManageController.php:35
+#: application/front/controller/admin/ShaareManageController.php:97
+msgid "Invalid bookmark ID provided."
+msgstr "不正なブックマーク ID が入力されました。"
+
+#: application/front/controller/admin/ShaareManageController.php:47
+#: application/front/controller/admin/ShaareManageController.php:120
+#: application/front/controller/admin/ShaareManageController.php:160
+#: application/front/controller/admin/ShaarePublishController.php:82
+#, php-format
+msgid "Bookmark with identifier %s could not be found."
+msgstr "%s という識別子を持ったブックマークは見つかりませんでした。"
+
+#: application/front/controller/admin/ShaareManageController.php:105
+msgid "Invalid visibility provided."
+msgstr "不正な公開設定が入力されました。"
+
+#: application/front/controller/admin/ShaarePublishController.php:173
+msgid "Edit"
+msgstr "共有"
+
+#: application/front/controller/admin/ShaarePublishController.php:176
+msgid "Shaare"
+msgstr "Shaare"
+
+#: application/front/controller/admin/ShaarePublishController.php:208
+msgid "Note: "
+msgstr "注: "
+
+#: application/front/controller/admin/ThumbnailsController.php:37
+msgid "Thumbnails update"
+msgstr "サムネイルの更新"
+
+#: application/front/controller/admin/ToolsController.php:31
+msgid "Tools"
+msgstr "ツール"
+
+#: application/front/controller/visitor/BookmarkListController.php:103
+msgid "Search: "
+msgstr "検索: "
+
+#: application/front/controller/visitor/DailyController.php:201
+msgid "day"
+msgstr "日"
+
+#: application/front/controller/visitor/DailyController.php:201
+#: application/front/controller/visitor/DailyController.php:204
+msgid "Daily"
+msgstr "デイリー"
+
+#: application/front/controller/visitor/DailyController.php:202
+msgid "week"
+msgstr "週"
+
+#: application/front/controller/visitor/DailyController.php:202
+msgid "Weekly"
+msgstr "週間"
+
+#: application/front/controller/visitor/DailyController.php:203
+msgid "month"
+msgstr "月"
+
+#: application/front/controller/visitor/DailyController.php:203
+msgid "Monthly"
+msgstr "月間"
+
+#: application/front/controller/visitor/ErrorController.php:30
+msgid "Error: "
+msgstr "エラー: "
+
+#: application/front/controller/visitor/ErrorController.php:34
+msgid "Please report it on Github."
+msgstr "GitHub で報告してください。"
+
+#: application/front/controller/visitor/ErrorController.php:39
+msgid "An unexpected error occurred."
+msgstr "予期しないエラーが発生しました。"
+
+#: application/front/controller/visitor/ErrorNotFoundController.php:25
+msgid "Requested page could not be found."
+msgstr "リクエストされたページは存在しません。"
+
+#: application/front/controller/visitor/InstallController.php:70
+msgid "Install Shaarli"
+msgstr "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 ""
+"セッションが正常にあなたのサーバー上で稼働していないようです。
PHP の"
+"設定ファイル内にて、正しく \"session.save_path\" の値が設定されていることと、"
+"権限が間違っていないことを確認してください。
現在 %s からPHPの設定ファイル"
+"を読み込んでいます。
一部のブラウザーにおいて、localhost や他のドットを含"
+"まないホスト名にてサーバーにアクセスする際に、クッキーを保存できないことがあ"
+"ります。IP アドレスや完全なドメイン名でサーバーにアクセスすることをおすすめし"
+"ます。
"
+
+#: application/front/controller/visitor/InstallController.php:162
+msgid ""
+"Shaarli is now configured. Please login and start shaaring your bookmarks!"
+msgstr ""
+"Shaarli の設定が完了しました。ログインして、あなたのブックマークを登録しま"
+"しょう!"
+
+#: application/front/controller/visitor/InstallController.php:176
+msgid "Insufficient permissions:"
+msgstr "権限がありません:"
+
+#: application/front/controller/visitor/LoginController.php:46
+msgid "Login"
+msgstr "ログイン"
+
+#: application/front/controller/visitor/LoginController.php:78
+msgid "Wrong login/password."
+msgstr "不正なユーザー名、またはパスワードです。"
+
+#: application/front/controller/visitor/PictureWallController.php:29
+msgid "Picture wall"
+msgstr "ピクチャウォール"
+
+#: application/front/controller/visitor/TagCloudController.php:90
+msgid "Tag "
+msgstr "タグ "
+
+#: application/front/exceptions/AlreadyInstalledException.php:11
+msgid "Shaarli has already been installed. Login to edit the configuration."
+msgstr "Shaarli がインストールされました。ログインして設定を変更できます。"
+
+#: application/front/exceptions/LoginBannedException.php:11
+msgid ""
+"You have been banned after too many failed login attempts. Try again later."
+msgstr "複数回に渡るログインへの失敗を検出しました。後でまた試してください。"
+
+#: application/front/exceptions/OpenShaarliPasswordException.php:16
+msgid "You are not supposed to change a password on an Open Shaarli."
+msgstr ""
+"公開されている Shaarli において、パスワードを変更することは想定されていませ"
+"ん。"
+
+#: application/front/exceptions/ThumbnailsDisabledException.php:11
+msgid "Picture wall unavailable (thumbnails are disabled)."
+msgstr "ピクチャ ウォールは利用できません (サムネイルが無効化されています)。"
+
+#: application/front/exceptions/WrongTokenException.php:16
+msgid "Wrong token."
+msgstr "不正なトークンです。"
+
+#: application/helper/ApplicationUtils.php:165
+#, php-format
+msgid ""
+"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
+"cannot run. Your PHP version has known security vulnerabilities and should "
+"be updated as soon as possible."
+msgstr ""
+"使用している PHP のバージョンが古すぎます! Shaarli の実行には最低でも PHP %s "
+"が必要です。 現在使用している PHP のバージョンには脆弱性があり、できるだけ速"
+"やかにアップデートするべきです。"
+
+#: application/helper/ApplicationUtils.php:200
+#: application/helper/ApplicationUtils.php:220
+msgid "directory is not readable"
+msgstr "ディレクトリを読み込めません"
+
+#: application/helper/ApplicationUtils.php:223
+msgid "directory is not writable"
+msgstr "ディレクトリに書き込めません"
+
+#: application/helper/ApplicationUtils.php:247
+msgid "file is not readable"
+msgstr "ファイルを読み取る権限がありません"
+
+#: application/helper/ApplicationUtils.php:250
+msgid "file is not writable"
+msgstr "ファイルを書き込む権限がありません"
+
+#: application/helper/ApplicationUtils.php:265
+msgid ""
+"Lock can not be acquired on the datastore. You might encounter concurrent "
+"access issues."
+msgstr ""
+"ロックはデータストアでは取得できません。同時にアクセスしたことによってエラー"
+"が発生した可能性があります。"
+
+#: application/helper/ApplicationUtils.php:298
+msgid "Configuration parsing"
+msgstr "設定ファイルのパース"
+
+#: application/helper/ApplicationUtils.php:299
+msgid "Slim Framework (routing, etc.)"
+msgstr "軽量なフレームワーク (ルーティングなど)"
+
+#: application/helper/ApplicationUtils.php:300
+msgid "Multibyte (Unicode) string support"
+msgstr "マルチバイト (Unicode) の文字のサポート"
+
+#: application/helper/ApplicationUtils.php:301
+msgid "Required to use thumbnails"
+msgstr "サムネイルを使うことは必須です"
+
+#: application/helper/ApplicationUtils.php:302
+msgid "Localized text sorting (e.g. e->è->f)"
+msgstr "ローカライズされた文字のソート (例: e->è->f)"
+
+#: application/helper/ApplicationUtils.php:303
+msgid "Better retrieval of bookmark metadata and thumbnail"
+msgstr "ブックマークのメタデータとサムネイルの改善された検索"
+
+#: application/helper/ApplicationUtils.php:304
+msgid "Use the translation system in gettext mode"
+msgstr "gettext モードで翻訳システムを使用"
+
+#: application/helper/ApplicationUtils.php:305
+msgid "Login using LDAP server"
+msgstr "LDAP サーバーを使用してログイン"
+
+#: application/helper/DailyPageHelper.php:179
+msgid "Week"
+msgstr "週"
+
+#: application/helper/DailyPageHelper.php:183
+msgid "Today"
+msgstr "今日"
+
+#: application/helper/DailyPageHelper.php:185
+msgid "Yesterday"
+msgstr "昨日"
+
+#: application/helper/FileUtils.php:100
+msgid "Provided path is not a directory."
+msgstr "入力されたパスはディレクトリではありません。"
+
+#: application/helper/FileUtils.php:104
+msgid "Trying to delete a folder outside of Shaarli path."
+msgstr "Shaarli のパスの外にあるフォルダを削除しようとしています。"
+
+#: application/legacy/LegacyLinkDB.php:131
+msgid "You are not authorized to add a link."
+msgstr "リンクを追加するには、ログインする必要があります。"
+
+#: application/legacy/LegacyLinkDB.php:134
+msgid "Internal Error: A link should always have an id and URL."
+msgstr "エラー: リンクにはIDとURLを登録しなければなりません。"
+
+#: application/legacy/LegacyLinkDB.php:137
+msgid "You must specify an integer as a key."
+msgstr "正常なキーの値ではありません。"
+
+#: application/legacy/LegacyLinkDB.php:140
+msgid "Array offset and link ID must be equal."
+msgstr "Array オフセットとリンクのIDは同じでなければなりません。"
+
+#: 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"
+"\n"
+"To learn how to use Shaarli, consult the link \"Documentation\" at the "
+"bottom of this page.\n"
+"\n"
+"You use the community supported version of the original Shaarli project, by "
+"Sebastien Sauvage."
+msgstr ""
+"Shaarli へようこそ! これはあなたの最初の公開ブックマークです。これを編集した"
+"り削除したりするには、ログインする必要があります。\n"
+"\n"
+"Shaarli の使い方を知るには、このページの下にある「ドキュメント」のリンクを開"
+"いてください。\n"
+"\n"
+"あなたは Sebastien Sauvage による、コミュニティーサポートのあるバージョンのオ"
+"リジナルのShaarli プロジェクトを使用しています。"
+
+#: application/legacy/LegacyLinkDB.php:266
+msgid "My secret stuff... - Pastebin.com"
+msgstr "わたしのひ💗み💗つ💗 - Pastebin.com"
+
+#: application/legacy/LegacyLinkDB.php:268
+msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
+msgstr ""
+"シーッ! これはあなたしか見られないプライベートリンクです。消すこともできま"
+"す。"
+
+#: application/legacy/LegacyUpdater.php:104
+msgid "Couldn't retrieve updater class methods."
+msgstr "アップデーターのクラスメゾットを受信できませんでした。"
+
+#: application/legacy/LegacyUpdater.php:540
+msgid ""
+msgstr ""
+
+#: application/netscape/NetscapeBookmarkUtils.php:63
+msgid "Invalid export selection:"
+msgstr "不正なエクスポートの選択:"
+
+#: application/netscape/NetscapeBookmarkUtils.php:215
+#, php-format
+msgid "File %s (%d bytes) "
+msgstr "ファイル %s (%d バイト) "
+
+#: application/netscape/NetscapeBookmarkUtils.php:217
+msgid "has an unknown file format. Nothing was imported."
+msgstr "は不明なファイル形式です。インポートは中止されました。"
+
+#: application/netscape/NetscapeBookmarkUtils.php:221
+#, php-format
+msgid ""
+"was successfully processed in %d seconds: %d bookmarks imported, %d "
+"bookmarks overwritten, %d bookmarks skipped."
+msgstr ""
+"が %d 秒で処理され、%d 件のリンクがインポートされ、%d 件のリンクが上書きさ"
+"れ、%d 件のリンクがスキップされました。"
+
+#: application/plugin/PluginManager.php:103
+#: application/plugin/PluginManager.php:141
+msgid " [plugin incompatibility]: "
+msgstr " [非対応のプラグイン]: "
+
+#: application/plugin/exception/PluginFileNotFoundException.php:22
+#, php-format
+msgid "Plugin \"%s\" files not found."
+msgstr "プラグイン「%s」のファイルが存在しません。"
+
+#: application/render/PageCacheManager.php:33
+#, php-format
+msgid "Cannot purge %s: no directory"
+msgstr "%s を削除できません: ディレクトリが存在しません"
+
+#: application/updater/exception/UpdaterException.php:51
+msgid "An error occurred while running the update "
+msgstr "更新中に問題が発生しました "
+
+#: index.php:82
+msgid "Shared bookmarks on "
+msgstr "次において共有されたリンク "
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:31
+msgid "URI"
+msgstr "URI"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:35
+msgid "Add link"
+msgstr "リンクを追加"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:52
+msgid "Adds the addlink input on the linklist page."
+msgstr "リンク一覧のページに、リンクを追加するためのフォームを表示する。"
+
+#: plugins/archiveorg/archiveorg.php:29
+msgid "View on archive.org"
+msgstr "archive.org 上で表示する"
+
+#: plugins/archiveorg/archiveorg.php:42
+msgid "For each link, add an Archive.org icon."
+msgstr "それぞれのリンクに、Archive.org のアイコンを追加する。"
+
+#: plugins/default_colors/default_colors.php:38
+msgid ""
+"Default colors plugin error: This plugin is active and no custom color is "
+"configured."
+msgstr ""
+"既定の色のプラグインにおけるエラー: このプラグインは有効なので、カスタム カ"
+"ラーは適用されません。"
+
+#: plugins/default_colors/default_colors.php:127
+msgid "Override default theme colors. Use any CSS valid color."
+msgstr ""
+"既定のテーマの色を上書きします。どのような CSS カラーコードでも使えます。"
+
+#: plugins/default_colors/default_colors.php:128
+msgid "Main color (navbar green)"
+msgstr "メイン カラー (ナビバーの緑)"
+
+#: plugins/default_colors/default_colors.php:129
+msgid "Background color (light grey)"
+msgstr "背景色 (灰色)"
+
+#: plugins/default_colors/default_colors.php:130
+msgid "Dark main color (e.g. visited links)"
+msgstr "暗い方の メイン カラー (例: 閲覧済みリンク)"
+
+#: plugins/demo_plugin/demo_plugin.php:528
+msgid ""
+"A demo plugin covering all use cases for template designers and plugin "
+"developers."
+msgstr ""
+"テンプレートのデザイナーや、プラグインの開発者のためのすべての状況に対応でき"
+"るデモプラグインです。"
+
+#: plugins/demo_plugin/demo_plugin.php:529
+msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
+msgstr "これはデモプラグイン専用のパラメーターです。末尾に追加されます。"
+
+#: plugins/demo_plugin/demo_plugin.php:530
+msgid "Other demo parameter"
+msgstr "他のデモ パラメーター"
+
+#: plugins/isso/isso.php:22
+msgid ""
+"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
+"administration page."
+msgstr ""
+"Isso プラグインエラー: \"ISSO_SERVER\" の値をプラグイン管理ページにて指定して"
+"ください。"
+
+#: plugins/isso/isso.php:92
+msgid "Let visitor comment your shaares on permalinks with Isso."
+msgstr ""
+"Isso を使って、あなたのパーマリンク上のリンクに第三者がコメントを残すことがで"
+"きます。"
+
+#: plugins/isso/isso.php:93
+msgid "Isso server URL (without 'http://')"
+msgstr "Isso server URL ('http://' 抜き)"
+
+#: plugins/piwik/piwik.php:24
+msgid ""
+"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
+"administration page."
+msgstr ""
+"Piwik プラグインエラー: PIWIK_URL と PIWIK_SITEID の値をプラグイン管理ページ"
+"で指定してください。"
+
+#: plugins/piwik/piwik.php:73
+msgid "A plugin that adds Piwik tracking code to Shaarli pages."
+msgstr "Piwik のトラッキングコードをShaarliに追加するプラグインです。"
+
+#: plugins/piwik/piwik.php:74
+msgid "Piwik URL"
+msgstr "Piwik URL"
+
+#: plugins/piwik/piwik.php:75
+msgid "Piwik site ID"
+msgstr "Piwik サイトID"
+
+#: plugins/playvideos/playvideos.php:26
+msgid "Video player"
+msgstr "動画プレイヤー"
+
+#: plugins/playvideos/playvideos.php:29
+msgid "Play Videos"
+msgstr "動画を再生"
+
+#: plugins/playvideos/playvideos.php:60
+msgid "Add a button in the toolbar allowing to watch all videos."
+msgstr "すべての動画を閲覧するボタンをツールバーに追加します。"
+
+#: plugins/playvideos/youtube_playlist.js:214
+msgid "plugins/playvideos/jquery-1.11.2.min.js"
+msgstr "plugins/playvideos/jquery-1.11.2.min.js"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:72
+#, php-format
+msgid "Could not publish to PubSubHubbub: %s"
+msgstr "PubSubHubbub に登録できませんでした: %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:99
+#, php-format
+msgid "Could not post to %s"
+msgstr "%s に登録できませんでした"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:103
+#, php-format
+msgid "Bad response from the hub %s"
+msgstr "ハブ %s からの不正なレスポンス"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:114
+msgid "Enable PubSubHubbub feed publishing."
+msgstr "PubSubHubbub へのフィードを公開する。"
+
+#: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72
+msgid "For each link, add a QRCode icon."
+msgstr "それぞれのリンクについて、QRコードのアイコンを追加する。"
+
+#: plugins/wallabag/wallabag.php:22
+msgid ""
+"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
+"plugin administration page."
+msgstr ""
+"Wallabag プラグインエラー: \"WALLABAG_URL\" の値をプラグイン管理ページにおい"
+"て指定してください。"
+
+#: plugins/wallabag/wallabag.php:49
+msgid "Save to wallabag"
+msgstr "Wallabag に保存"
+
+#: plugins/wallabag/wallabag.php:73
+msgid "Wallabag API URL"
+msgstr "Wallabag のAPIのURL"
+
+#: plugins/wallabag/wallabag.php:74
+msgid "Wallabag API version (1 or 2)"
+msgstr "Wallabag のAPIのバージョン (1 または 2)"
+
+#: tests/LanguagesTest.php:214 tests/LanguagesTest.php:227
+#: tests/languages/fr/LanguagesFrTest.php:159
+#: tests/languages/fr/LanguagesFrTest.php:172
+msgid "Search"
+msgid_plural "Search"
+msgstr[0] "検索"
+msgstr[1] "検索"
+
+#~ msgid "The page you are trying to reach does not exist or has been deleted."
+#~ msgstr "あなたが開こうとしたページは存在しないか、削除されています。"
+
+#~ msgid "404 Not Found"
+#~ msgstr "404 ページが存在しません"
+
+#~ msgid "Updates file path is not set, can't write updates."
+#~ msgstr ""
+#~ "更新するファイルのパスが指定されていないため、更新を書き込めません。"
+
+#~ msgid "Unable to write updates in "
+#~ msgstr "更新を次の項目に書き込めませんでした: "
+
+#~ msgid "I said: NO. You are banned for the moment. Go away."
+#~ msgstr "あなたはこのサーバーからBANされています。"
+
+#~ msgid "Tag cloud"
+#~ msgstr "タグクラウド"
+
+#~ msgid "Click to try again."
+#~ msgstr "クリックして再度試します。"
+
+#~ msgid "Description will be rendered with"
+#~ msgstr "説明は次の方法で描画されます:"
+
+#~ msgid "Markdown syntax documentation"
+#~ msgstr "マークダウン形式のドキュメント"
+
+#~ msgid "Markdown syntax"
+#~ msgstr "マークダウン形式"
+
+#~ msgid ""
+#~ "Render shaare description with Markdown syntax.
Warning"
+#~ "strong>:\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 ""
+#~ "リンクの説明をマークダウン形式で表示します。
警告:\n"
+#~ "リンクの説明にHTMLタグがこのプラグインを有効にする前に含まれていた場合、\n"
+#~ "正常にページを表示できなくなるかもしれません。\n"
+#~ "詳しくは README をご覧ください。"
+
+#~ msgid "Sorry, nothing to see here."
+#~ msgstr "すみませんが、ここには何もありません。"
+
+#~ msgid "URL or leave empty to post a note"
+#~ msgstr "URL を入力するか、空欄にするとノートを投稿します"
+
+#~ msgid "Current password"
+#~ msgstr "現在のパスワード"
+
+#~ msgid "New password"
+#~ msgstr "新しいパスワード"
+
+#~ msgid "Change"
+#~ msgstr "変更"
+
+#~ msgid "Tag"
+#~ msgstr "タグ"
+
+#~ msgid "New name"
+#~ msgstr "変更先の名前"
+
+#~ msgid "Case sensitive"
+#~ msgstr "大文字と小文字を区別"
+
+#~ msgid "Rename"
+#~ msgstr "名前を変更"
+
+#~ msgid "Delete"
+#~ msgstr "削除"
+
+#~ msgid "You can also edit tags in the"
+#~ msgstr "次に含まれるタグを編集することもできます:"
+
+#~ msgid "tag list"
+#~ msgstr "タグ一覧"
+
+#~ msgid "title"
+#~ msgstr "タイトル"
+
+#~ msgid "Home link"
+#~ msgstr "ホームのリンク先"
+
+#~ msgid "Default value"
+#~ msgstr "既定の値"
+
+#~ msgid "Theme"
+#~ msgstr "テーマ"
+
+#~ msgid "Language"
+#~ msgstr "言語"
+
+#~ msgid "Timezone"
+#~ msgstr "タイムゾーン"
+
+#~ msgid "Continent"
+#~ msgstr "大陸"
+
+#~ msgid "City"
+#~ msgstr "町"
+
+#~ msgid "Disable session cookie hijacking protection"
+#~ msgstr "不正ログイン防止のためのセッションクッキーを無効化"
+
+#~ msgid ""
+#~ "Check this if you get disconnected or if your IP address changes often"
+#~ msgstr ""
+#~ "あなたが切断されたり、IPアドレスが頻繁に変わる環境下であるならチェックを入"
+#~ "れてください"
+
+#~ msgid "Private links by default"
+#~ msgstr "既定でプライベートリンク"
+
+#~ msgid "All new links are private by default"
+#~ msgstr "すべての新規リンクをプライベートで作成"
+
+#~ msgid "RSS direct links"
+#~ msgstr "RSS 直リンク"
+
+#~ msgid "Check this to use direct URL instead of permalink in feeds"
+#~ msgstr "フィードでパーマリンクの代わりに直リンクを使う"
+
+#~ msgid "Hide public links"
+#~ msgstr "公開リンクを隠す"
+
+#~ msgid "Do not show any links if the user is not logged in"
+#~ msgstr "ログインしていないユーザーには何のリンクも表示しない"
+
+#~ msgid "Notify me when a new release is ready"
+#~ msgstr "新しいバージョンがリリースされたときに通知"
+
+#~ msgid "Enable REST API"
+#~ msgstr "REST API を有効化"
+
+#~ msgid "Allow third party software to use Shaarli such as mobile application"
+#~ msgstr ""
+#~ "モバイルアプリといったサードパーティーのソフトウェアにShaarliを使用するこ"
+#~ "とを許可"
+
+#~ msgid "API secret"
+#~ msgstr "API シークレット"
+
+#~ msgid "Save"
+#~ msgstr "保存"
+
+#~ msgid "The Daily Shaarli"
+#~ msgstr "デイリーSharli"
+
+#~ msgid "1 RSS entry per day"
+#~ msgstr "各日1つずつのRSS項目"
+
+#~ msgid "Previous day"
+#~ msgstr "前日"
+
+#~ msgid "All links of one day in a single page."
+#~ msgstr "1日に作成されたすべてのリンクです。"
+
+#~ msgid "Next day"
+#~ msgstr "翌日"
+
+#~ msgid "Created:"
+#~ msgstr "作成:"
+
+#~ msgid "URL"
+#~ msgstr "URL"
+
+#~ msgid "Title"
+#~ msgstr "タイトル"
+
+#~ msgid "Description"
+#~ msgstr "説明"
+
+#~ msgid "Tags"
+#~ msgstr "タグ"
+
+#~ msgid "Private"
+#~ msgstr "プライベート"
+
+#~ msgid "Apply Changes"
+#~ msgstr "変更を適用"
+
+#~ msgid "Export Database"
+#~ msgstr "データベースをエクスポート"
+
+#~ msgid "Selection"
+#~ msgstr "選択済み"
+
+#~ msgid "All"
+#~ msgstr "すべて"
+
+#~ msgid "Public"
+#~ msgstr "公開"
+
+#~ msgid "Prepend note permalinks with this Shaarli instance's URL"
+#~ msgstr ""
+#~ "この Shaarli のインスタンスのURL にノートへのパーマリンクを付け加える"
+
+#~ msgid "Useful to import bookmarks in a web browser"
+#~ msgstr "ウェブブラウザーのリンクをインポートするのに有効です"
+
+#~ msgid "Import Database"
+#~ msgstr "データベースをインポート"
+
+#~ msgid "Maximum size allowed:"
+#~ msgstr "最大サイズ:"
+
+#~ msgid "Visibility"
+#~ msgstr "可視性"
+
+#~ msgid "Use values from the imported file, default to public"
+#~ msgstr "インポート元のファイルの値を使用 (既定は公開リンクとなります)"
+
+#~ msgid "Import all bookmarks as public"
+#~ msgstr "すべてのブックマーク項目を公開リンクとしてインポート"
+
+#~ msgid "Overwrite existing bookmarks"
+#~ msgstr "既に存在しているブックマークを上書き"
+
+#~ msgid "Duplicates based on URL"
+#~ msgstr "URL による重複"
+
+#~ msgid "Add default tags"
+#~ msgstr "既定のタグを追加"
+
+#~ msgid ""
+#~ "It looks like it's the first time you run Shaarli. Please configure it."
+#~ msgstr "どうやら Shaarli を初めて起動しているようです。設定してください。"
+
+#~ msgid "Username"
+#~ msgstr "ユーザー名"
+
+#~ msgid "Password"
+#~ msgstr "パスワード"
+
+#~ msgid "Shaarli title"
+#~ msgstr "Shaarli のタイトル"
+
+#~ msgid "My links"
+#~ msgstr "自分のリンク"
+
+#~ msgid "Install"
+#~ msgstr "インストール"
+
+#~ msgid "shaare"
+#~ msgid_plural "shaares"
+#~ msgstr[0] "共有"
+#~ msgstr[1] "共有"
+
+#~ msgid "private link"
+#~ msgid_plural "private links"
+#~ msgstr[0] "プライベートリンク"
+#~ msgstr[1] "プライベートリンク"
+
+#~ msgid "Search text"
+#~ msgstr "文字列で検索"
+
+#~ msgid "Filter by tag"
+#~ msgstr "タグによって分類"
+
+#~ msgid "Nothing found."
+#~ msgstr "何も見つかりませんでした。"
+
+#~ msgid "%s result"
+#~ msgid_plural "%s results"
+#~ msgstr[0] "%s 件の結果"
+#~ msgstr[1] "%s 件の結果"
+
+#~ msgid "for"
+#~ msgstr "for"
+
+#~ msgid "tagged"
+#~ msgstr "タグ付けされた"
+
+#~ msgid "Remove tag"
+#~ msgstr "タグを削除"
+
+#~ msgid "with status"
+#~ msgstr "with status"
+
+#~ msgid "without any tag"
+#~ msgstr "タグなし"
+
+#~ msgid "Fold"
+#~ msgstr "畳む"
+
+#~ msgid "Edited: "
+#~ msgstr "編集済み: "
+
+#~ msgid "permalink"
+#~ msgstr "パーマリンク"
+
+#~ msgid "Add tag"
+#~ msgstr "タグを追加"
+
+#~ msgid "Filters"
+#~ msgstr "分類"
+
+#~ msgid "Only display private links"
+#~ msgstr "プライベートリンクのみを表示"
+
+#~ msgid "Only display public links"
+#~ msgstr "公開リンクのみを表示"
+
+#~ msgid "Filter untagged links"
+#~ msgstr "タグ付けされていないリンクで分類"
+
+#~ msgid "Fold all"
+#~ msgstr "すべて畳む"
+
+#~ msgid "Links per page"
+#~ msgstr "各ページをリンク"
+
+#~ msgid "Remember me"
+#~ msgstr "パスワードを保存"
+
+#~ msgid "by the Shaarli community"
+#~ msgstr "by Shaarli コミュニティ"
+
+#~ msgid "Documentation"
+#~ msgstr "ドキュメント"
+
+#~ msgid "Expand"
+#~ msgstr "展開する"
+
+#~ msgid "Expand all"
+#~ msgstr "すべて展開する"
+
+#~ msgid "Are you sure you want to delete this link?"
+#~ msgstr "本当にこのリンクを削除しますか?"
+
+#~ msgid "RSS Feed"
+#~ msgstr "RSS フィード"
+
+#~ msgid "Logout"
+#~ msgstr "ログアウト"
+
+#~ msgid "is available"
+#~ msgstr "が利用可能"
+
+#~ msgid "Picture Wall"
+#~ msgstr "ピクチャーウォール"
+
+#~ msgid "pics"
+#~ msgstr "画像"
+
+#~ msgid "You need to enable Javascript to change plugin loading order."
+#~ msgstr ""
+#~ "プラグインを読み込む順番を変更するには、Javascriptを有効にする必要がありま"
+#~ "す。"
+
+#~ msgid "Enabled Plugins"
+#~ msgstr "有効なプラグイン"
+
+#~ msgid "No plugin enabled."
+#~ msgstr "有効なプラグインはありません。"
+
+#~ msgid "Disable"
+#~ msgstr "無効化"
+
+#~ msgid "Name"
+#~ msgstr "名前"
+
+#~ msgid "Order"
+#~ msgstr "順序"
+
+#~ msgid "Disabled Plugins"
+#~ msgstr "無効なプラグイン"
+
+#~ msgid "No plugin disabled."
+#~ msgstr "無効なプラグインはありません。"
+
+#~ msgid "Enable"
+#~ msgstr "有効化"
+
+#~ msgid "More plugins available"
+#~ msgstr "さらに利用できるプラグインがあります"
+
+#~ msgid "in the documentation"
+#~ msgstr "ドキュメント内"
+
+#~ msgid "No parameter available."
+#~ msgstr "利用可能な設定項目はありません。"
+
+#~ msgid "tags"
+#~ msgstr "タグ"
+
+#~ msgid "List all links with those tags"
+#~ msgstr "このタグが付いているリンクをリスト化する"
+
+#~ msgid "Sort by:"
+#~ msgstr "分類:"
+
+#~ msgid "Cloud"
+#~ msgstr "クラウド"
+
+#~ msgid "Most used"
+#~ msgstr "もっとも使われた"
+
+#~ msgid "Alphabetical"
+#~ msgstr "アルファベット順"
+
+#~ msgid "Settings"
+#~ msgstr "設定"
+
+#~ msgid "Change Shaarli settings: title, timezone, etc."
+#~ msgstr "Shaarli の設定を変更: タイトル、タイムゾーンなど。"
+
+#~ msgid "Configure your Shaarli"
+#~ msgstr "あなたの Shaarli を設定"
+
+#~ msgid "Enable, disable and configure plugins"
+#~ msgstr "プラグインを有効化、無効化、設定する"
+
+#~ msgid "Change your password"
+#~ msgstr "パスワードを変更"
+
+#~ msgid "Rename or delete a tag in all links"
+#~ msgstr "すべてのリンクのタグの名前を変更する、または削除する"
+
+#~ msgid ""
+#~ "Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
+#~ "delicious...)"
+#~ msgstr ""
+#~ "Netscape HTML 形式のブックマークをインポートする (Firefox、Chrome、Operaと"
+#~ "いったブラウザーが含まれます)"
+
+#~ msgid "Import links"
+#~ msgstr "リンクをインポート"
+
+#~ msgid ""
+#~ "Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
+#~ "Opera, delicious...)"
+#~ msgstr ""
+#~ "Netscape HTML 形式のブックマークをエクスポートする (Firefox、Chrome、Opera"
+#~ "といったブラウザーが含まれます)"
+
+#~ msgid "Export database"
+#~ msgstr "リンクをエクスポート"
+
+#~ msgid ""
+#~ "Drag one of these button to your bookmarks toolbar or right-click it and "
+#~ "\"Bookmark This Link\""
+#~ msgstr ""
+#~ "これらのボタンのうち1つををブックマークバーにドラッグするか、右クリックし"
+#~ "て「このリンクをブックマークに追加」してください"
+
+#~ msgid "then click on the bookmarklet in any page you want to share."
+#~ msgstr "共有したいページでブックマークレットをクリックしてください。"
+
+#~ msgid ""
+#~ "Drag this link to your bookmarks toolbar or right-click it and Bookmark "
+#~ "This Link"
+#~ msgstr ""
+#~ "このリンクをブックマークバーにドラッグするか、右クリックして「このリンクを"
+#~ "ブックマークに追加」してください"
+
+#~ msgid "then click ✚Shaare link button in any page you want to share"
+#~ msgstr ""
+#~ "✚リンクを共有 ボタンをクリックすることで、どこでもリンクを共有できます"
+
+#~ msgid "The selected text is too long, it will be truncated."
+#~ msgstr "選択された文字列は長すぎるので、一部が切り捨てられます。"
+
+#~ msgid "Shaare link"
+#~ msgstr "共有リンク"
+
+#~ msgid ""
+#~ "Then click ✚Add Note button anytime to start composing a private Note "
+#~ "(text post) to your Shaarli"
+#~ msgstr ""
+#~ "✚ノートを追加 ボタンをクリックすることで、いつでもプライベートノート(テキ"
+#~ "スト形式)をShaarli上に作成できます"
+
+#~ msgid "Add Note"
+#~ msgstr "ノートを追加"
+
+#~ msgid ""
+#~ "You need to browse your Shaarli over HTTPS to use this "
+#~ "functionality."
+#~ msgstr ""
+#~ "この機能を使用するには、HTTPS 経由でShaarliに接続してくだ"
+#~ "さい。"
+
+#~ msgid "Add to"
+#~ msgstr "次に追加:"
+
+#~ msgid "3rd party"
+#~ msgstr "サードパーティー"
+
+#~ msgid "Plugin"
+#~ msgstr "プラグイン"
+
+#~ msgid "plugin"
+#~ msgstr "プラグイン"
+
+#~ msgid ""
+#~ "Drag this link to your bookmarks toolbar, or right-click it and choose "
+#~ "Bookmark This Link"
+#~ msgstr ""
+#~ "このリンクをブックマークバーにドラッグするか、右クリックして「このリンクを"
+#~ "ブックマークに追加」してください"
diff --git a/inc/languages/ru/LC_MESSAGES/shaarli.po b/inc/languages/ru/LC_MESSAGES/shaarli.po
new file mode 100644
index 00000000..98e70425
--- /dev/null
+++ b/inc/languages/ru/LC_MESSAGES/shaarli.po
@@ -0,0 +1,1944 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Shaarli\n"
+"POT-Creation-Date: 2020-11-14 07:47+0500\n"
+"PO-Revision-Date: 2020-11-15 06:16+0500\n"
+"Last-Translator: progit \n"
+"Language-Team: Shaarli\n"
+"Language: ru_RU\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.0.1\n"
+"X-Poedit-Basepath: ../../../..\n"
+"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
+"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Poedit-KeywordsList: t:1,2;t\n"
+"X-Poedit-SearchPath-0: application\n"
+"X-Poedit-SearchPath-1: tmp\n"
+"X-Poedit-SearchPath-2: index.php\n"
+"X-Poedit-SearchPath-3: init.php\n"
+"X-Poedit-SearchPath-4: plugins\n"
+
+#: application/History.php:181
+msgid "History file isn't readable or writable"
+msgstr "Файл истории не доступен для чтения или записи"
+
+#: application/History.php:192
+msgid "Could not parse history file"
+msgstr "Не удалось разобрать файл истории"
+
+#: application/Languages.php:184
+msgid "Automatic"
+msgstr "Автоматический"
+
+#: application/Languages.php:185
+msgid "German"
+msgstr "Немецкий"
+
+#: application/Languages.php:186
+msgid "English"
+msgstr "Английский"
+
+#: application/Languages.php:187
+msgid "French"
+msgstr "Французский"
+
+#: application/Languages.php:188
+msgid "Japanese"
+msgstr "Японский"
+
+#: application/Languages.php:189
+msgid "Russian"
+msgstr "Русский"
+
+#: application/Thumbnailer.php:62
+msgid ""
+"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
+"disabled. Please reload the page."
+msgstr ""
+"для использования миниатюр необходимо загрузить расширение php-gd. Миниатюры "
+"сейчас отключены. Перезагрузите страницу."
+
+#: application/Utils.php:405
+msgid "Setting not set"
+msgstr "Настройка не задана"
+
+#: application/Utils.php:412
+msgid "Unlimited"
+msgstr "Неограниченно"
+
+#: application/Utils.php:415
+msgid "B"
+msgstr "Б"
+
+#: application/Utils.php:415
+msgid "kiB"
+msgstr "КБ"
+
+#: application/Utils.php:415
+msgid "MiB"
+msgstr "МБ"
+
+#: application/Utils.php:415
+msgid "GiB"
+msgstr "ГБ"
+
+#: 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 "У вас нет прав на изменение хранилища данных"
+
+#: application/bookmark/BookmarkFileService.php:210
+msgid "This bookmarks already exists"
+msgstr "Эта закладка уже существует"
+
+#: application/bookmark/BookmarkInitializer.php:42
+msgid "(private bookmark with thumbnail demo)"
+msgstr "(личная закладка с показом миниатюр)"
+
+#: 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 автоматически подберет миниатюру для ссылок на различные сайты.\n"
+"\n"
+"Изучите Shaarli, попробовав элементы управления и меню.\n"
+"Посетите проект [Github](https://github.com/shaarli/Shaarli) или "
+"[документацию](https://shaarli.readthedocs.io/en/master/),чтобы узнать "
+"больше о Shaarli.\n"
+"\n"
+"Теперь вы можете редактировать или удалять шаары по умолчанию.\n"
+
+#: application/bookmark/BookmarkInitializer.php:58
+msgid "Note: Shaare descriptions"
+msgstr "Примечание: описания Шаар"
+
+#: 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 ""
+"При добавлении закладки без ввода URL адреса создается текстовая \"заметка"
+"\", такая как эта.\n"
+"Эта заметка является личной, поэтому вы единственный, кто может ее увидеть, "
+"находясь в системе.\n"
+"\n"
+"Вы можете использовать это для хранения заметок, публикации статей, "
+"фрагментов кода и многого другого.\n"
+"\n"
+"Параметр форматирования Markdown позволяет форматировать заметки и описание "
+"закладок:\n"
+"\n"
+"### Заголовок заголовков\n"
+"\n"
+"#### Multiple headings levels\n"
+" * маркированные списки\n"
+" * _наклонный_ текст\n"
+" * **жирный** текст\n"
+" * ~~зачеркнутый~~ текст\n"
+" * блоки `кода`\n"
+" * изображения\n"
+" * [ссылки](https://en.wikipedia.org/wiki/Markdown)\n"
+"\n"
+"Markdown также поддерживает таблицы:\n"
+"\n"
+"| Имя | Тип | Цвет | Количество |\n"
+"| ------- | --------- | ------ | ----- |\n"
+"| Апельсин | Фрукт | Оранжевый | 126 |\n"
+"| Яблоко | Фрукт | Любой | 62 |\n"
+"| Лимон | Фрукт | Желтый | 30 |\n"
+"| Морковь | Овощ | Красный | 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 "Личный, минималистичный, сверхбыстрый сервис закладок без баз данных"
+
+#: 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 ""
+"Добро пожаловать в Shaarli!\n"
+"\n"
+"Shaarli позволяет добавлять в закладки свои любимые страницы и делиться ими "
+"с другими или хранить их в частном порядке.\n"
+"Вы можете добавить описание к своим закладкам, например этой, и пометить "
+"их.\n"
+"\n"
+"Создайте новую закладку, нажав кнопку `+Поделиться`, или используя любой из "
+"рекомендуемых инструментов (расширение для браузера, мобильное приложение, "
+"букмарклет, REST API и т.д.).\n"
+"\n"
+"Вы можете легко получить свои ссылки, даже если их тысячи, с помощью "
+"внутренней поисковой системы или поиска по тегам (например, эта заметка "
+"помечена тегами `shaarli` and `help`).\n"
+"Также поддерживаются хэштеги, такие как #shaarli #help.\n"
+"Вы можете также фильтровать доступный [RSS канал](/feed/atom) и галерею по "
+"тегу или по поиску текста.\n"
+"\n"
+"Мы надеемся, что вам понравится использовать Shaarli, с ❤️ поддерживаемый "
+"сообществом!\n"
+"Не стесняйтесь открывать [запрос](https://github.com/shaarli/Shaarli/"
+"issues), если у вас есть предложение или возникла проблема.\n"
+
+#: application/bookmark/exception/BookmarkNotFoundException.php:14
+msgid "The link you are trying to reach does not exist or has been deleted."
+msgstr ""
+"Ссылка, по которой вы пытаетесь перейти, не существует или была удалена."
+
+#: 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 не удалось создать файл конфигурации. Убедитесь, что у Shaarli есть "
+"право на запись в папку, в которой он установлен."
+
+#: application/config/ConfigManager.php:137
+#: application/config/ConfigManager.php:164
+msgid "Invalid setting key parameter. String expected, got: "
+msgstr "Неверная настройка ключевого параметра. Ожидалась строка, получено: "
+
+#: application/config/exception/MissingFieldConfigException.php:20
+#, php-format
+msgid "Configuration value is required for %s"
+msgstr "Значение конфигурации требуется для %s"
+
+#: application/config/exception/PluginConfigOrderException.php:15
+msgid "An error occurred while trying to save plugins loading order."
+msgstr "Произошла ошибка при попытке сохранить порядок загрузки плагинов."
+
+#: application/config/exception/UnauthorizedConfigException.php:15
+msgid "You are not authorized to alter config."
+msgstr "Вы не авторизованы для изменения конфигурации."
+
+#: application/exceptions/IOException.php:23
+msgid "Error accessing"
+msgstr "Ошибка доступа"
+
+#: application/feed/FeedBuilder.php:180
+msgid "Direct link"
+msgstr "Прямая ссылка"
+
+#: 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 "Постоянная ссылка"
+
+#: application/front/controller/admin/ConfigureController.php:56
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "Configure"
+msgstr "Настройка"
+
+#: application/front/controller/admin/ConfigureController.php:106
+#: application/legacy/LegacyUpdater.php:539
+msgid "You have enabled or changed thumbnails mode."
+msgstr "Вы включили или изменили режим миниатюр."
+
+#: application/front/controller/admin/ConfigureController.php:108
+#: application/front/controller/admin/ServerController.php:76
+#: application/legacy/LegacyUpdater.php:540
+msgid "Please synchronize them."
+msgstr "Пожалуйста, синхронизируйте их."
+
+#: application/front/controller/admin/ConfigureController.php:119
+#: application/front/controller/visitor/InstallController.php:149
+msgid "Error while writing config file after configuration update."
+msgstr "Ошибка при записи файла конфигурации после обновления конфигурации."
+
+#: application/front/controller/admin/ConfigureController.php:128
+msgid "Configuration was saved."
+msgstr "Конфигурация сохранена."
+
+#: application/front/controller/admin/ExportController.php:26
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
+msgid "Export"
+msgstr "Экспорт"
+
+#: application/front/controller/admin/ExportController.php:42
+msgid "Please select an export mode."
+msgstr "Выберите режим экспорта."
+
+#: application/front/controller/admin/ImportController.php:41
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+msgid "Import"
+msgstr "Импорт"
+
+#: application/front/controller/admin/ImportController.php:55
+msgid "No import file provided."
+msgstr "Файл импорта не предоставлен."
+
+#: 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 ""
+"Файл, который вы пытаетесь загрузить, вероятно, больше, чем может принять "
+"этот сервер (%s). Пожалуйста, загружайте небольшими частями."
+
+#: application/front/controller/admin/ManageTagController.php:30
+msgid "whitespace"
+msgstr "пробел"
+
+#: application/front/controller/admin/ManageTagController.php:35
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+msgid "Manage tags"
+msgstr "Управление тегами"
+
+#: application/front/controller/admin/ManageTagController.php:54
+msgid "Invalid tags provided."
+msgstr "Предоставлены недействительные теги."
+
+#: 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] "Тег был удален из %d закладки."
+msgstr[1] "Тег был удален из %d закладок."
+msgstr[2] "Тег был удален из %d закладок."
+
+#: 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] "Тег был переименован в %d закладке."
+msgstr[1] "Тег был переименован в %d закладках."
+msgstr[2] "Тег был переименован в %d закладках."
+
+#: application/front/controller/admin/ManageTagController.php:105
+msgid "Tags separator must be a single character."
+msgstr "Разделитель тегов должен состоять из одного символа."
+
+#: application/front/controller/admin/ManageTagController.php:111
+msgid "These characters are reserved and can't be used as tags separator: "
+msgstr ""
+"Эти символы зарезервированы и не могут использоваться в качестве разделителя "
+"тегов: "
+
+#: application/front/controller/admin/PasswordController.php:28
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+msgid "Change password"
+msgstr "Изменить пароль"
+
+#: application/front/controller/admin/PasswordController.php:55
+msgid "You must provide the current and new password to change it."
+msgstr "Вы должны предоставить текущий и новый пароль, чтобы изменить его."
+
+#: application/front/controller/admin/PasswordController.php:71
+msgid "The old password is not correct."
+msgstr "Старый пароль неверен."
+
+#: application/front/controller/admin/PasswordController.php:97
+msgid "Your password has been changed"
+msgstr "Пароль изменен"
+
+#: application/front/controller/admin/PluginsController.php:45
+msgid "Plugin Administration"
+msgstr "Управление плагинами"
+
+#: application/front/controller/admin/PluginsController.php:76
+msgid "Setting successfully saved."
+msgstr "Настройка успешно сохранена."
+
+#: application/front/controller/admin/PluginsController.php:79
+msgid "Error while saving plugin configuration: "
+msgstr "Ошибка при сохранении конфигурации плагина: "
+
+#: application/front/controller/admin/ServerController.php:35
+msgid "Check disabled"
+msgstr "Проверка отключена"
+
+#: application/front/controller/admin/ServerController.php:57
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Server administration"
+msgstr "Администрирование сервера"
+
+#: application/front/controller/admin/ServerController.php:74
+msgid "Thumbnails cache has been cleared."
+msgstr "Кэш миниатюр очищен."
+
+#: application/front/controller/admin/ServerController.php:85
+msgid "Shaarli's cache folder has been cleared!"
+msgstr "Папка с кэшем Shaarli очищена!"
+
+#: application/front/controller/admin/ShaareAddController.php:26
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+msgid "Shaare a new link"
+msgstr "Поделиться новой ссылкой"
+
+#: application/front/controller/admin/ShaareManageController.php:35
+#: application/front/controller/admin/ShaareManageController.php:93
+msgid "Invalid bookmark ID provided."
+msgstr "Указан неверный идентификатор закладки."
+
+#: 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 "Закладка с идентификатором %s не найдена."
+
+#: application/front/controller/admin/ShaareManageController.php:101
+msgid "Invalid visibility provided."
+msgstr "Предоставлена недопустимая видимость."
+
+#: application/front/controller/admin/ShaarePublishController.php:173
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
+msgid "Edit"
+msgstr "Редактировать"
+
+#: application/front/controller/admin/ShaarePublishController.php:176
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
+msgid "Shaare"
+msgstr "Поделиться"
+
+#: application/front/controller/admin/ShaarePublishController.php:208
+msgid "Note: "
+msgstr "Заметка: "
+
+#: application/front/controller/admin/ThumbnailsController.php:37
+#: tmp/thumbnails.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Thumbnails update"
+msgstr "Обновление миниатюр"
+
+#: application/front/controller/admin/ToolsController.php:31
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33
+msgid "Tools"
+msgstr "Инструменты"
+
+#: application/front/controller/visitor/BookmarkListController.php:121
+msgid "Search: "
+msgstr "Поиск: "
+
+#: application/front/controller/visitor/DailyController.php:200
+msgid "day"
+msgstr "день"
+
+#: application/front/controller/visitor/DailyController.php:200
+#: application/front/controller/visitor/DailyController.php:203
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
+msgid "Daily"
+msgstr "За день"
+
+#: application/front/controller/visitor/DailyController.php:201
+msgid "week"
+msgstr "неделя"
+
+#: application/front/controller/visitor/DailyController.php:201
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Weekly"
+msgstr "За неделю"
+
+#: application/front/controller/visitor/DailyController.php:202
+msgid "month"
+msgstr "месяц"
+
+#: application/front/controller/visitor/DailyController.php:202
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "Monthly"
+msgstr "За месяц"
+
+#: application/front/controller/visitor/ErrorController.php:30
+msgid "Error: "
+msgstr "Ошибка: "
+
+#: application/front/controller/visitor/ErrorController.php:34
+msgid "Please report it on Github."
+msgstr "Пожалуйста, сообщите об этом на Github."
+
+#: application/front/controller/visitor/ErrorController.php:39
+msgid "An unexpected error occurred."
+msgstr "Произошла непредвиденная ошибка."
+
+#: application/front/controller/visitor/ErrorNotFoundController.php:25
+msgid "Requested page could not be found."
+msgstr "Запрошенная страница не может быть найдена."
+
+#: application/front/controller/visitor/InstallController.php:65
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+msgid "Install Shaarli"
+msgstr "Установить Shaarli"
+
+#: application/front/controller/visitor/InstallController.php:85
+#, 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 ""
+"Сессии на вашем сервере работают некорректно.
Убедитесь, что "
+"переменная \"session.save_path\" правильно установлена в вашей конфигурации "
+"PHP и что у вас есть доступ к ней на запись.
В настоящее время она "
+"указывает на %s.
В некоторых браузерах доступ к вашему серверу через имя "
+"хоста, например localhost или любое другое имя хоста без точки, приводит к "
+"сбою хранилища файлов cookie. Мы рекомендуем получить доступ к вашему "
+"серверу через его IP адрес или полное доменное имя.
"
+
+#: application/front/controller/visitor/InstallController.php:157
+msgid ""
+"Shaarli is now configured. Please login and start shaaring your bookmarks!"
+msgstr "Shaarli настроен. Войдите и начните делиться своими закладками!"
+
+#: application/front/controller/visitor/InstallController.php:171
+msgid "Insufficient permissions:"
+msgstr "Недостаточно разрешений:"
+
+#: 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 "Вход"
+
+#: application/front/controller/visitor/LoginController.php:78
+msgid "Wrong login/password."
+msgstr "Неверный логин или пароль."
+
+#: 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 "Галерея"
+
+#: application/front/controller/visitor/TagCloudController.php:90
+msgid "Tag "
+msgstr "Тег "
+
+#: application/front/exceptions/AlreadyInstalledException.php:11
+msgid "Shaarli has already been installed. Login to edit the configuration."
+msgstr "Shaarli уже установлен. Войдите, чтобы изменить конфигурацию."
+
+#: application/front/exceptions/LoginBannedException.php:11
+msgid ""
+"You have been banned after too many failed login attempts. Try again later."
+msgstr ""
+"Вы были заблокированы из-за большого количества неудачных попыток входа в "
+"систему. Попробуйте позже."
+
+#: application/front/exceptions/OpenShaarliPasswordException.php:16
+msgid "You are not supposed to change a password on an Open Shaarli."
+msgstr "Вы не должны менять пароль на Open Shaarli."
+
+#: application/front/exceptions/ThumbnailsDisabledException.php:11
+msgid "Picture wall unavailable (thumbnails are disabled)."
+msgstr "Галерея недоступна (миниатюры отключены)."
+
+#: application/front/exceptions/WrongTokenException.php:16
+msgid "Wrong token."
+msgstr "Неправильный токен."
+
+#: application/helper/ApplicationUtils.php:163
+#, php-format
+msgid ""
+"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
+"cannot run. Your PHP version has known security vulnerabilities and should "
+"be updated as soon as possible."
+msgstr ""
+"Ваша версия PHP устарела! Shaarli требует как минимум PHP %s, и поэтому не "
+"может работать. В вашей версии PHP есть известные уязвимости в системе "
+"безопасности, и ее следует обновить как можно скорее."
+
+#: application/helper/ApplicationUtils.php:198
+#: application/helper/ApplicationUtils.php:218
+msgid "directory is not readable"
+msgstr "папка не доступна для чтения"
+
+#: application/helper/ApplicationUtils.php:221
+msgid "directory is not writable"
+msgstr "папка не доступна для записи"
+
+#: application/helper/ApplicationUtils.php:245
+msgid "file is not readable"
+msgstr "файл не доступен для чтения"
+
+#: application/helper/ApplicationUtils.php:248
+msgid "file is not writable"
+msgstr "файл не доступен для записи"
+
+#: application/helper/ApplicationUtils.php:282
+msgid "Configuration parsing"
+msgstr "Разбор конфигурации"
+
+#: application/helper/ApplicationUtils.php:283
+msgid "Slim Framework (routing, etc.)"
+msgstr "Slim Framework (маршрутизация и т. д.)"
+
+#: application/helper/ApplicationUtils.php:284
+msgid "Multibyte (Unicode) string support"
+msgstr "Поддержка многобайтовых (Unicode) строк"
+
+#: application/helper/ApplicationUtils.php:285
+msgid "Required to use thumbnails"
+msgstr "Обязательно использование миниатюр"
+
+#: application/helper/ApplicationUtils.php:286
+msgid "Localized text sorting (e.g. e->è->f)"
+msgstr "Локализованная сортировка текста (например, e->è->f)"
+
+#: application/helper/ApplicationUtils.php:287
+msgid "Better retrieval of bookmark metadata and thumbnail"
+msgstr "Лучшее получение метаданных закладок и миниатюр"
+
+#: application/helper/ApplicationUtils.php:288
+msgid "Use the translation system in gettext mode"
+msgstr "Используйте систему перевода в режиме gettext"
+
+#: application/helper/ApplicationUtils.php:289
+msgid "Login using LDAP server"
+msgstr "Вход через LDAP сервер"
+
+#: application/helper/DailyPageHelper.php:172
+msgid "Week"
+msgstr "Неделя"
+
+#: application/helper/DailyPageHelper.php:176
+msgid "Today"
+msgstr "Сегодня"
+
+#: application/helper/DailyPageHelper.php:178
+msgid "Yesterday"
+msgstr "Вчера"
+
+#: application/helper/FileUtils.php:100
+msgid "Provided path is not a directory."
+msgstr "Указанный путь не является папкой."
+
+#: application/helper/FileUtils.php:104
+msgid "Trying to delete a folder outside of Shaarli path."
+msgstr "Попытка удалить папку за пределами пути Shaarli."
+
+#: application/legacy/LegacyLinkDB.php:131
+msgid "You are not authorized to add a link."
+msgstr "Вы не авторизованы для изменения ссылки."
+
+#: application/legacy/LegacyLinkDB.php:134
+msgid "Internal Error: A link should always have an id and URL."
+msgstr "Внутренняя ошибка: ссылка всегда должна иметь идентификатор и URL."
+
+#: application/legacy/LegacyLinkDB.php:137
+msgid "You must specify an integer as a key."
+msgstr "В качестве ключа необходимо указать целое число."
+
+#: application/legacy/LegacyLinkDB.php:140
+msgid "Array offset and link ID must be equal."
+msgstr "Смещение массива и идентификатор ссылки должны быть одинаковыми."
+
+#: 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"
+"\n"
+"To learn how to use Shaarli, consult the link \"Documentation\" at the "
+"bottom of this page.\n"
+"\n"
+"You use the community supported version of the original Shaarli project, by "
+"Sebastien Sauvage."
+msgstr ""
+"Добро пожаловать в Shaarli! Это ваша первая общедоступная закладка. Чтобы "
+"отредактировать или удалить меня, вы должны сначала авторизоваться.\n"
+"\n"
+"Чтобы узнать, как использовать Shaarli, перейдите по ссылке \"Документация\" "
+"внизу этой страницы.\n"
+"\n"
+"Вы используете поддерживаемую сообществом версию оригинального проекта "
+"Shaarli от Себастьяна Соваж."
+
+#: application/legacy/LegacyLinkDB.php:266
+msgid "My secret stuff... - Pastebin.com"
+msgstr "Мой секрет... - Pastebin.com"
+
+#: application/legacy/LegacyLinkDB.php:268
+msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
+msgstr ""
+"Тссс! Это личная ссылка, которую видите только ВЫ. Вы тоже можете удалить "
+"меня."
+
+#: application/legacy/LegacyUpdater.php:104
+msgid "Couldn't retrieve updater class methods."
+msgstr "Не удалось получить методы класса средства обновления."
+
+#: application/legacy/LegacyUpdater.php:540
+msgid ""
+msgstr ""
+
+#: application/netscape/NetscapeBookmarkUtils.php:63
+msgid "Invalid export selection:"
+msgstr "Неверный выбор экспорта:"
+
+#: application/netscape/NetscapeBookmarkUtils.php:215
+#, php-format
+msgid "File %s (%d bytes) "
+msgstr "Файл %s (%d байт) "
+
+#: application/netscape/NetscapeBookmarkUtils.php:217
+msgid "has an unknown file format. Nothing was imported."
+msgstr "имеет неизвестный формат файла. Ничего не импортировано."
+
+#: application/netscape/NetscapeBookmarkUtils.php:221
+#, php-format
+msgid ""
+"was successfully processed in %d seconds: %d bookmarks imported, %d "
+"bookmarks overwritten, %d bookmarks skipped."
+msgstr ""
+"успешно обработано за %d секунд: %d закладок импортировано, %d закладок "
+"перезаписаны, %d закладок пропущено."
+
+#: application/plugin/PluginManager.php:125
+msgid " [plugin incompatibility]: "
+msgstr " [несовместимость плагинов]: "
+
+#: application/plugin/exception/PluginFileNotFoundException.php:22
+#, php-format
+msgid "Plugin \"%s\" files not found."
+msgstr "Файл плагина \"%s\" не найден."
+
+#: application/render/PageCacheManager.php:32
+#, php-format
+msgid "Cannot purge %s: no directory"
+msgstr "Невозможно очистить%s: нет папки"
+
+#: application/updater/exception/UpdaterException.php:51
+msgid "An error occurred while running the update "
+msgstr "Произошла ошибка при запуске обновления "
+
+#: index.php:81
+msgid "Shared bookmarks on "
+msgstr "Общие закладки на "
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:31
+msgid "URI"
+msgstr "URI"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:35
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
+msgid "Add link"
+msgstr "Добавить ссылку"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:52
+msgid "Adds the addlink input on the linklist page."
+msgstr ""
+"Добавляет на страницу списка ссылок поле для добавления новой закладки."
+
+#: plugins/archiveorg/archiveorg.php:29
+msgid "View on archive.org"
+msgstr "Посмотреть на archive.org"
+
+#: plugins/archiveorg/archiveorg.php:42
+msgid "For each link, add an Archive.org icon."
+msgstr "Для каждой ссылки добавить значок с Archive.org."
+
+#: plugins/default_colors/default_colors.php:38
+msgid ""
+"Default colors plugin error: This plugin is active and no custom color is "
+"configured."
+msgstr ""
+"Ошибка плагина цветов по умолчанию: этот плагин активен, и пользовательский "
+"цвет не настроен."
+
+#: plugins/default_colors/default_colors.php:113
+msgid "Override default theme colors. Use any CSS valid color."
+msgstr ""
+"Переопределить цвета темы по умолчанию. Используйте любой допустимый цвет "
+"CSS."
+
+#: plugins/default_colors/default_colors.php:114
+msgid "Main color (navbar green)"
+msgstr "Основной цвет (зеленый на панели навигации)"
+
+#: plugins/default_colors/default_colors.php:115
+msgid "Background color (light grey)"
+msgstr "Цвет фона (светло-серый)"
+
+#: plugins/default_colors/default_colors.php:116
+msgid "Dark main color (e.g. visited links)"
+msgstr "Темный основной цвет (например, посещенные ссылки)"
+
+#: plugins/demo_plugin/demo_plugin.php:478
+msgid ""
+"A demo plugin covering all use cases for template designers and plugin "
+"developers."
+msgstr ""
+"Демо плагин, охватывающий все варианты использования для дизайнеров шаблонов "
+"и разработчиков плагинов."
+
+#: plugins/demo_plugin/demo_plugin.php:479
+msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
+msgstr ""
+"Это параметр предназначен для демонстрационного плагина. Это будет суффикс."
+
+#: plugins/demo_plugin/demo_plugin.php:480
+msgid "Other demo parameter"
+msgstr "Другой демонстрационный параметр"
+
+#: plugins/isso/isso.php:22
+msgid ""
+"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
+"administration page."
+msgstr ""
+"Ошибка плагина Isso: определите параметр \"ISSO_SERVER\" на странице "
+"настройки плагина."
+
+#: plugins/isso/isso.php:92
+msgid "Let visitor comment your shaares on permalinks with Isso."
+msgstr ""
+"Позволить посетителю комментировать ваши закладки по постоянным ссылкам с "
+"Isso."
+
+#: plugins/isso/isso.php:93
+msgid "Isso server URL (without 'http://')"
+msgstr "URL сервера Isso (без 'http: //')"
+
+#: plugins/piwik/piwik.php:24
+msgid ""
+"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
+"administration page."
+msgstr ""
+"Ошибка плагина Piwik: укажите PIWIK_URL и PIWIK_SITEID на странице настройки "
+"плагина."
+
+#: plugins/piwik/piwik.php:73
+msgid "A plugin that adds Piwik tracking code to Shaarli pages."
+msgstr "Плагин, который добавляет код отслеживания Piwik на страницы Shaarli."
+
+#: plugins/piwik/piwik.php:74
+msgid "Piwik URL"
+msgstr "Piwik URL"
+
+#: plugins/piwik/piwik.php:75
+msgid "Piwik site ID"
+msgstr "Piwik site ID"
+
+#: plugins/playvideos/playvideos.php:26
+msgid "Video player"
+msgstr "Видео плеер"
+
+#: plugins/playvideos/playvideos.php:29
+msgid "Play Videos"
+msgstr "Воспроизвести видео"
+
+#: plugins/playvideos/playvideos.php:60
+msgid "Add a button in the toolbar allowing to watch all videos."
+msgstr ""
+"Добавьте кнопку на панель инструментов, позволяющую смотреть все видео."
+
+#: plugins/playvideos/youtube_playlist.js:214
+msgid "plugins/playvideos/jquery-1.11.2.min.js"
+msgstr "plugins/playvideos/jquery-1.11.2.min.js"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:72
+#, php-format
+msgid "Could not publish to PubSubHubbub: %s"
+msgstr "Не удалось опубликовать в PubSubHubbub: %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:99
+#, php-format
+msgid "Could not post to %s"
+msgstr "Не удалось отправить сообщение в %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:103
+#, php-format
+msgid "Bad response from the hub %s"
+msgstr "Плохой ответ от хаба %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:114
+msgid "Enable PubSubHubbub feed publishing."
+msgstr "Включить публикацию канала PubSubHubbub."
+
+#: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72
+msgid "For each link, add a QRCode icon."
+msgstr "Для каждой ссылки добавить значок QR кода."
+
+#: plugins/wallabag/wallabag.php:22
+msgid ""
+"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
+"plugin administration page."
+msgstr ""
+"Ошибка плагина Wallabag: определите параметр \"WALLABAG_URL\" на странице "
+"настройки плагина."
+
+#: plugins/wallabag/wallabag.php:49
+msgid "Save to wallabag"
+msgstr "Сохранить в wallabag"
+
+#: plugins/wallabag/wallabag.php:73
+msgid "Wallabag API URL"
+msgstr "Wallabag API URL"
+
+#: plugins/wallabag/wallabag.php:74
+msgid "Wallabag API version (1 or 2)"
+msgstr "Wallabag версия API (1 или 2)"
+
+#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
+msgid "Sorry, nothing to see here."
+msgstr "Извините, тут ничего нет."
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "URL or leave empty to post a note"
+msgstr "URL или оставьте пустым, чтобы опубликовать заметку"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "BULK CREATION"
+msgstr "МАССОВОЕ СОЗДАНИЕ"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+msgid "Metadata asynchronous retrieval is disabled."
+msgstr "Асинхронное получение метаданных отключено."
+
+#: 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 ""
+"Мы рекомендуем включить параметр general > enable_async_metadata в "
+"вашем файле конфигурации, чтобы использовать массовое создание ссылок."
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
+msgid "Shaare multiple new links"
+msgstr "Поделиться несколькими новыми ссылками"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
+msgid "Add one URL per line to create multiple bookmarks."
+msgstr "Добавьте по одному URL в строке, чтобы создать несколько закладок."
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
+msgid "Tags"
+msgstr "Теги"
+
+#: 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 "Личный"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
+msgid "Add links"
+msgstr "Добавить ссылки"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Current password"
+msgstr "Текущий пароль"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "New password"
+msgstr "Новый пароль"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Change"
+msgstr "Изменить"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
+msgid "Tag"
+msgstr "Тег"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "New name"
+msgstr "Новое имя"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
+msgid "Case sensitive"
+msgstr "С учетом регистра"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
+msgid "Rename tag"
+msgstr "Переименовать тег"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+msgid "Delete tag"
+msgstr "Удалить тег"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+msgid "You can also edit tags in the"
+msgstr "Вы также можете редактировать теги в"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+msgid "tag list"
+msgstr "список тегов"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+msgid "Change tags separator"
+msgstr "Изменить разделитель тегов"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
+msgid "Your current tag separator is"
+msgstr "Текущий разделитель тегов"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
+msgid "New separator"
+msgstr "Новый разделитель"
+
+#: 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 "Сохранить"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
+msgid "Note that hashtags won't fully work with a non-whitespace separator."
+msgstr ""
+"Обратите внимание, что хэштеги не будут полностью работать с разделителем, "
+"отличным от пробелов."
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "title"
+msgstr "заголовок"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+msgid "Home link"
+msgstr "Домашняя ссылка"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+msgid "Default value"
+msgstr "Значение по умолчанию"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "Theme"
+msgstr "Тема"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
+msgid "Description formatter"
+msgstr "Средство форматирования описания"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
+msgid "Language"
+msgstr "Язык"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
+msgid "Timezone"
+msgstr "Часовой пояс"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+msgid "Continent"
+msgstr "Континент"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+msgid "City"
+msgstr "Город"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191
+msgid "Disable session cookie hijacking protection"
+msgstr "Отключить защиту от перехвата файлов сеанса cookie"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:193
+msgid "Check this if you get disconnected or if your IP address changes often"
+msgstr "Проверьте это, если вы отключаетесь или ваш IP адрес часто меняется"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210
+msgid "Private links by default"
+msgstr "Приватные ссылки по умолчанию"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211
+msgid "All new links are private by default"
+msgstr "Все новые ссылки по умолчанию являются приватными"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226
+msgid "RSS direct links"
+msgstr "RSS прямые ссылки"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:227
+msgid "Check this to use direct URL instead of permalink in feeds"
+msgstr ""
+"Установите этот флажок, чтобы использовать прямой URL вместо постоянной "
+"ссылки в фидах"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242
+msgid "Hide public links"
+msgstr "Скрыть общедоступные ссылки"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:243
+msgid "Do not show any links if the user is not logged in"
+msgstr "Не показывать ссылки, если пользователь не авторизован"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:258
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:149
+msgid "Check updates"
+msgstr "Проверить обновления"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:259
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
+msgid "Notify me when a new release is ready"
+msgstr "Оповестить, когда будет готов новый выпуск"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
+msgid "Automatically retrieve description for new bookmarks"
+msgstr "Автоматически получать описание для новых закладок"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275
+msgid "Shaarli will try to retrieve the description from meta HTML headers"
+msgstr "Shaarli попытается получить описание из мета заголовков HTML"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
+msgid "Enable REST API"
+msgstr "Включить REST API"
+
+#: 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 ""
+"Разрешить стороннему программному обеспечению использовать Shaarli, например "
+"мобильное приложение"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306
+msgid "API secret"
+msgstr "API ключ"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
+msgid "Enable thumbnails"
+msgstr "Включить миниатюры"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324
+msgid "You need to enable the extension php-gd
to use thumbnails."
+msgstr ""
+"Вам необходимо включить расширение php-gd
для использования "
+"миниатюр."
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
+msgid "Synchronize thumbnails"
+msgstr "Синхронизировать миниатюры"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+msgid "All"
+msgstr "Все"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
+msgid "Only common media hosts"
+msgstr "Только обычные медиа хосты"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
+msgid "None"
+msgstr "Ничего"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+msgid "1 RSS entry per :type"
+msgid_plural ""
+msgstr[0] "1 RSS запись для каждого :type"
+msgstr[1] "1 RSS запись для каждого :type"
+msgstr[2] "1 RSS запись для каждого :type"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
+msgid "Previous :type"
+msgid_plural ""
+msgstr[0] "Предыдущий :type"
+msgstr[1] "Предыдущих :type"
+msgstr[2] "Предыдущих :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] "Все ссылки одного :type на одной странице."
+msgstr[1] "Все ссылки одного :type на одной странице."
+msgstr[2] "Все ссылки одного :type на одной странице."
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+msgid "Next :type"
+msgid_plural ""
+msgstr[0] "Следующий :type"
+msgstr[1] "Следующие :type"
+msgstr[2] "Следующие :type"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+msgid "Edit Shaare"
+msgstr "Изменить закладку"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+msgid "New Shaare"
+msgstr "Новая закладка"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
+msgid "Created:"
+msgstr "Создано:"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+msgid "URL"
+msgstr "URL"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+msgid "Title"
+msgstr "Заголовок"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
+msgid "Description"
+msgstr "Описание"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:89
+msgid "Description will be rendered with"
+msgstr "Описание будет отображаться с"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
+msgid "Markdown syntax documentation"
+msgstr "Документация по синтаксису Markdown"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
+msgid "Markdown syntax"
+msgstr "Синтаксис Markdown"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:115
+msgid "Cancel"
+msgstr "Отменить"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
+msgid "Apply Changes"
+msgstr "Применить изменения"
+
+#: 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 "Удалить"
+
+#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
+#: tmp/editlink.batch.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
+msgid "Save all"
+msgstr "Сохранить все"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Export Database"
+msgstr "Экспорт базы данных"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Selection"
+msgstr "Выбор"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+msgid "Public"
+msgstr "Общедоступно"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
+msgid "Prepend note permalinks with this Shaarli instance's URL"
+msgstr ""
+"Добавить постоянные ссылки на заметку с URL адресом этого экземпляра Shaarli"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
+msgid "Useful to import bookmarks in a web browser"
+msgstr "Useful to import bookmarks in a web browser"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Import Database"
+msgstr "Импорт базы данных"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Maximum size allowed:"
+msgstr "Максимально допустимый размер:"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Visibility"
+msgstr "Видимость"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Use values from the imported file, default to public"
+msgstr ""
+"Использовать значения из импортированного файла, по умолчанию общедоступные"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+msgid "Import all bookmarks as private"
+msgstr "Импортировать все закладки как личные"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+msgid "Import all bookmarks as public"
+msgstr "Импортировать все закладки как общедоступные"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
+msgid "Overwrite existing bookmarks"
+msgstr "Заменить существующие закладки"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "Duplicates based on URL"
+msgstr "Дубликаты на основе URL"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
+msgid "Add default tags"
+msgstr "Добавить теги по умолчанию"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
+msgid "It looks like it's the first time you run Shaarli. Please configure it."
+msgstr "Похоже, вы впервые запускаете Shaarli. Пожалуйста, настройте его."
+
+#: 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 "Имя пользователя"
+
+#: 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 "Пароль"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:62
+msgid "Shaarli title"
+msgstr "Заголовок Shaarli"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
+msgid "My links"
+msgstr "Мои ссылки"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
+msgid "Install"
+msgstr "Установка"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:190
+msgid "Server requirements"
+msgstr "Системные требования"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
+msgid "shaare"
+msgid_plural "shaares"
+msgstr[0] "закладка"
+msgstr[1] "закладки"
+msgstr[2] "закладок"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+msgid "private link"
+msgid_plural "private links"
+msgstr[0] "личная ссылка"
+msgstr[1] "личные ссылки"
+msgstr[2] "личных ссылок"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123
+msgid "Search text"
+msgstr "Поиск текста"
+
+#: 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:65
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+msgid "Filter by tag"
+msgstr "Фильтровать по тегу"
+
+#: 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"
+msgstr "Поиск"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
+msgid "Nothing found."
+msgstr "Ничего не найдено."
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118
+#, php-format
+msgid "%s result"
+msgid_plural "%s results"
+msgstr[0] "%s результат"
+msgstr[1] "%s результатов"
+msgstr[2] "%s результатов"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
+msgid "for"
+msgstr "для"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
+msgid "tagged"
+msgstr "отмечено"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
+msgid "Remove tag"
+msgstr "Удалить тег"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+msgid "with status"
+msgstr "со статусом"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
+msgid "without any tag"
+msgstr "без тега"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41
+msgid "Fold"
+msgstr "Сложить"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
+msgid "Edited: "
+msgstr "Отредактировано: "
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
+msgid "permalink"
+msgstr "постоянная ссылка"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
+msgid "Add tag"
+msgstr "Добавить тег"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185
+msgid "Toggle sticky"
+msgstr "Закрепить / Открепить"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187
+msgid "Sticky"
+msgstr "Закреплено"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
+msgid "Share a private link"
+msgstr "Поделиться личной ссылкой"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5
+msgid "Filters"
+msgstr "Фильтры"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:10
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:10
+msgid "Only display private links"
+msgstr "Отображать только личные ссылки"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:13
+msgid "Only display public links"
+msgstr "Отображать только общедоступные ссылки"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18
+msgid "Filter untagged links"
+msgstr "Фильтровать неотмеченные ссылки"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24
+msgid "Select all"
+msgstr "Выбрать все"
+
+#: 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 "Сложить все"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76
+msgid "Links per page"
+msgstr "Ссылок на страницу"
+
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171
+msgid "Remember me"
+msgstr "Запомнить меня"
+
+#: 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 "by the Shaarli community"
+msgstr "сообществом Shaarli"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:16
+msgid "Documentation"
+msgstr "Документация"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
+msgid "Expand"
+msgstr "Развернуть"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
+msgid "Expand all"
+msgstr "Развернуть все"
+
+#: 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 "Вы уверены, что хотите удалить эту ссылку?"
+
+#: 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 "Вы уверены, что хотите удалить этот тег?"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:11
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11
+msgid "Menu"
+msgstr "Меню"
+
+#: 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 "Облако тегов"
+
+#: 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 канал"
+
+#: 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 "Выйти"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152
+msgid "Set public"
+msgstr "Сделать общедоступным"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:157
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157
+msgid "Set private"
+msgstr "Сделать личным"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189
+msgid "is available"
+msgstr "доступно"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:196
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196
+msgid "Error"
+msgstr "Ошибка"
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "There is no cached thumbnail."
+msgstr "Нет кэшированных миниатюр."
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
+msgid "Try to synchronize them."
+msgstr "Попробуйте синхронизировать их."
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Picture Wall"
+msgstr "Галерея"
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "pics"
+msgstr "изображений"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "You need to enable Javascript to change plugin loading order."
+msgstr ""
+"Вам необходимо включить Javascript, чтобы изменить порядок загрузки плагинов."
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+msgid "Plugin administration"
+msgstr "Управление плагинами"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Enabled Plugins"
+msgstr "Включенные плагины"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
+msgid "No plugin enabled."
+msgstr "Нет включенных плагинов."
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
+msgid "Disable"
+msgstr "Отключить"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
+msgid "Name"
+msgstr "Имя"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
+msgid "Order"
+msgstr "Порядок"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
+msgid "Disabled Plugins"
+msgstr "Отключенные плагины"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
+msgid "No plugin disabled."
+msgstr "Нет отключенных плагинов."
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
+msgid "Enable"
+msgstr "Включить"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
+msgid "More plugins available"
+msgstr "Доступны другие плагины"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
+msgid "in the documentation"
+msgstr "в документации"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
+msgid "Plugin configuration"
+msgstr "Настройка плагинов"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195
+msgid "No parameter available."
+msgstr "Нет доступных параметров."
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "General"
+msgstr "Общее"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
+msgid "Index URL"
+msgstr "Индексный URL"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Base path"
+msgstr "Базовый путь"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Client IP"
+msgstr "IP клиента"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+msgid "Trusted reverse proxies"
+msgstr "Надежные обратные прокси"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "N/A"
+msgstr "Нет данных"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
+msgid "Visit releases page on Github"
+msgstr "Посетить страницу релизов на Github"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
+msgid "Synchronize all link thumbnails"
+msgstr "Синхронизировать все миниатюры ссылок"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:2
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2
+msgid "Permissions"
+msgstr "Разрешения"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:8
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8
+msgid "There are permissions that need to be fixed."
+msgstr "Есть разрешения, которые нужно исправить."
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23
+msgid "All read/write permissions are properly set."
+msgstr "Все разрешения на чтение и запись установлены правильно."
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:32
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32
+msgid "Running PHP"
+msgstr "Запуск PHP"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36
+msgid "End of life: "
+msgstr "Конец жизни: "
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48
+msgid "Extension"
+msgstr "Расширение"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49
+msgid "Usage"
+msgstr "Применение"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50
+msgid "Status"
+msgstr "Статус"
+
+#: 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 "Загружено"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
+msgid "Required"
+msgstr "Обязательно"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:60
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
+msgid "Optional"
+msgstr "Необязательно"
+
+#: tmp/server.requirements.b91ef64efc3688266305ea9b42e5017e.rtpl.php:70
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70
+msgid "Not loaded"
+msgstr "Не загружено"
+
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "tags"
+msgstr "теги"
+
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "List all links with those tags"
+msgstr "Список всех ссылок с этими тегами"
+
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "Tag list"
+msgstr "Список тегов"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
+msgid "Sort by:"
+msgstr "Сортировать по:"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5
+msgid "Cloud"
+msgstr "Облако"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6
+msgid "Most used"
+msgstr "Наиболее используемое"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7
+msgid "Alphabetical"
+msgstr "Алфавит"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Settings"
+msgstr "Настройки"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Change Shaarli settings: title, timezone, etc."
+msgstr "Измените настройки Shaarli: заголовок, часовой пояс и т.д."
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
+msgid "Configure your Shaarli"
+msgstr "Настройка Shaarli"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
+msgid "Enable, disable and configure plugins"
+msgstr "Включить, отключить и настроить плагины"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27
+msgid "Check instance's server configuration"
+msgstr "Проверка конфигурации экземпляра сервера"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+msgid "Change your password"
+msgstr "Изменить пароль"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+msgid "Rename or delete a tag in all links"
+msgstr "Переименовать или удалить тег во всех ссылках"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+msgid ""
+"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
+"delicious...)"
+msgstr ""
+"Импорт закладок Netscape HTML (экспортированные из Firefox, Chrome, Opera, "
+"delicious...)"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+msgid "Import links"
+msgstr "Импорт ссылок"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
+msgid ""
+"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
+"Opera, delicious...)"
+msgstr ""
+"Экспорт закладок Netscape HTML (которые могут быть импортированы в Firefox, "
+"Chrome, Opera, delicious...)"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54
+msgid "Export database"
+msgstr "Экспорт базы данных"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
+msgid ""
+"Drag one of these button to your bookmarks toolbar or right-click it and "
+"\"Bookmark This Link\""
+msgstr ""
+"Перетащите одну из этих кнопок на панель закладок или щелкните по ней правой "
+"кнопкой мыши и выберите \"Добавить ссылку в закладки\""
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
+msgid "then click on the bookmarklet in any page you want to share."
+msgstr ""
+"затем щелкните букмарклет на любой странице, которой хотите поделиться."
+
+#: 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"
+msgstr ""
+"Перетащите эту ссылку на панель закладок или щелкните по ней правой кнопкой "
+"мыши и добавьте эту ссылку в закладки"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+msgid "then click ✚Shaare link button in any page you want to share"
+msgstr ""
+"затем нажмите кнопку ✚Поделиться ссылкой на любой странице, которой хотите "
+"поделиться"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
+msgid "The selected text is too long, it will be truncated."
+msgstr "Выделенный текст слишком длинный, он будет обрезан."
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+msgid "Shaare link"
+msgstr "Поделиться ссылкой"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
+msgid ""
+"Then click ✚Add Note button anytime to start composing a private Note (text "
+"post) to your Shaarli"
+msgstr ""
+"Затем в любое время нажмите кнопку ✚Добавить заметку, чтобы начать создавать "
+"личную заметку (текстовое сообщение) в своем Shaarli"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
+msgid "Add Note"
+msgstr "Добавить заметку"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132
+msgid "3rd party"
+msgstr "Третья сторона"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140
+msgid "plugin"
+msgstr "плагин"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
+msgid ""
+"Drag this link to your bookmarks toolbar, or right-click it and choose "
+"Bookmark This Link"
+msgstr ""
+"Перетащите эту ссылку на панель закладок или щелкните по ней правой кнопкой "
+"мыши и выберите \"Добавить ссылку в закладки\""
diff --git a/inc/languages/zh_CN/LC_MESSAGES/shaarli.po b/inc/languages/zh_CN/LC_MESSAGES/shaarli.po
new file mode 100644
index 00000000..07c300c0
--- /dev/null
+++ b/inc/languages/zh_CN/LC_MESSAGES/shaarli.po
@@ -0,0 +1,1807 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: Shaarli\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2021-08-10 05:21+0800\n"
+"PO-Revision-Date: 2021-08-10 07:17+0800\n"
+"Last-Translator: YFdyh000 \n"
+"Language-Team: Shaarli\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 3.0\n"
+"X-Poedit-Basepath: ../../../..\n"
+"Plural-Forms: nplurals=1; plural=0;\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Poedit-KeywordsList: t:1,2;t\n"
+"X-Poedit-SearchPath-0: .\n"
+"X-Poedit-SearchPathExcluded-0: node_modules\n"
+"X-Poedit-SearchPathExcluded-1: vendor\n"
+"X-Poedit-SearchPathExcluded-2: tests/legacy/LegacyControllerTest.php\n"
+"X-Poedit-SearchPathExcluded-3: tests/languages\n"
+"X-Poedit-SearchPathExcluded-4: tests/LanguagesTest.php\n"
+
+#: application/History.php:181
+msgid "History file isn't readable or writable"
+msgstr "历史记录文件无法读或写"
+
+#: application/History.php:192
+msgid "Could not parse history file"
+msgstr "无法解析历史记录文件"
+
+#: application/Languages.php:184
+msgid "Automatic"
+msgstr "自动"
+
+#: application/Languages.php:185
+msgid "German"
+msgstr "德语"
+
+#: application/Languages.php:186
+msgid "English"
+msgstr "英语"
+
+#: application/Languages.php:187
+msgid "French"
+msgstr "法语"
+
+#: application/Languages.php:188
+msgid "Japanese"
+msgstr "日语"
+
+#: application/Languages.php:189
+msgid "Russian"
+msgstr "俄语"
+
+#: application/Languages.php:190
+msgid "Chinese (Simplified)"
+msgstr "简体中文"
+
+#: application/Thumbnailer.php:62
+msgid ""
+"php-gd extension must be loaded to use thumbnails. Thumbnails are now "
+"disabled. Please reload the page."
+msgstr "php-gd 扩展未加载,因此缩略图已禁用。请刷新页面。"
+
+#: application/Utils.php:406 tests/UtilsTest.php:329
+msgid "Setting not set"
+msgstr "尚未设置"
+
+#: application/Utils.php:413 tests/UtilsTest.php:327 tests/UtilsTest.php:328
+msgid "Unlimited"
+msgstr "无限制"
+
+#: application/Utils.php:416 tests/UtilsTest.php:324 tests/UtilsTest.php:325
+#: tests/UtilsTest.php:339
+msgid "B"
+msgstr "B"
+
+#: application/Utils.php:416 tests/UtilsTest.php:318 tests/UtilsTest.php:319
+#: tests/UtilsTest.php:326
+msgid "kiB"
+msgstr "kiB"
+
+#: application/Utils.php:416 tests/UtilsTest.php:320 tests/UtilsTest.php:321
+#: tests/UtilsTest.php:337 tests/UtilsTest.php:338
+msgid "MiB"
+msgstr "MiB"
+
+#: application/Utils.php:416 tests/UtilsTest.php:322 tests/UtilsTest.php:323
+msgid "GiB"
+msgstr "GiB"
+
+#: application/bookmark/BookmarkFileService.php:203
+#: application/bookmark/BookmarkFileService.php:225
+#: application/bookmark/BookmarkFileService.php:247
+#: application/bookmark/BookmarkFileService.php:261
+msgid "You're not authorized to alter the datastore"
+msgstr "您无权更改数据存储"
+
+#: application/bookmark/BookmarkFileService.php:228
+msgid "This bookmarks already exists"
+msgstr "此书签已存在"
+
+#: application/bookmark/BookmarkInitializer.php:42
+msgid "(private bookmark with thumbnail demo)"
+msgstr "(演示有缩略图的私有书签)"
+
+#: 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 可自动经链接从各种网站上抓取缩略图。\n"
+"\n"
+"尝试各个菜单和选项,体验崭新 Shaarli 的功能吧。\n"
+"访问我们在 [Github](https://github.com/shaarli/Shaarli) 上的项目或[这里的文"
+"档](https://shaarli.readthedocs.io/en/master/)可进一步了解 Shaarli。\n"
+"\n"
+"您可以编辑或者删除默认预置的这些记录。\n"
+
+#: application/bookmark/BookmarkInitializer.php:58
+msgid "Note: Shaare descriptions"
+msgstr "便签功能介绍"
+
+#: 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 ""
+"在本软件中添加“记录”时不输入网址(URL)则会创建一个仅有文本内容的便签。\n"
+"这是一个私有标签,只有登录后的您能看到它。\n"
+"\n"
+"您可使用便签功能记下随想、网络文章、代码片段等内容。\n"
+"\n"
+"您可以在便签和书签描述中使用 Markdown 格式:\n"
+"\n"
+"### 标题文本\n"
+"\n"
+"#### 多级标题\n"
+" * 无序列表\n"
+" * _斜体_文本\n"
+" * **粗体**文本\n"
+" * ~~删除线~~文本\n"
+" * `代码`块\n"
+" * 图像\n"
+" * [链接](https://zh.wikipedia.org/wiki/Markdown)\n"
+"\n"
+"也支持 Markdown 表格语法:\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"
+
+#: application/bookmark/BookmarkInitializer.php:94
+#: application/legacy/LegacyLinkDB.php:246
+#: 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 "个人、超快、极简、掌握数据库的书签服务"
+
+#: 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 ""
+"欢迎使用 Shaarli 软件!\n"
+"\n"
+"Shaarli 可供您添加自己最爱的页面,并可与其他人分享或者为个人存储。\n"
+"您可以在添加书签时提供描述和标签。\n"
+"\n"
+"单击“+ Shaare”按钮或通过任意推荐的工具(浏览器扩展、移动应用、书签、REST API "
+"等)来创建新的 shaare 记录。\n"
+"\n"
+"您可使用内置的搜索引擎轻松检索链接,也可以按标签搜索(例如这条 Shaare 标"
+"有“shaarli”和“help”)。\n"
+"也支持哈希标记(hashtag)格式,如 #shaarli #help 这般。\n"
+"您还可以通过标签(tag)或纯文本搜索来过滤提供的 [RSS 订阅源](/feed/atom) 和图"
+"片墙。\n"
+"\n"
+"Shaarli 社区希望您享受到本软件的便利性和❤️!\n"
+"如有任何建议或遇到问题,随时开启一个[问题](https://github.com/shaarli/"
+"Shaarli/issues)(建议使用英语)。\n"
+
+#: application/bookmark/exception/BookmarkNotFoundException.php:14
+msgid "The link you are trying to reach does not exist or has been deleted."
+msgstr "您尝试访问的链接不存在或已被删除。"
+
+#: 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 无法创建配置文件。请确保 Shaarli 有权写入其所在的文件夹。"
+
+#: application/config/ConfigManager.php:137
+#: application/config/ConfigManager.php:164
+msgid "Invalid setting key parameter. String expected, got: "
+msgstr "无效的设置键值。预期字符串,但得到:"
+
+#: application/config/exception/MissingFieldConfigException.php:20
+#, php-format
+msgid "Configuration value is required for %s"
+msgstr "%s 需要配置值"
+
+#: application/config/exception/PluginConfigOrderException.php:15
+msgid "An error occurred while trying to save plugins loading order."
+msgstr "尝试保存插件加载顺序时出错。"
+
+#: application/config/exception/UnauthorizedConfigException.php:15
+msgid "You are not authorized to alter config."
+msgstr "您无权更改配置。"
+
+#: application/exceptions/IOException.php:23
+msgid "Error accessing"
+msgstr "访问出错"
+
+#: application/feed/FeedBuilder.php:174
+msgid "Direct link"
+msgstr "直接链接"
+
+#: application/feed/FeedBuilder.php:176
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
+msgid "Permalink"
+msgstr "永久链接"
+
+#: application/front/controller/admin/ConfigureController.php:56
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "Configure"
+msgstr "配置"
+
+#: application/front/controller/admin/ConfigureController.php:106
+#: application/legacy/LegacyUpdater.php:539
+msgid "You have enabled or changed thumbnails mode."
+msgstr "您刚刚启用或更改缩略图模式。"
+
+#: application/front/controller/admin/ConfigureController.php:108
+#: application/front/controller/admin/ServerController.php:81
+#: application/legacy/LegacyUpdater.php:540
+msgid "Please synchronize them."
+msgstr "请进行同步。"
+
+#: application/front/controller/admin/ConfigureController.php:119
+#: application/front/controller/visitor/InstallController.php:154
+msgid "Error while writing config file after configuration update."
+msgstr "更新配置后写入配置文件时出错。"
+
+#: application/front/controller/admin/ConfigureController.php:128
+msgid "Configuration was saved."
+msgstr "已保存配置。"
+
+#: application/front/controller/admin/ExportController.php:26
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:64
+msgid "Export"
+msgstr "导出"
+
+#: application/front/controller/admin/ExportController.php:42
+msgid "Please select an export mode."
+msgstr "请选择导出模式。"
+
+#: application/front/controller/admin/ImportController.php:41
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+msgid "Import"
+msgstr "导入"
+
+#: application/front/controller/admin/ImportController.php:55
+msgid "No import file provided."
+msgstr "没有提供要导入的文件。"
+
+#: 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 ""
+"您尝试上传的文件可能超过了网页服务器所能接受的 (%s) 大小。请上传小一些的。"
+
+#: application/front/controller/admin/ManageTagController.php:30
+msgid "whitespace"
+msgstr "空白"
+
+#: application/front/controller/admin/ManageTagController.php:35
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+msgid "Manage tags"
+msgstr "管理标签"
+
+#: application/front/controller/admin/ManageTagController.php:54
+msgid "Invalid tags provided."
+msgstr "提供的标签无效。"
+
+#: application/front/controller/admin/ManageTagController.php:81
+#, php-format
+msgid "The tag was removed from %d bookmark."
+msgid_plural "The tag was removed from %d bookmarks."
+msgstr[0] "%d 个书签中的该标签已移除。"
+
+#: application/front/controller/admin/ManageTagController.php:86
+#, php-format
+msgid "The tag was renamed in %d bookmark."
+msgid_plural "The tag was renamed in %d bookmarks."
+msgstr[0] "%d 个书签中的该标签已重命名。"
+
+#: application/front/controller/admin/ManageTagController.php:108
+msgid "Tags separator must be a single character."
+msgstr "标签分隔符只能是一个字符。"
+
+#: application/front/controller/admin/ManageTagController.php:114
+msgid "These characters are reserved and can't be used as tags separator: "
+msgstr "如下保留字符不能用作标签分隔符:"
+
+#: application/front/controller/admin/PasswordController.php:28
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+msgid "Change password"
+msgstr "更改密码"
+
+#: application/front/controller/admin/PasswordController.php:55
+msgid "You must provide the current and new password to change it."
+msgstr "您必须提供当前密码和新密码才能更改。"
+
+#: application/front/controller/admin/PasswordController.php:71
+msgid "The old password is not correct."
+msgstr "旧密码不正确。"
+
+#: application/front/controller/admin/PasswordController.php:97
+msgid "Your password has been changed"
+msgstr "您的密码已更改"
+
+#: application/front/controller/admin/PluginsController.php:45
+msgid "Plugin Administration"
+msgstr "插件管理"
+
+#: application/front/controller/admin/PluginsController.php:76
+msgid "Setting successfully saved."
+msgstr "设置保存成功。"
+
+#: application/front/controller/admin/PluginsController.php:79
+msgid "Error while saving plugin configuration: "
+msgstr "保存插件配置出错:"
+
+#: application/front/controller/admin/ServerController.php:35
+msgid "Check disabled"
+msgstr "已禁用检查"
+
+#: application/front/controller/admin/ServerController.php:62
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Server administration"
+msgstr "服务器管理"
+
+#: application/front/controller/admin/ServerController.php:79
+msgid "Thumbnails cache has been cleared."
+msgstr "缩略图缓存已清除。"
+
+#: application/front/controller/admin/ServerController.php:90
+msgid "Shaarli's cache folder has been cleared!"
+msgstr "Shaarli 的缓存文件夹已清除!"
+
+#: application/front/controller/admin/ShaareAddController.php:26
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+msgid "Shaare a new link"
+msgstr "记录一个新链接"
+
+#: application/front/controller/admin/ShaareManageController.php:35
+#: application/front/controller/admin/ShaareManageController.php:97
+msgid "Invalid bookmark ID provided."
+msgstr "提供的书签 ID 无效。"
+
+#: application/front/controller/admin/ShaareManageController.php:47
+#: application/front/controller/admin/ShaareManageController.php:120
+#: application/front/controller/admin/ShaareManageController.php:160
+#: application/front/controller/admin/ShaarePublishController.php:82
+#, php-format
+msgid "Bookmark with identifier %s could not be found."
+msgstr "没有找到有 %s 标识符的书签。"
+
+#: application/front/controller/admin/ShaareManageController.php:105
+msgid "Invalid visibility provided."
+msgstr "提供的可见性无效。"
+
+#: application/front/controller/admin/ShaarePublishController.php:173
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:171
+msgid "Edit"
+msgstr "编辑"
+
+#: application/front/controller/admin/ShaarePublishController.php:176
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:28
+msgid "Shaare"
+msgstr "Shaare"
+
+#: application/front/controller/admin/ShaarePublishController.php:208
+msgid "Note: "
+msgstr "便签:"
+
+#: application/front/controller/admin/ThumbnailsController.php:37
+msgid "Thumbnails update"
+msgstr "缩略图更新"
+
+#: application/front/controller/admin/ToolsController.php:31
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:33
+msgid "Tools"
+msgstr "工具"
+
+#: application/front/controller/visitor/BookmarkListController.php:103
+msgid "Search: "
+msgstr "搜索:"
+
+#: application/front/controller/visitor/DailyController.php:201
+msgid "day"
+msgstr "天"
+
+#: application/front/controller/visitor/DailyController.php:201
+#: application/front/controller/visitor/DailyController.php:204
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:48
+msgid "Daily"
+msgstr "每天"
+
+#: application/front/controller/visitor/DailyController.php:202
+msgid "week"
+msgstr "周"
+
+#: application/front/controller/visitor/DailyController.php:202
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Weekly"
+msgstr "每周"
+
+#: application/front/controller/visitor/DailyController.php:203
+msgid "month"
+msgstr "月"
+
+#: application/front/controller/visitor/DailyController.php:203
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "Monthly"
+msgstr "每月"
+
+#: application/front/controller/visitor/ErrorController.php:30
+msgid "Error: "
+msgstr "错误: "
+
+#: application/front/controller/visitor/ErrorController.php:34
+msgid "Please report it on Github."
+msgstr "请赴 Github 报告该问题。"
+
+#: application/front/controller/visitor/ErrorController.php:39
+msgid "An unexpected error occurred."
+msgstr "发生意外错误。"
+
+#: application/front/controller/visitor/ErrorNotFoundController.php:25
+msgid "Requested page could not be found."
+msgstr "请求的页面未找到。"
+
+#: application/front/controller/visitor/InstallController.php:70
+msgid "Install Shaarli"
+msgstr "安装 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 ""
+"您的服务器上会话似乎不能正常工作。
请确保您的 PHP 配置中变量 "
+"\"session.save_path\" 设置正确并且您有权写入。
目前指向 %s。
在某些浏览"
+"器上,通过主机名(如\"localhost\")或任何没有点的自定义主机名访问服务器会导"
+"致 Cookie 存储失败。我们建议通过您的 IP 地址或标准的域名访问您的服务器。"
+"
"
+
+#: application/front/controller/visitor/InstallController.php:162
+msgid ""
+"Shaarli is now configured. Please login and start shaaring your bookmarks!"
+msgstr "Shaarli 已配置就绪。登录并开始记录你的书签!"
+
+#: application/front/controller/visitor/InstallController.php:176
+msgid "Insufficient permissions:"
+msgstr "权限不足:"
+
+#: application/front/controller/visitor/LoginController.php:46
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:77
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:101
+msgid "Login"
+msgstr "登录"
+
+#: application/front/controller/visitor/LoginController.php:78
+msgid "Wrong login/password."
+msgstr "登录名/密码错误。"
+
+#: application/front/controller/visitor/PictureWallController.php:29
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:43
+msgid "Picture wall"
+msgstr "图片墙"
+
+#: application/front/controller/visitor/TagCloudController.php:90
+msgid "Tag "
+msgstr "标签 "
+
+#: application/front/exceptions/AlreadyInstalledException.php:11
+msgid "Shaarli has already been installed. Login to edit the configuration."
+msgstr "Shaarli 已安装完毕。登录以编辑配置。"
+
+#: application/front/exceptions/LoginBannedException.php:11
+msgid ""
+"You have been banned after too many failed login attempts. Try again later."
+msgstr "您已因多次登录尝试失败而被禁止。稍后再试。"
+
+#: application/front/exceptions/OpenShaarliPasswordException.php:16
+msgid "You are not supposed to change a password on an Open Shaarli."
+msgstr "不能在公开的 Shaarli 上更改密码。"
+
+#: application/front/exceptions/ThumbnailsDisabledException.php:11
+msgid "Picture wall unavailable (thumbnails are disabled)."
+msgstr "图片墙不可用(缩略图已禁用)。"
+
+#: application/front/exceptions/WrongTokenException.php:16
+msgid "Wrong token."
+msgstr "令牌有误。"
+
+#: application/helper/ApplicationUtils.php:165
+#, php-format
+msgid ""
+"Your PHP version is obsolete! Shaarli requires at least PHP %s, and thus "
+"cannot run. Your PHP version has known security vulnerabilities and should "
+"be updated as soon as possible."
+msgstr ""
+"您的 PHP 版本已过时!Shaarli 需要至少 PHP %s,因此无法运行。您的 PHP 版本存在"
+"已知安全漏洞,应尽快更新。"
+
+#: application/helper/ApplicationUtils.php:200
+#: application/helper/ApplicationUtils.php:220
+msgid "directory is not readable"
+msgstr "目录不可读"
+
+#: application/helper/ApplicationUtils.php:223
+msgid "directory is not writable"
+msgstr "目录不可写"
+
+#: application/helper/ApplicationUtils.php:247
+msgid "file is not readable"
+msgstr "文件不可读"
+
+#: application/helper/ApplicationUtils.php:250
+msgid "file is not writable"
+msgstr "文件不可写"
+
+#: application/helper/ApplicationUtils.php:265
+msgid ""
+"Lock can not be acquired on the datastore. You might encounter concurrent "
+"access issues."
+msgstr "无法获得数据库锁。您可能会遇到并发访问问题。"
+
+#: application/helper/ApplicationUtils.php:298
+msgid "Configuration parsing"
+msgstr "配置解析"
+
+#: application/helper/ApplicationUtils.php:299
+msgid "Slim Framework (routing, etc.)"
+msgstr "Slim 框架(网页路由等)"
+
+#: application/helper/ApplicationUtils.php:300
+msgid "Multibyte (Unicode) string support"
+msgstr "多字节(Unicode)字符串支持"
+
+#: application/helper/ApplicationUtils.php:301
+msgid "Required to use thumbnails"
+msgstr "缩略图功能所需"
+
+#: application/helper/ApplicationUtils.php:302
+msgid "Localized text sorting (e.g. e->è->f)"
+msgstr "本地化的文本排序(例如 e->è->f)"
+
+#: application/helper/ApplicationUtils.php:303
+msgid "Better retrieval of bookmark metadata and thumbnail"
+msgstr "更佳的书签元数据和缩略图检索"
+
+#: application/helper/ApplicationUtils.php:304
+msgid "Use the translation system in gettext mode"
+msgstr "gettext 模式的界面翻译系统"
+
+#: application/helper/ApplicationUtils.php:305
+msgid "Login using LDAP server"
+msgstr "通过 LDAP 服务器登录"
+
+#: application/helper/DailyPageHelper.php:179
+msgid "Week"
+msgstr "周"
+
+#: application/helper/DailyPageHelper.php:183
+msgid "Today"
+msgstr "今天"
+
+#: application/helper/DailyPageHelper.php:185
+msgid "Yesterday"
+msgstr "昨天"
+
+#: application/helper/FileUtils.php:100
+msgid "Provided path is not a directory."
+msgstr "提供的路径不是一个目录。"
+
+#: application/helper/FileUtils.php:104
+msgid "Trying to delete a folder outside of Shaarli path."
+msgstr "尝试删除 Shaarli 路径外的文件夹。"
+
+#: application/legacy/LegacyLinkDB.php:131
+msgid "You are not authorized to add a link."
+msgstr "您未获权添加一个链接。"
+
+#: application/legacy/LegacyLinkDB.php:134
+msgid "Internal Error: A link should always have an id and URL."
+msgstr "内部错误:一个链接应始终有 ID 和 URL。"
+
+#: application/legacy/LegacyLinkDB.php:137
+msgid "You must specify an integer as a key."
+msgstr "您必须指定一个整数作为键。"
+
+#: application/legacy/LegacyLinkDB.php:140
+msgid "Array offset and link ID must be equal."
+msgstr "数组偏移和链接 ID 必须相等。"
+
+#: 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"
+"\n"
+"To learn how to use Shaarli, consult the link \"Documentation\" at the "
+"bottom of this page.\n"
+"\n"
+"You use the community supported version of the original Shaarli project, by "
+"Sebastien Sauvage."
+msgstr ""
+"欢迎使用 Shaarli!这是你的第一个公开书签。如要编辑或删除它,您必须先登录。\n"
+"\n"
+"要了解如何使用 Shaarli,请查阅本页底部的“文档”链接。\n"
+"\n"
+"您使用社区支持的原始沙利项目的版本,由塞巴斯蒂安·索瓦奇。"
+
+#: application/legacy/LegacyLinkDB.php:266
+msgid "My secret stuff... - Pastebin.com"
+msgstr "我的私密记录 - Pastebin.com"
+
+#: application/legacy/LegacyLinkDB.php:268
+msgid "Shhhh! I'm a private link only YOU can see. You can delete me too."
+msgstr "嘘!我是一个私有链接,只有你可以看到。您也可以删除我。"
+
+#: application/legacy/LegacyUpdater.php:104
+msgid "Couldn't retrieve updater class methods."
+msgstr "无法检索更新器类的方法。"
+
+#: application/legacy/LegacyUpdater.php:540
+msgid ""
+msgstr ""
+
+#: application/netscape/NetscapeBookmarkUtils.php:63
+msgid "Invalid export selection:"
+msgstr "无效的导出选择:"
+
+#: application/netscape/NetscapeBookmarkUtils.php:215
+#, php-format
+msgid "File %s (%d bytes) "
+msgstr "文件 %s(%d 字节)"
+
+#: application/netscape/NetscapeBookmarkUtils.php:217
+msgid "has an unknown file format. Nothing was imported."
+msgstr "是未知的文件格式。没有导入任何东西。"
+
+#: application/netscape/NetscapeBookmarkUtils.php:221
+#, php-format
+msgid ""
+"was successfully processed in %d seconds: %d bookmarks imported, %d "
+"bookmarks overwritten, %d bookmarks skipped."
+msgstr ""
+"已成功,花费 %d 秒:导入 %d 个书签,覆盖了 %d 个书签,跳过了 %d 个书签。"
+
+#: application/plugin/PluginManager.php:103
+#: application/plugin/PluginManager.php:141
+msgid " [plugin incompatibility]: "
+msgstr " [插件不兼容]:"
+
+#: application/plugin/exception/PluginFileNotFoundException.php:22
+#, php-format
+msgid "Plugin \"%s\" files not found."
+msgstr "未找到插件文件“%s”。"
+
+#: application/render/PageCacheManager.php:33
+#, php-format
+msgid "Cannot purge %s: no directory"
+msgstr "无法清除 %s:无目录"
+
+#: application/updater/exception/UpdaterException.php:51
+msgid "An error occurred while running the update "
+msgstr "更新它时发生错误:"
+
+#: index.php:82
+msgid "Shared bookmarks on "
+msgstr "已共享书签于"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:31
+msgid "URI"
+msgstr "URI"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:35
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
+msgid "Add link"
+msgstr "添加链接"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:52
+msgid "Adds the addlink input on the linklist page."
+msgstr "在链接列表页面上提供一个“添加链接”输入框。"
+
+#: plugins/archiveorg/archiveorg.php:29
+msgid "View on archive.org"
+msgstr "在 archive.org 上查看"
+
+#: plugins/archiveorg/archiveorg.php:42
+msgid "For each link, add an Archive.org icon."
+msgstr "为每个链接添加一个 Archive.org 图标。"
+
+#: plugins/default_colors/default_colors.php:38
+msgid ""
+"Default colors plugin error: This plugin is active and no custom color is "
+"configured."
+msgstr "默认颜色插件错误:此插件处于活动状态,并且没有配置自定义颜色。"
+
+#: plugins/default_colors/default_colors.php:127
+msgid "Override default theme colors. Use any CSS valid color."
+msgstr "覆盖默认主题配色。使用任何有效的 CSS 颜色。"
+
+#: plugins/default_colors/default_colors.php:128
+msgid "Main color (navbar green)"
+msgstr "主色调(导航栏绿)"
+
+#: plugins/default_colors/default_colors.php:129
+msgid "Background color (light grey)"
+msgstr "背景颜色(浅灰)"
+
+#: plugins/default_colors/default_colors.php:130
+msgid "Dark main color (e.g. visited links)"
+msgstr "深主色(例如已访问的链接)"
+
+#: plugins/demo_plugin/demo_plugin.php:528
+msgid ""
+"A demo plugin covering all use cases for template designers and plugin "
+"developers."
+msgstr "Demo 演示插件,供模板设计人员和插件开发人员,涵盖所有用法。"
+
+#: plugins/demo_plugin/demo_plugin.php:529
+msgid "This is a parameter dedicated to the demo plugin. It'll be suffixed."
+msgstr "这是专用于演示插件的参数。它将被后缀。"
+
+#: plugins/demo_plugin/demo_plugin.php:530
+msgid "Other demo parameter"
+msgstr "其他演示参数"
+
+#: plugins/isso/isso.php:22
+msgid ""
+"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
+"administration page."
+msgstr "Isso 插件错误:请在插件管理页面中定义“ISSO_SERVER”设置。"
+
+#: plugins/isso/isso.php:92
+msgid "Let visitor comment your shaares on permalinks with Isso."
+msgstr "让访客通过 Isso 基于您的记录的永久链接留下评论。"
+
+#: plugins/isso/isso.php:93
+msgid "Isso server URL (without 'http://')"
+msgstr "Isso 服务器网址(无“http://”)"
+
+#: plugins/piwik/piwik.php:24
+msgid ""
+"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
+"administration page."
+msgstr "Piwik 插件错误:请在插件管理页面中定义 PIWIK_URL 和 PIWIK_SITEID。"
+
+#: plugins/piwik/piwik.php:73
+msgid "A plugin that adds Piwik tracking code to Shaarli pages."
+msgstr "将 Piwik 追踪代码添加到 Shaarli 各个页面的插件。"
+
+#: plugins/piwik/piwik.php:74
+msgid "Piwik URL"
+msgstr "URL"
+
+#: plugins/piwik/piwik.php:75
+msgid "Piwik site ID"
+msgstr "Piwik 网站 ID"
+
+#: plugins/playvideos/playvideos.php:26
+msgid "Video player"
+msgstr "视频播放器"
+
+#: plugins/playvideos/playvideos.php:29
+msgid "Play Videos"
+msgstr "播放视频"
+
+#: plugins/playvideos/playvideos.php:60
+msgid "Add a button in the toolbar allowing to watch all videos."
+msgstr "工具栏中添加一个按钮,以便观看所有视频。"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:72
+#, php-format
+msgid "Could not publish to PubSubHubbub: %s"
+msgstr "无法发布到 PubSubHubbub:%s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:99
+#, php-format
+msgid "Could not post to %s"
+msgstr "无法发布到 %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:103
+#, php-format
+msgid "Bad response from the hub %s"
+msgstr "集线器 %s 传回异常响应"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:114
+msgid "Enable PubSubHubbub feed publishing."
+msgstr "启用 PubSubHubbub 订阅点发布。"
+
+#: plugins/qrcode/qrcode.php:74 plugins/wallabag/wallabag.php:72
+msgid "For each link, add a QRCode icon."
+msgstr "为每个链接添加一个 QR 码(二维码)图标。"
+
+#: plugins/wallabag/wallabag.php:22
+msgid ""
+"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
+"plugin administration page."
+msgstr "Wallabag 插件错误:请在插件管理页面中定义“WALLABAG_URL”设置。"
+
+#: plugins/wallabag/wallabag.php:49
+msgid "Save to wallabag"
+msgstr "保存到 wallabag"
+
+#: plugins/wallabag/wallabag.php:73
+msgid "Wallabag API URL"
+msgstr "Wallabag API 网址"
+
+#: plugins/wallabag/wallabag.php:74
+msgid "Wallabag API version (1 or 2)"
+msgstr "Wallabag API 版本 (1 或 2)"
+
+#: tmp/404.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
+msgid "Sorry, nothing to see here."
+msgstr "抱歉,这里没什么可看的。"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "URL or leave empty to post a note"
+msgstr "填入要记录的网址,或者留空以添加便签"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "BULK CREATION"
+msgstr "批量创建"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+msgid "Metadata asynchronous retrieval is disabled."
+msgstr "元数据异步检索已禁用。"
+
+#: 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 ""
+"我们建议您在配置中启用 常规 > enable_async_metadata 以进行批量的链接"
+"创建。"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
+msgid "Shaare multiple new links"
+msgstr "记录多个新链接"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
+msgid "Add one URL per line to create multiple bookmarks."
+msgstr "每行一个网址(URL),批量创建书签。"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+msgid "Tags"
+msgstr "标签"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+msgid "Private"
+msgstr "私有"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
+msgid "Add links"
+msgstr "添加链接"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Current password"
+msgstr "当前密码"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "New password"
+msgstr "新密码"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Change"
+msgstr "更改"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
+msgid "Tag"
+msgstr "标签"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "New name"
+msgstr "新名称"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
+msgid "Case sensitive"
+msgstr "区分大小写"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:68
+msgid "Rename tag"
+msgstr "重命名标签"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+msgid "Delete tag"
+msgstr "删除标签"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+msgid "You can also edit tags in the"
+msgstr "您也可以在这里编辑标签:"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+msgid "tag list"
+msgstr "标签列表"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+msgid "Change tags separator"
+msgstr "更改标签分隔符"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:50
+msgid "Your current tag separator is"
+msgstr "您当前的标签分隔符是"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
+msgid "New separator"
+msgstr "新的分隔符"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:355
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
+msgid "Save"
+msgstr "保存"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
+msgid "Note that hashtags won't fully work with a non-whitespace separator."
+msgstr "注意,井号标签(hashtags)不完全兼容非空白符号的分隔符。"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "title"
+msgstr "标题"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+msgid "Home link"
+msgstr "(左上角)首页网址"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+msgid "Default value"
+msgstr "默认值"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "Theme"
+msgstr "主题"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:85
+msgid "Description formatter"
+msgstr "描述栏的格式化语法"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
+msgid "Language"
+msgstr "语言设置"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
+msgid "Timezone"
+msgstr "时区"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+msgid "Continent"
+msgstr "大洲"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+msgid "City"
+msgstr "城市"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:191
+msgid "Disable session cookie hijacking protection"
+msgstr "禁用会话 Cookie 劫持防护"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:193
+msgid "Check this if you get disconnected or if your IP address changes often"
+msgstr "仅建议您在反复遇到退出登录或者频繁的 IP 地址变化时选中"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:210
+msgid "Private links by default"
+msgstr "默认私有链接"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:211
+msgid "All new links are private by default"
+msgstr "默认将所有链接定为私有"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:226
+msgid "RSS direct links"
+msgstr "RSS 直接链接"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:227
+msgid "Check this to use direct URL instead of permalink in feeds"
+msgstr "选中此项则在 RSS 订阅源中提供直接链接,而非永久链接"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:242
+msgid "Hide public links"
+msgstr "隐藏公开链接"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:243
+msgid "Do not show any links if the user is not logged in"
+msgstr "用户未登录时不显示任何链接"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:258
+msgid "Check updates"
+msgstr "检查更新"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:259
+msgid "Notify me when a new release is ready"
+msgstr "有新版本时通知我"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:274
+msgid "Automatically retrieve description for new bookmarks"
+msgstr "自动为新书签检索描述"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:275
+msgid "Shaarli will try to retrieve the description from meta HTML headers"
+msgstr "Shaarli 将尝试从 HTML 头部的元信息检索描述"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:290
+msgid "Enable REST API"
+msgstr "启用 REST API"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:291
+msgid "Allow third party software to use Shaarli such as mobile application"
+msgstr "允许第三方软件使用 Shaarli(如移动应用程序)"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:306
+msgid "API secret"
+msgstr "API 密钥"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:320
+msgid "Enable thumbnails"
+msgstr "缩略图"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:324
+msgid "You need to enable the extension php-gd
to use thumbnails."
+msgstr "您需要启用扩展 php-gd
才能使用缩略图。"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:328
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
+msgid "Synchronize thumbnails"
+msgstr "同步缩略图"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:339
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+msgid "All"
+msgstr "全部"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:343
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:106
+msgid "Only common media hosts"
+msgstr "仅大型媒体网站"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:347
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
+msgid "None"
+msgstr "无"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+msgid "1 RSS entry per :type"
+msgid_plural ""
+msgstr[0] "每:type一个 RSS 订阅源"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:49
+msgid "Previous :type"
+msgid_plural ""
+msgstr[0] "上一:type"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:56
+msgid "All links of one :type in a single page."
+msgid_plural ""
+msgstr[0] "一:type内的所有链接,汇聚在一个页面上。"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+msgid "Next :type"
+msgid_plural ""
+msgstr[0] "下一:type"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Export Database"
+msgstr "备份和导出数据库"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Selection"
+msgstr "选择"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+msgid "Public"
+msgstr "公开"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:51
+msgid "Prepend note permalinks with this Shaarli instance's URL"
+msgstr "便签指向本实例上的永久链接"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
+msgid "Useful to import bookmarks in a web browser"
+msgstr "推荐用于在网页浏览器中导入书签"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Import Database"
+msgstr "导入数据库"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Maximum size allowed:"
+msgstr "最大允许:"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Visibility"
+msgstr "可见性"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Use values from the imported file, default to public"
+msgstr "使用被导入文件中的值,默认为公开"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+msgid "Import all bookmarks as private"
+msgstr "将全部书签导入为私有"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+msgid "Import all bookmarks as public"
+msgstr "将全部书签导入为公开"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
+msgid "Overwrite existing bookmarks"
+msgstr "覆盖现有书签"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "Duplicates based on URL"
+msgstr "去重基于网址"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
+msgid "Add default tags"
+msgstr "添加默认标签"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
+msgid "shaare"
+msgid_plural "shaares"
+msgstr[0] "条记录"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+msgid "private link"
+msgid_plural "private links"
+msgstr[0] "私有链接"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:30
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:123
+msgid "Search text"
+msgstr "搜索文本"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:130
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: 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 "按标签过滤"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+#: 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"
+msgstr "搜索"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:110
+msgid "Nothing found."
+msgstr "什么都没找到。"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:118
+#, php-format
+msgid "%s result"
+msgid_plural "%s results"
+msgstr[0] "%s 条结果"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
+msgid "for"
+msgstr "对于"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
+msgid "tagged"
+msgstr "标签"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:133
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
+msgid "Remove tag"
+msgstr "移除标签"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:144
+msgid "with status"
+msgstr "状态:"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
+msgid "without any tag"
+msgstr "没有任何标签"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:173
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
+msgid "Delete"
+msgstr "删除"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:41
+msgid "Fold"
+msgstr "折叠"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:177
+msgid "Edited: "
+msgstr "编辑于: "
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
+msgid "permalink"
+msgstr "永久链接"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:183
+msgid "Add tag"
+msgstr "添加标签"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:185
+msgid "Toggle sticky"
+msgstr "切换置顶"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:187
+msgid "Sticky"
+msgstr "置顶"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:189
+msgid "Share a private link"
+msgstr "分享一个私有链接"
+
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:5
+msgid "Filters"
+msgstr "过滤器"
+
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:10
+msgid "Only display private links"
+msgstr "仅显示私有链接"
+
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:13
+msgid "Only display public links"
+msgstr "仅显示公开链接"
+
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18
+msgid "Filter untagged links"
+msgstr "过滤出没有标签的链接"
+
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:24
+msgid "Select all"
+msgstr "全选"
+
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:29
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:89
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
+msgid "Fold all"
+msgstr "全部折叠"
+
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:76
+msgid "Links per page"
+msgstr "每页链接数"
+
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:48
+msgid "by the Shaarli community"
+msgstr "由 Shaarli 社区提供"
+
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:16
+msgid "Documentation"
+msgstr "文档"
+
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
+msgid "Expand"
+msgstr "展开"
+
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
+msgid "Expand all"
+msgstr "全部展开"
+
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45
+msgid "Are you sure you want to delete this link?"
+msgstr "确定删除此链接?"
+
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46
+msgid "Are you sure you want to delete this tag?"
+msgstr "确定删除此标签?"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:11
+msgid "Menu"
+msgstr "菜单"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:38
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "Tag cloud"
+msgstr "标签云"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:67
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:92
+msgid "RSS Feed"
+msgstr "RSS 订阅源"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:72
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:108
+msgid "Logout"
+msgstr "退出登录"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:152
+msgid "Set public"
+msgstr "设为公开"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:157
+msgid "Set private"
+msgstr "设为私有"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:167
+msgid "Username"
+msgstr "用户名"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:168
+msgid "Password"
+msgstr "密码"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:171
+msgid "Remember me"
+msgstr "记住登录"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:189
+msgid "is available"
+msgstr "可用"
+
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:196
+msgid "Error"
+msgstr "出错"
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "There is no cached thumbnail."
+msgstr "没有已缓存的缩略图。"
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
+msgid "Try to synchronize them."
+msgstr "尝试同步。"
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Picture Wall"
+msgstr "图片墙"
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "pics"
+msgstr "张图片"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "You need to enable Javascript to change plugin loading order."
+msgstr "启用 JavaScript 才能更改插件加载顺序。"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+msgid "Plugin administration"
+msgstr "插件管理"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Enabled Plugins"
+msgstr "已启用的插件"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
+msgid "No plugin enabled."
+msgstr "没有已启用的插件。"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
+msgid "Disable"
+msgstr "禁用"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:98
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
+msgid "Name"
+msgstr "名称"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:75
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:99
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
+msgid "Description"
+msgstr "描述"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
+msgid "Order"
+msgstr "排序"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
+msgid "Disabled Plugins"
+msgstr "已禁用的插件"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
+msgid "No plugin disabled."
+msgstr "没有已禁用插件。"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
+msgid "Enable"
+msgstr "启用"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
+msgid "More plugins available"
+msgstr "更多可用插件"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
+msgid "in the documentation"
+msgstr "见文档"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
+msgid "Plugin configuration"
+msgstr "插件配置"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:195
+msgid "No parameter available."
+msgstr "没有可用参数。"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "General"
+msgstr "常规"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:20
+msgid "Index URL"
+msgstr "索引网址"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Base path"
+msgstr "基础路径"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Client IP"
+msgstr "客户端 IP"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+msgid "Trusted reverse proxies"
+msgstr "受信任的反向代理"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "N/A"
+msgstr "不适用"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
+msgid "Version"
+msgstr "版本"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
+msgid "Current version"
+msgstr "当前版本"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
+msgid "Latest release"
+msgstr "最新版本"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
+msgid "Visit releases page on Github"
+msgstr "访问 Github 上的发布页面"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
+msgid "Thumbnails"
+msgstr "缩略图"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:96
+msgid "Thumbnails status"
+msgstr "缩略图状态"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:121
+msgid "Synchronize all link thumbnails"
+msgstr "同步所有链接缩略图"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:128
+msgid "Cache"
+msgstr "缓存"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132
+msgid "Clear main cache"
+msgstr "清除主缓存"
+
+#: tmp/server.b91ef64efc3688266305ea9b42e5017e.rtpl.php:138
+msgid "Clear thumbnails cache"
+msgstr "清除缩略图缓存"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:2
+msgid "Permissions"
+msgstr "权限"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:8
+msgid "There are permissions that need to be fixed."
+msgstr "有些权限需要修复。"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:23
+msgid "All read/write permissions are properly set."
+msgstr "所有读/写权限已正确设置。"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:32
+msgid "Running PHP"
+msgstr "运行有 PHP"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:36
+msgid "End of life: "
+msgstr "生命周期终结:"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:48
+msgid "Extension"
+msgstr "扩展"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:49
+msgid "Usage"
+msgstr "使用量"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:50
+msgid "Status"
+msgstr "状态"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:51
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:66
+msgid "Loaded"
+msgstr "已加载"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
+msgid "Required"
+msgstr "必需"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:60
+msgid "Optional"
+msgstr "可选"
+
+#: tmp/server.requirements.cedf684561d925457130839629000a81.rtpl.php:70
+msgid "Not loaded"
+msgstr "未加载"
+
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "tags"
+msgstr "个标签"
+
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "List all links with those tags"
+msgstr "列出带有这些标签的所有链接"
+
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "Tag list"
+msgstr "标签列表"
+
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
+msgid "Sort by:"
+msgstr "排序方式:"
+
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5
+msgid "Cloud"
+msgstr "云"
+
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6
+msgid "Most used"
+msgstr "最常用"
+
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7
+msgid "Alphabetical"
+msgstr "字母顺序"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Settings"
+msgstr "设置"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Change Shaarli settings: title, timezone, etc."
+msgstr "更改 Shaarli 的设置:标题、时区等。"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
+msgid "Configure your Shaarli"
+msgstr "配置 Shaarli"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
+msgid "Enable, disable and configure plugins"
+msgstr "启用、禁用和配置插件"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:27
+msgid "Check instance's server configuration"
+msgstr "检查本实例的服务器配置"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+msgid "Change your password"
+msgstr "更改您的密码"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+msgid "Rename or delete a tag in all links"
+msgstr "重命名或删除特定标签"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+msgid ""
+"Import Netscape HTML bookmarks (as exported from Firefox, Chrome, Opera, "
+"delicious...)"
+msgstr ""
+"导入 Netscape HTML 书签格式(可从 Firefox、Chrome、Opera、delicious 等导出)"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+msgid "Import links"
+msgstr "导入链接"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
+msgid ""
+"Export Netscape HTML bookmarks (which can be imported in Firefox, Chrome, "
+"Opera, delicious...)"
+msgstr ""
+"导出 Netscape HTML 书签格式(可用于 Firefox、Chrome、Opera、delicious 等导"
+"入)"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:54
+msgid "Export database"
+msgstr "导出数据库"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
+msgid ""
+"Drag one of these button to your bookmarks toolbar or right-click it and "
+"\"Bookmark This Link\""
+msgstr "拖拽下方的按钮到您的书签工具栏,或者右击它并选择“将链接加入书签”"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
+msgid "then click on the bookmarklet in any page you want to share."
+msgstr "然后在处于任何您想记录或分享的页面时,单击已添加的按钮。"
+
+#: 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"
+msgstr "拖拽此链接到您的书签工具栏,或者右击它并选择“将链接加入书签”"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+msgid "then click ✚Shaare link button in any page you want to share"
+msgstr "然后在处于您想记录或分享的页面时,单击“✚记录链接”按钮"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:92
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:114
+msgid "The selected text is too long, it will be truncated."
+msgstr "所选文本太长,将会被截断。"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+msgid "Shaare link"
+msgstr "记录链接"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:107
+msgid ""
+"Then click ✚Add Note button anytime to start composing a private Note (text "
+"post) to your Shaarli"
+msgstr "然后随时单击“✚记录便签”按钮,开始在您的 Shaarli 中撰写个人便签"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
+msgid "Add Note"
+msgstr "记录便签"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:132
+msgid "3rd party"
+msgstr "第三方"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:135
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:140
+msgid "plugin"
+msgstr "插件"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:165
+msgid ""
+"Drag this link to your bookmarks toolbar, or right-click it and choose "
+"Bookmark This Link"
+msgstr "将此链接拖拽到您的书签工具栏,或者右击它并选择将此链接加入书签"
+
+#: plugins/playvideos/youtube_playlist.js:214
+msgid "plugins/playvideos/jquery-1.11.2.min.js"
+msgstr "plugins/playvideos/jquery-1.11.2.min.js"
+
+#, fuzzy
+#~ msgid "Created:"
+#~ msgstr "创建于:"
+
+#, fuzzy
+#~ msgid "URL"
+#~ msgstr "网址"
+
+#, fuzzy
+#~ msgid "Title"
+#~ msgstr "标题"
+
+#, fuzzy
+#~ msgid "Cancel"
+#~ msgstr "取消"
+
+#, fuzzy
+#~ msgid "Apply Changes"
+#~ msgstr "应用更改"
+
+#, fuzzy
+#~ msgid "Save all"
+#~ msgstr ""
+#~ "保存所有\n"
+#~ "两侧文件都将保存"
+
+#, fuzzy
+#~ msgid "Install"
+#~ msgstr "安装"
+
+#, fuzzy
+#~ msgid "Rename"
+#~ msgstr "Umbenennen"
+
+#, fuzzy
+#~ msgid "The Daily Shaarli"
+#~ msgstr "Der tägliche Shaarli"
+
+#, fuzzy
+#~ 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"
+#~ "strong> aufrufen."
+
+#~ msgid "Add to"
+#~ msgstr "添加到"
+
+#~ msgid "Plugin"
+#~ msgstr "插件"
diff --git a/index.php b/index.php
index 1c71d773..f4210aed 100644
--- a/index.php
+++ b/index.php
@@ -1,4 +1,5 @@
/shaarli/
-define('WEB_PATH', substr($_SERVER['REQUEST_URI'], 0, 1+strrpos($_SERVER['REQUEST_URI'], '/', 0)));
-
-// High execution time in case of problematic imports/exports.
-ini_set('max_input_time', '60');
-
-// Try to set max upload file size and read
-ini_set('memory_limit', '128M');
-ini_set('post_max_size', '16M');
-ini_set('upload_max_filesize', '16M');
-
-// See all error except warnings
-error_reporting(E_ALL^E_WARNING);
-// See all errors (for debugging only)
-//error_reporting(-1);
-
-
-// 3rd-party libraries
-if (! file_exists(__DIR__ . '/vendor/autoload.php')) {
- header('Content-Type: text/plain; charset=utf-8');
- echo "Error: missing Composer configuration\n\n"
- ."If you installed Shaarli through Git or using the development branch,\n"
- ."please refer to the installation documentation to install PHP"
- ." dependencies using Composer:\n"
- ."- https://shaarli.readthedocs.io/en/master/Server-configuration/\n"
- ."- https://shaarli.readthedocs.io/en/master/Download-and-Installation/";
- exit;
-}
require_once 'inc/rain.tpl.class.php';
require_once __DIR__ . '/vendor/autoload.php';
// Shaarli library
require_once 'application/bookmark/LinkUtils.php';
require_once 'application/config/ConfigPlugin.php';
-require_once 'application/feed/Cache.php';
require_once 'application/http/HttpUtils.php';
require_once 'application/http/UrlUtils.php';
-require_once 'application/updater/UpdaterUtils.php';
-require_once 'application/FileUtils.php';
require_once 'application/TimeZone.php';
require_once 'application/Utils.php';
-use \Shaarli\ApplicationUtils;
-use \Shaarli\Bookmark\Exception\LinkNotFoundException;
-use \Shaarli\Bookmark\LinkDB;
-use \Shaarli\Config\ConfigManager;
-use \Shaarli\Feed\CachedPage;
-use \Shaarli\Feed\FeedBuilder;
-use \Shaarli\History;
-use \Shaarli\Languages;
-use \Shaarli\Netscape\NetscapeBookmarkUtils;
-use \Shaarli\Plugin\PluginManager;
-use \Shaarli\Render\PageBuilder;
-use \Shaarli\Render\ThemeUtils;
-use \Shaarli\Router;
-use \Shaarli\Security\LoginManager;
-use \Shaarli\Security\SessionManager;
-use \Shaarli\Thumbnailer;
-use \Shaarli\Updater\Updater;
+require_once __DIR__ . '/init.php';
-// Ensure the PHP version is supported
-try {
- ApplicationUtils::checkPHPVersion('5.5', PHP_VERSION);
-} catch (Exception $exc) {
- header('Content-Type: text/plain; charset=utf-8');
- echo $exc->getMessage();
- exit;
-}
-
-define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
-
-// Force cookie path (but do not change lifetime)
-$cookie = session_get_cookie_params();
-$cookiedir = '';
-if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
- $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
-}
-// Set default cookie expiration and path.
-session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
-// Set session parameters on server side.
-// Use cookies to store session.
-ini_set('session.use_cookies', 1);
-// Force cookies for session (phpsessionID forbidden in URL).
-ini_set('session.use_only_cookies', 1);
-// Prevent PHP form using sessionID in URL if cookies are disabled.
-ini_set('session.use_trans_sid', false);
-
-session_name('shaarli');
-// Start session if needed (Some server auto-start sessions).
-if (session_status() == PHP_SESSION_NONE) {
- session_start();
-}
-
-// Regenerate session ID if invalid or not defined in cookie.
-if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
- session_regenerate_id(true);
- $_COOKIE['shaarli'] = session_id();
-}
+use Katzgrau\KLogger\Logger;
+use Psr\Log\LogLevel;
+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;
+use Shaarli\Security\SessionManager;
+use Slim\App;
$conf = new ConfigManager();
-$sessionManager = new SessionManager($_SESSION, $conf);
-$loginManager = new LoginManager($conf, $sessionManager);
-$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
-$clientIpId = client_ip_id($_SERVER);
-// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
-if (! defined('LC_MESSAGES')) {
- define('LC_MESSAGES', LC_COLLATE);
+// Manually override root URL for complex server configurations
+define('SHAARLI_ROOT_URL', $conf->get('general.root_url', null));
+
+// In dev mode, throw exception on any warning
+if ($conf->get('dev.debug', false)) {
+ // See all errors (for debugging only)
+ error_reporting(-1);
+
+ set_error_handler(function ($errno, $errstr, $errfile, $errline, array $errcontext = []) {
+ // Skip PHP 8 deprecation warning with Pimple.
+ if (strpos($errfile, 'src/Pimple/Container.php') !== -1 && strpos($errstr, 'ArrayAccess::') !== -1) {
+ return error_log($errstr);
+ }
+
+ throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
+ });
}
+$logger = new Logger(
+ is_writable($conf->get('resource.log')) ? dirname($conf->get('resource.log')) : 'php://temp',
+ !$conf->get('dev.debug') ? LogLevel::INFO : LogLevel::DEBUG,
+ ['filename' => basename($conf->get('resource.log'))]
+);
+$sessionManager = new SessionManager($_SESSION, $conf, session_save_path());
+$sessionManager->initialize();
+$cookieManager = new CookieManager($_COOKIE);
+$banManager = new BanManager(
+ $conf->get('security.trusted_proxies', []),
+ $conf->get('security.ban_after'),
+ $conf->get('security.ban_duration'),
+ $conf->get('resource.ban_file', 'data/ipbans.php'),
+ $logger
+);
+$loginManager = new LoginManager($conf, $sessionManager, $cookieManager, $banManager, $logger);
+$loginManager->generateStaySignedInToken($_SERVER['REMOTE_ADDR']);
+
// Sniff browser language and set date format accordingly.
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
@@ -142,1819 +84,102 @@
new Languages(setlocale(LC_MESSAGES, 0), $conf);
$conf->setEmpty('general.timezone', date_default_timezone_get());
-$conf->setEmpty('general.title', t('Shared links on '). escape(index_url($_SERVER)));
-RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme').'/'; // template directory
-RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
+$conf->setEmpty('general.title', t('Shared bookmarks on ') . escape(index_url($_SERVER)));
-$pluginManager = new PluginManager($conf);
-$pluginManager->load($conf->get('general.enabled_plugins'));
+RainTPL::$tpl_dir = $conf->get('resource.raintpl_tpl') . '/' . $conf->get('resource.theme') . '/'; // template directory
+RainTPL::$cache_dir = $conf->get('resource.raintpl_tmp'); // cache directory
date_default_timezone_set($conf->get('general.timezone', 'UTC'));
-ob_start(); // Output buffering for the page cache.
-
-// Prevent caching on client side or proxy: (yes, it's ugly)
-header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
-header("Cache-Control: no-store, no-cache, must-revalidate");
-header("Cache-Control: post-check=0, pre-check=0", false);
-header("Pragma: no-cache");
-
-if (! is_file($conf->getConfigFileExt())) {
- // Ensure Shaarli has proper access to its resources
- $errors = ApplicationUtils::checkResourcePermissions($conf);
-
- if ($errors != array()) {
- $message = ''. t('Insufficient permissions:') .'
';
-
- foreach ($errors as $error) {
- $message .= '- '.$error.'
';
- }
- $message .= '
';
-
- header('Content-Type: text/html; charset=utf-8');
- echo $message;
- exit;
- }
-
- // Display the installation form if no existing config is found
- install($conf, $sessionManager, $loginManager);
-}
-
-$loginManager->checkLoginState($_COOKIE, $clientIpId);
-
-/**
- * Adapter function to ensure compatibility with third-party templates
- *
- * @see https://github.com/shaarli/Shaarli/pull/1086
- *
- * @return bool true when the user is logged in, false otherwise
- */
-function isLoggedIn()
-{
- global $loginManager;
- return $loginManager->isLoggedIn();
-}
-
-
-// ------------------------------------------------------------------------------------------
-// Process login form: Check if login/password is correct.
-if (isset($_POST['login'])) {
- if (! $loginManager->canLogin($_SERVER)) {
- die(t('I said: NO. You are banned for the moment. Go away.'));
- }
- if (isset($_POST['password'])
- && $sessionManager->checkToken($_POST['token'])
- && $loginManager->checkCredentials($_SERVER['REMOTE_ADDR'], $clientIpId, $_POST['login'], $_POST['password'])
- ) {
- $loginManager->handleSuccessfulLogin($_SERVER);
-
- $cookiedir = '';
- if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
- // Note: Never forget the trailing slash on the cookie path!
- $cookiedir = dirname($_SERVER["SCRIPT_NAME"]) . '/';
- }
-
- if (!empty($_POST['longlastingsession'])) {
- // Keep the session cookie even after the browser closes
- $sessionManager->setStaySignedIn(true);
- $expirationTime = $sessionManager->extendSession();
-
- setcookie(
- $loginManager::$STAY_SIGNED_IN_COOKIE,
- $loginManager->getStaySignedInToken(),
- $expirationTime,
- WEB_PATH
- );
- } else {
- // Standard session expiration (=when browser closes)
- $expirationTime = 0;
- }
-
- // Send cookie with the new expiration date to the browser
- session_set_cookie_params($expirationTime, $cookiedir, $_SERVER['SERVER_NAME']);
- session_regenerate_id(true);
-
- // Optional redirect after login:
- if (isset($_GET['post'])) {
- $uri = '?post='. urlencode($_GET['post']);
- foreach (array('description', 'source', 'title', 'tags') as $param) {
- if (!empty($_GET[$param])) {
- $uri .= '&'.$param.'='.urlencode($_GET[$param]);
- }
- }
- header('Location: '. $uri);
- exit;
- }
-
- if (isset($_GET['edit_link'])) {
- header('Location: ?edit_link='. escape($_GET['edit_link']));
- exit;
- }
-
- if (isset($_POST['returnurl'])) {
- // Prevent loops over login screen.
- if (strpos($_POST['returnurl'], 'do=login') === false) {
- header('Location: '. generateLocation($_POST['returnurl'], $_SERVER['HTTP_HOST']));
- exit;
- }
- }
- header('Location: ?');
- exit;
- } else {
- $loginManager->handleFailedLogin($_SERVER);
- $redir = '&username='. urlencode($_POST['login']);
- if (isset($_GET['post'])) {
- $redir .= '&post=' . urlencode($_GET['post']);
- foreach (array('description', 'source', 'title', 'tags') as $param) {
- if (!empty($_GET[$param])) {
- $redir .= '&' . $param . '=' . urlencode($_GET[$param]);
- }
- }
- }
- // Redirect to login screen.
- echo '';
- exit;
- }
-}
-
-// ------------------------------------------------------------------------------------------
-// Token management for XSRF protection
-// Token should be used in any form which acts on data (create,update,delete,import...).
-if (!isset($_SESSION['tokens'])) {
- $_SESSION['tokens']=array(); // Token are attached to the session.
-}
-
-/**
- * Daily RSS feed: 1 RSS entry per day giving all the links on that day.
- * Gives the last 7 days (which have links).
- * This RSS feed cannot be filtered.
- *
- * @param ConfigManager $conf Configuration Manager instance
- * @param LoginManager $loginManager LoginManager instance
- */
-function showDailyRSS($conf, $loginManager)
-{
- // Cache system
- $query = $_SERVER['QUERY_STRING'];
- $cache = new CachedPage(
- $conf->get('config.PAGE_CACHE'),
- page_url($_SERVER),
- startsWith($query, 'do=dailyrss') && !$loginManager->isLoggedIn()
- );
- $cached = $cache->cachedVersion();
- if (!empty($cached)) {
- echo $cached;
- exit;
- }
-
- // If cached was not found (or not usable), then read the database and build the response:
- // Read links from database (and filter private links if used it not logged in).
- $LINKSDB = new LinkDB(
- $conf->get('resource.datastore'),
- $loginManager->isLoggedIn(),
- $conf->get('privacy.hide_public_links')
- );
-
- /* Some Shaarlies may have very few links, so we need to look
- back in time until we have enough days ($nb_of_days).
- */
- $nb_of_days = 7; // We take 7 days.
- $today = date('Ymd');
- $days = array();
-
- foreach ($LINKSDB as $link) {
- $day = $link['created']->format('Ymd'); // Extract day (without time)
- if (strcmp($day, $today) < 0) {
- if (empty($days[$day])) {
- $days[$day] = array();
- }
- $days[$day][] = $link;
- }
-
- if (count($days) > $nb_of_days) {
- break; // Have we collected enough days?
- }
- }
-
- // Build the RSS feed.
- header('Content-Type: application/rss+xml; charset=utf-8');
- $pageaddr = escape(index_url($_SERVER));
- echo '';
- echo '';
- echo 'Daily - '. $conf->get('general.title') . ' ';
- echo ''. $pageaddr .'';
- echo 'Daily shared links ';
- echo 'en-en ';
- echo ''. $pageaddr .' '. PHP_EOL;
-
- // For each day.
- foreach ($days as $day => $links) {
- $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
- $absurl = escape(index_url($_SERVER).'?do=daily&day='.$day); // Absolute URL of the corresponding "Daily" page.
-
- // We pre-format some fields for proper output.
- foreach ($links as &$link) {
- $link['formatedDescription'] = format_description($link['description']);
- $link['timestamp'] = $link['created']->getTimestamp();
- if (is_note($link['url'])) {
- $link['url'] = index_url($_SERVER) . $link['url']; // make permalink URL absolute
- }
- }
-
- // Then build the HTML for this day:
- $tpl = new RainTPL;
- $tpl->assign('title', $conf->get('general.title'));
- $tpl->assign('daydate', $dayDate->getTimestamp());
- $tpl->assign('absurl', $absurl);
- $tpl->assign('links', $links);
- $tpl->assign('rssdate', escape($dayDate->format(DateTime::RSS)));
- $tpl->assign('hide_timestamps', $conf->get('privacy.hide_timestamps', false));
- $tpl->assign('index_url', $pageaddr);
- $html = $tpl->draw('dailyrss', true);
-
- echo $html . PHP_EOL;
- }
- echo ' ';
-
- $cache->cache(ob_get_contents());
- ob_end_flush();
- exit;
-}
-
-/**
- * Show the 'Daily' page.
- *
- * @param PageBuilder $pageBuilder Template engine wrapper.
- * @param LinkDB $LINKSDB LinkDB instance.
- * @param ConfigManager $conf Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance.
- * @param LoginManager $loginManager Login Manager instance
- */
-function showDaily($pageBuilder, $LINKSDB, $conf, $pluginManager, $loginManager)
-{
- if (isset($_GET['day'])) {
- $day = $_GET['day'];
- if ($day === date('Ymd', strtotime('now'))) {
- $pageBuilder->assign('dayDesc', t('Today'));
- } elseif ($day === date('Ymd', strtotime('-1 days'))) {
- $pageBuilder->assign('dayDesc', t('Yesterday'));
- }
- } else {
- $day = date('Ymd', strtotime('now')); // Today, in format YYYYMMDD.
- $pageBuilder->assign('dayDesc', t('Today'));
- }
-
- $days = $LINKSDB->days();
- $i = array_search($day, $days);
- if ($i === false && count($days)) {
- // no links for day, but at least one day with links
- $i = count($days) - 1;
- $day = $days[$i];
- }
- $previousday = '';
- $nextday = '';
-
- if ($i !== false) {
- if ($i >= 1) {
- $previousday=$days[$i - 1];
- }
- if ($i < count($days) - 1) {
- $nextday = $days[$i + 1];
- }
- }
- try {
- $linksToDisplay = $LINKSDB->filterDay($day);
- } catch (Exception $exc) {
- error_log($exc);
- $linksToDisplay = array();
- }
-
- // We pre-format some fields for proper output.
- foreach ($linksToDisplay as $key => $link) {
- $taglist = explode(' ', $link['tags']);
- uasort($taglist, 'strcasecmp');
- $linksToDisplay[$key]['taglist']=$taglist;
- $linksToDisplay[$key]['formatedDescription'] = format_description($link['description']);
- $linksToDisplay[$key]['timestamp'] = $link['created']->getTimestamp();
- }
-
- $dayDate = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $day.'_000000');
- $data = array(
- 'pagetitle' => $conf->get('general.title') .' - '. format_date($dayDate, false),
- 'linksToDisplay' => $linksToDisplay,
- 'day' => $dayDate->getTimestamp(),
- 'dayDate' => $dayDate,
- 'previousday' => $previousday,
- 'nextday' => $nextday,
- );
-
- /* Hook is called before column construction so that plugins don't have
- to deal with columns. */
- $pluginManager->executeHooks('render_daily', $data, array('loggedin' => $loginManager->isLoggedIn()));
-
- /* We need to spread the articles on 3 columns.
- I did not want to use a JavaScript lib like http://masonry.desandro.com/
- so I manually spread entries with a simple method: I roughly evaluate the
- height of a div according to title and description length.
- */
- $columns = array(array(), array(), array()); // Entries to display, for each column.
- $fill = array(0, 0, 0); // Rough estimate of columns fill.
- foreach ($data['linksToDisplay'] as $key => $link) {
- // Roughly estimate length of entry (by counting characters)
- // Title: 30 chars = 1 line. 1 line is 30 pixels height.
- // Description: 836 characters gives roughly 342 pixel height.
- // This is not perfect, but it's usually OK.
- $length = strlen($link['title']) + (342 * strlen($link['description'])) / 836;
- if ($link['thumbnail']) {
- $length += 100; // 1 thumbnails roughly takes 100 pixels height.
- }
- // Then put in column which is the less filled:
- $smallest = min($fill); // find smallest value in array.
- $index = array_search($smallest, $fill); // find index of this smallest value.
- array_push($columns[$index], $link); // Put entry in this column.
- $fill[$index] += $length;
- }
-
- $data['cols'] = $columns;
-
- foreach ($data as $key => $value) {
- $pageBuilder->assign($key, $value);
- }
-
- $pageBuilder->assign('pagetitle', t('Daily') .' - '. $conf->get('general.title', 'Shaarli'));
- $pageBuilder->renderPage('daily');
- exit;
-}
-
-/**
- * Renders the linklist
- *
- * @param pageBuilder $PAGE pageBuilder instance.
- * @param LinkDB $LINKSDB LinkDB instance.
- * @param ConfigManager $conf Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance.
- */
-function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
-{
- buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
- $PAGE->renderPage('linklist');
-}
-
-/**
- * Render HTML page (according to URL parameters and user rights)
- *
- * @param ConfigManager $conf Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance,
- * @param LinkDB $LINKSDB
- * @param History $history instance
- * @param SessionManager $sessionManager SessionManager instance
- * @param LoginManager $loginManager LoginManager instance
- */
-function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager, $loginManager)
-{
- $updater = new Updater(
- read_updates_file($conf->get('resource.updates')),
- $LINKSDB,
- $conf,
- $loginManager->isLoggedIn(),
- $_SESSION
- );
- try {
- $newUpdates = $updater->update();
- if (! empty($newUpdates)) {
- write_updates_file(
- $conf->get('resource.updates'),
- $updater->getDoneUpdates()
- );
- }
- } catch (Exception $e) {
- die($e->getMessage());
- }
-
- $PAGE = new PageBuilder($conf, $_SESSION, $LINKSDB, $sessionManager->generateToken(), $loginManager->isLoggedIn());
- $PAGE->assign('linkcount', count($LINKSDB));
- $PAGE->assign('privateLinkcount', count_private($LINKSDB));
- $PAGE->assign('plugin_errors', $pluginManager->getErrors());
-
- // Determine which page will be rendered.
- $query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
- $targetPage = Router::findPage($query, $_GET, $loginManager->isLoggedIn());
-
- if (// if the user isn't logged in
- !$loginManager->isLoggedIn() &&
- // and Shaarli doesn't have public content...
- $conf->get('privacy.hide_public_links') &&
- // and is configured to enforce the login
- $conf->get('privacy.force_login') &&
- // and the current page isn't already the login page
- $targetPage !== Router::$PAGE_LOGIN &&
- // and the user is not requesting a feed (which would lead to a different content-type as expected)
- $targetPage !== Router::$PAGE_FEED_ATOM &&
- $targetPage !== Router::$PAGE_FEED_RSS
- ) {
- // force current page to be the login page
- $targetPage = Router::$PAGE_LOGIN;
- }
-
- // Call plugin hooks for header, footer and includes, specifying which page will be rendered.
- // Then assign generated data to RainTPL.
- $common_hooks = array(
- 'includes',
- 'header',
- 'footer',
- );
-
- foreach ($common_hooks as $name) {
- $plugin_data = array();
- $pluginManager->executeHooks(
- 'render_' . $name,
- $plugin_data,
- array(
- 'target' => $targetPage,
- 'loggedin' => $loginManager->isLoggedIn()
- )
- );
- $PAGE->assign('plugins_' . $name, $plugin_data);
- }
-
- // -------- Display login form.
- if ($targetPage == Router::$PAGE_LOGIN) {
- if ($conf->get('security.open_shaarli')) {
- header('Location: ?');
- exit;
- } // No need to login for open Shaarli
- if (isset($_GET['username'])) {
- $PAGE->assign('username', escape($_GET['username']));
- }
- $PAGE->assign('returnurl', (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']):''));
- // add default state of the 'remember me' checkbox
- $PAGE->assign('remember_user_default', $conf->get('privacy.remember_user_default'));
- $PAGE->assign('user_can_login', $loginManager->canLogin($_SERVER));
- $PAGE->assign('pagetitle', t('Login') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('loginform');
- exit;
- }
- // -------- User wants to logout.
- if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=logout')) {
- invalidateCaches($conf->get('resource.page_cache'));
- $sessionManager->logout();
- setcookie(LoginManager::$STAY_SIGNED_IN_COOKIE, 'false', 0, WEB_PATH);
- header('Location: ?');
- exit;
- }
-
- // -------- Picture wall
- if ($targetPage == Router::$PAGE_PICWALL) {
- $PAGE->assign('pagetitle', t('Picture wall') .' - '. $conf->get('general.title', 'Shaarli'));
- /*if (! $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) === Thumbnailer::MODE_NONE) {
- $PAGE->assign('linksToDisplay', []);
- $PAGE->renderPage('picwall');
- exit;
- }*/
-
- // Optionally filter the results:
- $links = $LINKSDB->filterSearch($_GET);
- $linksToDisplay = array();
-
- // Get only links which have a thumbnail.
- // Note: we do not retrieve thumbnails here, the request is too heavy.
- foreach($links as $key => $link)
- {
- /*if (isset($link['thumbnail']) && $link['thumbnail'] !== false) {
- $linksToDisplay[] = $link; // Add to array.
- }*/
- if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
- $link['url'] = index_url($_SERVER) . $link['url'];
- }
-
- $thumUrl = $conf->get('plugins.ExternalThumbshot_URL');
- if(!empty($conf->get('plugins.ExternalThumbshot_KEY'))){
- $key = $conf->get('plugins.ExternalThumbshot_KEY');
- }
- //if(empty($link['thumbnail'])){
- //$thumb = computeThumbnail($conf, $link['url']);
- //if(empty($link['thumbnail'])){
- if(!empty($key)){
- $hmac = '&hm='.hash_hmac('sha1', $link['url'], $key).'&url=';
- } else {
- $hmac = null;
- }
- $link['thumbnail'] = $thumUrl.$hmac.urlencode($link['url']);
- //} else {
- //$link['thumbnail'] = $link['thumbnail'];
- // }
-
- //}
- $linksToDisplay[]=$link; // Add to array.
- }
-
- $data = array(
- 'linksToDisplay' => $linksToDisplay,
- );
-
- $pluginManager->executeHooks('render_picwall', $data, array('loggedin' => $loginManager->isLoggedIn()));
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $PAGE->renderPage('picwall');
- exit;
- }
-
-
-
-
-
- // -------- Tag cloud
- if ($targetPage == Router::$PAGE_TAGCLOUD) {
- $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
- $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
- $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
-
- // We sort tags alphabetically, then choose a font size according to count.
- // First, find max value.
- $maxcount = 0;
- foreach ($tags as $value) {
- $maxcount = max($maxcount, $value);
- }
-
- alphabetical_sort($tags, false, true);
-
- $tagList = array();
- foreach ($tags as $key => $value) {
- if (in_array($key, $filteringTags)) {
- continue;
- }
- // Tag font size scaling:
- // default 15 and 30 logarithm bases affect scaling,
- // 22 and 6 are arbitrary font sizes for max and min sizes.
- $size = log($value, 15) / log($maxcount, 30) * 2.2 + 0.8;
- $tagList[$key] = array(
- 'count' => $value,
- 'size' => number_format($size, 2, '.', ''),
- );
- }
-
- $searchTags = implode(' ', escape($filteringTags));
- $data = array(
- 'search_tags' => $searchTags,
- 'tags' => $tagList,
- );
- $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => $loginManager->isLoggedIn()));
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
- $PAGE->assign('pagetitle', $searchTags. t('Tag cloud') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('tag.cloud');
- exit;
- }
-
- // -------- Tag list
- if ($targetPage == Router::$PAGE_TAGLIST) {
- $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
- $filteringTags = isset($_GET['searchtags']) ? explode(' ', $_GET['searchtags']) : [];
- $tags = $LINKSDB->linksCountPerTag($filteringTags, $visibility);
- foreach ($filteringTags as $tag) {
- if (array_key_exists($tag, $tags)) {
- unset($tags[$tag]);
- }
- }
-
- if (! empty($_GET['sort']) && $_GET['sort'] === 'alpha') {
- alphabetical_sort($tags, false, true);
- }
-
- $searchTags = implode(' ', escape($filteringTags));
- $data = [
- 'search_tags' => $searchTags,
- 'tags' => $tags,
- ];
- $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => $loginManager->isLoggedIn()]);
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $searchTags = ! empty($searchTags) ? $searchTags .' - ' : '';
- $PAGE->assign('pagetitle', $searchTags . t('Tag list') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('tag.list');
- exit;
- }
-
- // Daily page.
- if ($targetPage == Router::$PAGE_DAILY) {
- showDaily($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
- }
-
- // ATOM and RSS feed.
- if ($targetPage == Router::$PAGE_FEED_ATOM || $targetPage == Router::$PAGE_FEED_RSS) {
- $feedType = $targetPage == Router::$PAGE_FEED_RSS ? FeedBuilder::$FEED_RSS : FeedBuilder::$FEED_ATOM;
- header('Content-Type: application/'. $feedType .'+xml; charset=utf-8');
-
- // Cache system
- $query = $_SERVER['QUERY_STRING'];
- $cache = new CachedPage(
- $conf->get('resource.page_cache'),
- page_url($_SERVER),
- startsWith($query, 'do='. $targetPage) && !$loginManager->isLoggedIn()
- );
- $cached = $cache->cachedVersion();
- if (!empty($cached)) {
- echo $cached;
- exit;
- }
-
- // Generate data.
- $feedGenerator = new FeedBuilder($LINKSDB, $feedType, $_SERVER, $_GET, $loginManager->isLoggedIn());
- $feedGenerator->setLocale(strtolower(setlocale(LC_COLLATE, 0)));
- $feedGenerator->setHideDates($conf->get('privacy.hide_timestamps') && !$loginManager->isLoggedIn());
- $feedGenerator->setUsePermalinks(isset($_GET['permalinks']) || !$conf->get('feed.rss_permalinks'));
- $data = $feedGenerator->buildData();
-
- // Process plugin hook.
- $pluginManager->executeHooks('render_feed', $data, array(
- 'loggedin' => $loginManager->isLoggedIn(),
- 'target' => $targetPage,
- ));
-
- // Render the template.
- $PAGE->assignAll($data);
- $PAGE->renderPage('feed.'. $feedType);
- $cache->cache(ob_get_contents());
- ob_end_flush();
- exit;
- }
-
- // Display opensearch plugin (XML)
- if ($targetPage == Router::$PAGE_OPENSEARCH) {
- header('Content-Type: application/xml; charset=utf-8');
- $PAGE->assign('serverurl', index_url($_SERVER));
- $PAGE->renderPage('opensearch');
- exit;
- }
-
- // -------- User clicks on a tag in a link: The tag is added to the list of searched tags (searchtags=...)
- if (isset($_GET['addtag'])) {
- // Get previous URL (http_referer) and add the tag to the searchtags parameters in query.
- if (empty($_SERVER['HTTP_REFERER'])) {
- // In case browser does not send HTTP_REFERER
- header('Location: ?searchtags='.urlencode($_GET['addtag']));
- exit;
- }
- parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
-
- // Prevent redirection loop
- if (isset($params['addtag'])) {
- unset($params['addtag']);
- }
-
- // Check if this tag is already in the search query and ignore it if it is.
- // Each tag is always separated by a space
- if (isset($params['searchtags'])) {
- $current_tags = explode(' ', $params['searchtags']);
- } else {
- $current_tags = array();
- }
- $addtag = true;
- foreach ($current_tags as $value) {
- if ($value === $_GET['addtag']) {
- $addtag = false;
- break;
- }
- }
- // Append the tag if necessary
- if (empty($params['searchtags'])) {
- $params['searchtags'] = trim($_GET['addtag']);
- } elseif ($addtag) {
- $params['searchtags'] = trim($params['searchtags']).' '.trim($_GET['addtag']);
- }
-
- // We also remove page (keeping the same page has no sense, since the
- // results are different)
- unset($params['page']);
-
- header('Location: ?'.http_build_query($params));
- exit;
- }
-
- // -------- User clicks on a tag in result count: Remove the tag from the list of searched tags (searchtags=...)
- if (isset($_GET['removetag'])) {
- // Get previous URL (http_referer) and remove the tag from the searchtags parameters in query.
- if (empty($_SERVER['HTTP_REFERER'])) {
- header('Location: ?');
- exit;
- }
-
- // In case browser does not send HTTP_REFERER
- parse_str(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_QUERY), $params);
-
- // Prevent redirection loop
- if (isset($params['removetag'])) {
- unset($params['removetag']);
- }
-
- if (isset($params['searchtags'])) {
- $tags = explode(' ', $params['searchtags']);
- // Remove value from array $tags.
- $tags = array_diff($tags, array($_GET['removetag']));
- $params['searchtags'] = implode(' ', $tags);
-
- if (empty($params['searchtags'])) {
- unset($params['searchtags']);
- }
-
- // We also remove page (keeping the same page has no sense, since
- // the results are different)
- unset($params['page']);
- }
- header('Location: ?'.http_build_query($params));
- exit;
- }
-
- // -------- User wants to change the number of links per page (linksperpage=...)
- if (isset($_GET['linksperpage'])) {
- if (is_numeric($_GET['linksperpage'])) {
- $_SESSION['LINKS_PER_PAGE']=abs(intval($_GET['linksperpage']));
- }
-
- if (! empty($_SERVER['HTTP_REFERER'])) {
- $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('linksperpage'));
- } else {
- $location = '?';
- }
- header('Location: '. $location);
- exit;
- }
-
- // -------- User wants to see only private links (toggle)
- if (isset($_GET['visibility'])) {
- if ($_GET['visibility'] === 'private') {
- // Visibility not set or not already private, set private, otherwise reset it
- if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'private') {
- // See only private links
- $_SESSION['visibility'] = 'private';
- } else {
- unset($_SESSION['visibility']);
- }
- } elseif ($_GET['visibility'] === 'public') {
- if (empty($_SESSION['visibility']) || $_SESSION['visibility'] !== 'public') {
- // See only public links
- $_SESSION['visibility'] = 'public';
- } else {
- unset($_SESSION['visibility']);
- }
- }
-
- if (! empty($_SERVER['HTTP_REFERER'])) {
- $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('visibility'));
- } else {
- $location = '?';
- }
- header('Location: '. $location);
- exit;
- }
-
- // -------- User wants to see only untagged links (toggle)
- if (isset($_GET['untaggedonly'])) {
- $_SESSION['untaggedonly'] = empty($_SESSION['untaggedonly']);
-
- if (! empty($_SERVER['HTTP_REFERER'])) {
- $location = generateLocation($_SERVER['HTTP_REFERER'], $_SERVER['HTTP_HOST'], array('untaggedonly'));
- } else {
- $location = '?';
- }
- header('Location: '. $location);
- exit;
- }
-
- // -------- Handle other actions allowed for non-logged in users:
- if (!$loginManager->isLoggedIn()) {
- // User tries to post new link but is not logged in:
- // Show login screen, then redirect to ?post=...
- if (isset($_GET['post'])) {
- header( // Redirect to login page, then back to post link.
- 'Location: ?do=login&post='.urlencode($_GET['post']).
- (!empty($_GET['title'])?'&title='.urlencode($_GET['title']):'').
- (!empty($_GET['description'])?'&description='.urlencode($_GET['description']):'').
- (!empty($_GET['tags'])?'&tags='.urlencode($_GET['tags']):'').
- (!empty($_GET['source'])?'&source='.urlencode($_GET['source']):'')
- );
- exit;
- }
-
- showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
- if (isset($_GET['edit_link'])) {
- header('Location: ?do=login&edit_link='. escape($_GET['edit_link']));
- exit;
- }
-
- exit; // Never remove this one! All operations below are reserved for logged in user.
- }
-
- // -------- All other functions are reserved for the registered user:
-
- // -------- Display the Tools menu if requested (import/export/bookmarklet...)
- if ($targetPage == Router::$PAGE_TOOLS) {
- $data = [
- 'pageabsaddr' => index_url($_SERVER),
- 'sslenabled' => is_https($_SERVER),
- ];
- $pluginManager->executeHooks('render_tools', $data);
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $PAGE->assign('pagetitle', t('Tools') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('tools');
- exit;
- }
-
- // -------- User wants to change his/her password.
- if ($targetPage == Router::$PAGE_CHANGEPASSWORD) {
- if ($conf->get('security.open_shaarli')) {
- die(t('You are not supposed to change a password on an Open Shaarli.'));
- }
-
- if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword'])) {
- if (!$sessionManager->checkToken($_POST['token'])) {
- die(t('Wrong token.')); // Go away!
- }
-
- // Make sure old password is correct.
- $oldhash = sha1(
- $_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt')
- );
- if ($oldhash != $conf->get('credentials.hash')) {
- echo '';
- exit;
- }
- // Save new password
- // Salt renders rainbow-tables attacks useless.
- $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
- $conf->set(
- 'credentials.hash',
- sha1(
- $_POST['setpassword']
- . $conf->get('credentials.login')
- . $conf->get('credentials.salt')
- )
- );
- try {
- $conf->write($loginManager->isLoggedIn());
- } catch (Exception $e) {
- error_log(
- 'ERROR while writing config file after changing password.' . PHP_EOL .
- $e->getMessage()
- );
-
- // TODO: do not handle exceptions/errors in JS.
- echo '';
- exit;
- }
- echo '';
- exit;
- } else {
- // show the change password form.
- $PAGE->assign('pagetitle', t('Change password') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('changepassword');
- exit;
- }
- }
-
- // -------- User wants to change configuration
- if ($targetPage == Router::$PAGE_CONFIGURE) {
- if (!empty($_POST['title'])) {
- if (!$sessionManager->checkToken($_POST['token'])) {
- die(t('Wrong token.')); // Go away!
- }
- $tz = 'UTC';
- if (!empty($_POST['continent']) && !empty($_POST['city'])
- && isTimeZoneValid($_POST['continent'], $_POST['city'])
- ) {
- $tz = $_POST['continent'] . '/' . $_POST['city'];
- }
- $conf->set('general.timezone', $tz);
- $conf->set('general.title', escape($_POST['title']));
- $conf->set('general.header_link', escape($_POST['titleLink']));
- $conf->set('general.retrieve_description', !empty($_POST['retrieveDescription']));
- $conf->set('resource.theme', escape($_POST['theme']));
- $conf->set('security.session_protection_disabled', !empty($_POST['disablesessionprotection']));
- $conf->set('privacy.default_private_links', !empty($_POST['privateLinkByDefault']));
- $conf->set('feed.rss_permalinks', !empty($_POST['enableRssPermalinks']));
- $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
- $conf->set('privacy.hide_public_links', !empty($_POST['hidePublicLinks']));
- $conf->set('api.enabled', !empty($_POST['enableApi']));
- $conf->set('api.secret', escape($_POST['apiSecret']));
- $conf->set('translation.language', escape($_POST['language']));
-
- $thumbnailsMode = extension_loaded('gd') ? $_POST['enableThumbnails'] : Thumbnailer::MODE_NONE;
- if ($thumbnailsMode !== Thumbnailer::MODE_NONE
- && $thumbnailsMode !== $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE)
- ) {
- $_SESSION['warnings'][] = t(
- 'You have enabled or changed thumbnails mode. '
- .'Please synchronize them.'
- );
- }
- $conf->set('thumbnails.mode', $thumbnailsMode);
-
- try {
- $conf->write($loginManager->isLoggedIn());
- $history->updateSettings();
- invalidateCaches($conf->get('resource.page_cache'));
- } catch (Exception $e) {
- error_log(
- 'ERROR while writing config file after configuration update.' . PHP_EOL .
- $e->getMessage()
- );
-
- // TODO: do not handle exceptions/errors in JS.
- echo '';
- exit;
- }
- echo '';
- exit;
- } else {
- // Show the configuration form.
- $PAGE->assign('title', $conf->get('general.title'));
- $PAGE->assign('theme', $conf->get('resource.theme'));
- $PAGE->assign('theme_available', ThemeUtils::getThemes($conf->get('resource.raintpl_tpl')));
- list($continents, $cities) = generateTimeZoneData(
- timezone_identifiers_list(),
- $conf->get('general.timezone')
- );
- $PAGE->assign('continents', $continents);
- $PAGE->assign('cities', $cities);
- $PAGE->assign('retrieve_description', $conf->get('general.retrieve_description'));
- $PAGE->assign('private_links_default', $conf->get('privacy.default_private_links', false));
- $PAGE->assign('session_protection_disabled', $conf->get('security.session_protection_disabled', false));
- $PAGE->assign('enable_rss_permalinks', $conf->get('feed.rss_permalinks', false));
- $PAGE->assign('enable_update_check', $conf->get('updates.check_updates', true));
- $PAGE->assign('hide_public_links', $conf->get('privacy.hide_public_links', false));
- $PAGE->assign('api_enabled', $conf->get('api.enabled', true));
- $PAGE->assign('api_secret', $conf->get('api.secret'));
- $PAGE->assign('languages', Languages::getAvailableLanguages());
- $PAGE->assign('gd_enabled', extension_loaded('gd'));
- $PAGE->assign('thumbnails_mode', $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE));
- $PAGE->assign('pagetitle', t('Configure') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('configure');
- exit;
- }
- }
-
- // -------- User wants to rename a tag or delete it
- if ($targetPage == Router::$PAGE_CHANGETAG) {
- if (empty($_POST['fromtag']) || (empty($_POST['totag']) && isset($_POST['renametag']))) {
- $PAGE->assign('fromtag', ! empty($_GET['fromtag']) ? escape($_GET['fromtag']) : '');
- $PAGE->assign('pagetitle', t('Manage tags') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('changetag');
- exit;
- }
-
- if (!$sessionManager->checkToken($_POST['token'])) {
- die(t('Wrong token.'));
- }
-
- $toTag = isset($_POST['totag']) ? escape($_POST['totag']) : null;
- $alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), $toTag);
- $LINKSDB->save($conf->get('resource.page_cache'));
- foreach ($alteredLinks as $link) {
- $history->updateLink($link);
- }
- $delete = empty($_POST['totag']);
- $redirect = $delete ? 'do=changetag' : 'searchtags='. urlencode(escape($_POST['totag']));
- $count = count($alteredLinks);
- $alert = $delete
- ? sprintf(t('The tag was removed from %d link.', 'The tag was removed from %d links.', $count), $count)
- : sprintf(t('The tag was renamed in %d link.', 'The tag was renamed in %d links.', $count), $count);
- echo '';
- exit;
- }
-
- // -------- User wants to add a link without using the bookmarklet: Show form.
- if ($targetPage == Router::$PAGE_ADDLINK) {
- $PAGE->assign('pagetitle', t('Shaare a new link') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('addlink');
- exit;
- }
-
- // -------- User clicked the "Save" button when editing a link: Save link to database.
- if (isset($_POST['save_edit'])) {
- // Go away!
- if (! $sessionManager->checkToken($_POST['token'])) {
- die(t('Wrong token.'));
- }
-
- // lf_id should only be present if the link exists.
- $id = isset($_POST['lf_id']) ? intval(escape($_POST['lf_id'])) : $LINKSDB->getNextId();
- $link['id'] = $id;
- // Linkdate is kept here to:
- // - use the same permalink for notes as they're displayed when creating them
- // - let users hack creation date of their posts
- // See: https://shaarli.readthedocs.io/en/master/guides/various-hacks/#changing-the-timestamp-for-a-shaare
- $linkdate = escape($_POST['lf_linkdate']);
- $link['created'] = DateTime::createFromFormat(LinkDB::LINK_DATE_FORMAT, $linkdate);
- if (isset($LINKSDB[$id])) {
- // Edit
- $link['updated'] = new DateTime();
- $link['shorturl'] = $LINKSDB[$id]['shorturl'];
- $link['sticky'] = isset($LINKSDB[$id]['sticky']) ? $LINKSDB[$id]['sticky'] : false;
- $new = false;
- } else {
- // New link
- $link['updated'] = null;
- $link['shorturl'] = link_small_hash($link['created'], $id);
- $link['sticky'] = false;
- $new = true;
- }
-
- // Remove multiple spaces.
- $tags = trim(preg_replace('/\s\s+/', ' ', $_POST['lf_tags']));
- // Remove first '-' char in tags.
- $tags = preg_replace('/(^| )\-/', '$1', $tags);
- // Remove duplicates.
- $tags = implode(' ', array_unique(explode(' ', $tags)));
-
- if (empty(trim($_POST['lf_url']))) {
- $_POST['lf_url'] = '?' . smallHash($linkdate . $id);
- }
- $url = whitelist_protocols(trim($_POST['lf_url']), $conf->get('security.allowed_protocols'));
-
- $link = array_merge($link, [
- 'title' => trim($_POST['lf_title']),
- 'url' => $url,
- 'description' => $_POST['lf_description'],
- 'private' => (isset($_POST['lf_private']) ? 1 : 0),
- 'tags' => str_replace(',', ' ', $tags),
- ]);
-
- // If title is empty, use the URL as title.
- if ($link['title'] == '') {
- $link['title'] = $link['url'];
- }
-
- if ($conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE
- && ! is_note($link['url'])
- ) {
- $thumbnailer = new Thumbnailer($conf);
- $link['thumbnail'] = $thumbnailer->get($url);
- }
-
- $link['sticky'] = isset($link['sticky']) ? $link['sticky'] : false;
-
- $pluginManager->executeHooks('save_link', $link);
-
- $LINKSDB[$id] = $link;
- $LINKSDB->save($conf->get('resource.page_cache'));
- if ($new) {
- $history->addLink($link);
- } else {
- $history->updateLink($link);
- }
-
- // If we are called from the bookmarklet, we must close the popup:
- if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
- echo '';
- exit;
- }
-
- $returnurl = !empty($_POST['returnurl']) ? $_POST['returnurl'] : '?';
- $location = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
- // Scroll to the link which has been edited.
- $location .= '#' . $link['shorturl'];
- // After saving the link, redirect to the page the user was on.
- header('Location: '. $location);
- exit;
- }
-
- // -------- User clicked the "Cancel" button when editing a link.
- if (isset($_POST['cancel_edit'])) {
- $id = isset($_POST['lf_id']) ? (int) escape($_POST['lf_id']) : false;
- if (! isset($LINKSDB[$id])) {
- header('Location: ?');
- }
- // If we are called from the bookmarklet, we must close the popup:
- if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
- echo '';
- exit;
- }
- $link = $LINKSDB[$id];
- $returnurl = ( isset($_POST['returnurl']) ? $_POST['returnurl'] : '?' );
- // Scroll to the link which has been edited.
- $returnurl .= '#'. $link['shorturl'];
- $returnurl = generateLocation($returnurl, $_SERVER['HTTP_HOST'], array('addlink', 'post', 'edit_link'));
- header('Location: '.$returnurl); // After canceling, redirect to the page the user was on.
- exit;
- }
-
- // -------- User clicked the "Delete" button when editing a link: Delete link from database.
- if ($targetPage == Router::$PAGE_DELETELINK) {
- if (! $sessionManager->checkToken($_GET['token'])) {
- die(t('Wrong token.'));
- }
-
- $ids = trim($_GET['lf_linkdate']);
- if (strpos($ids, ' ') !== false) {
- // multiple, space-separated ids provided
- $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
- } else {
- // only a single id provided
- $ids = [$ids];
- }
- // assert at least one id is given
- if (!count($ids)) {
- die('no id provided');
- }
- foreach ($ids as $id) {
- $id = (int) escape($id);
- $link = $LINKSDB[$id];
- $pluginManager->executeHooks('delete_link', $link);
- $history->deleteLink($link);
- unset($LINKSDB[$id]);
- }
- $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
-
- // If we are called from the bookmarklet, we must close the popup:
- if (isset($_GET['source']) && ($_GET['source']=='bookmarklet' || $_GET['source']=='firefoxsocialapi')) {
- echo '';
- exit;
- }
-
- $location = '?';
- if (isset($_SERVER['HTTP_REFERER'])) {
- // Don't redirect to where we were previously if it was a permalink or an edit_link, because it would 404.
- $location = generateLocation(
- $_SERVER['HTTP_REFERER'],
- $_SERVER['HTTP_HOST'],
- ['delete_link', 'edit_link', $link['shorturl']]
- );
- }
-
- header('Location: ' . $location); // After deleting the link, redirect to appropriate location
- exit;
- }
-
- // -------- User clicked either "Set public" or "Set private" bulk operation
- if ($targetPage == Router::$PAGE_CHANGE_VISIBILITY) {
- if (! $sessionManager->checkToken($_GET['token'])) {
- die(t('Wrong token.'));
- }
-
- $ids = trim($_GET['ids']);
- if (strpos($ids, ' ') !== false) {
- // multiple, space-separated ids provided
- $ids = array_values(array_filter(preg_split('/\s+/', escape($ids))));
- } else {
- // only a single id provided
- $ids = [$ids];
- }
-
- // assert at least one id is given
- if (!count($ids)) {
- die('no id provided');
- }
- // assert that the visibility is valid
- if (!isset($_GET['newVisibility']) || !in_array($_GET['newVisibility'], ['public', 'private'])) {
- die('invalid visibility');
- } else {
- $private = $_GET['newVisibility'] === 'private';
- }
- foreach ($ids as $id) {
- $id = (int) escape($id);
- $link = $LINKSDB[$id];
- $link['private'] = $private;
- $pluginManager->executeHooks('save_link', $link);
- $LINKSDB[$id] = $link;
- }
- $LINKSDB->save($conf->get('resource.page_cache')); // save to disk
-
- $location = '?';
- if (isset($_SERVER['HTTP_REFERER'])) {
- $location = generateLocation(
- $_SERVER['HTTP_REFERER'],
- $_SERVER['HTTP_HOST']
- );
- }
- header('Location: ' . $location); // After deleting the link, redirect to appropriate location
- exit;
- }
-
- // -------- User clicked the "EDIT" button on a link: Display link edit form.
- if (isset($_GET['edit_link'])) {
- $id = (int) escape($_GET['edit_link']);
- $link = $LINKSDB[$id]; // Read database
- if (!$link) {
- header('Location: ?');
- exit;
- } // Link not found in database.
- $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
- $data = array(
- 'link' => $link,
- 'link_is_new' => false,
- 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
- 'tags' => $LINKSDB->linksCountPerTag(),
- );
- $pluginManager->executeHooks('render_editlink', $data);
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $PAGE->assign('pagetitle', t('Edit') .' '. t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('editlink');
- exit;
- }
-
- // -------- User want to post a new link: Display link edit form.
- if (isset($_GET['post'])) {
- $url = cleanup_url($_GET['post']);
-
- $link_is_new = false;
- // Check if URL is not already in database (in this case, we will edit the existing link)
- $link = $LINKSDB->getLinkFromUrl($url);
- if (! $link) {
- $link_is_new = true;
- $linkdate = strval(date(LinkDB::LINK_DATE_FORMAT));
- // Get title if it was provided in URL (by the bookmarklet).
- $title = empty($_GET['title']) ? '' : escape($_GET['title']);
- // Get description if it was provided in URL (by the bookmarklet). [Bronco added that]
- $description = empty($_GET['description']) ? '' : escape($_GET['description']);
- $tags = empty($_GET['tags']) ? '' : escape($_GET['tags']);
- $private = !empty($_GET['private']) && $_GET['private'] === "1" ? 1 : 0;
-
- // If this is an HTTP(S) link, we try go get the page to extract
- // the title (otherwise we will to straight to the edit form.)
- if (empty($title) && strpos(get_url_scheme($url), 'http') !== false) {
- $retrieveDescription = $conf->get('general.retrieve_description');
- // Short timeout to keep the application responsive
- // The callback will fill $charset and $title with data from the downloaded page.
- get_http_response(
- $url,
- $conf->get('general.download_timeout', 30),
- $conf->get('general.download_max_size', 4194304),
- get_curl_download_callback($charset, $title, $description, $tags, $retrieveDescription)
- );
- if (! empty($title) && strtolower($charset) != 'utf-8') {
- $title = mb_convert_encoding($title, 'utf-8', $charset);
- }
- }
-
- if ($url == '') {
- $url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
- $title = $conf->get('general.default_note_title', t('Note: '));
- }
- $url = escape($url);
- $title = escape($title);
-
- $link = array(
- 'linkdate' => $linkdate,
- 'title' => $title,
- 'url' => $url,
- 'description' => $description,
- 'tags' => $tags,
- 'private' => $private,
- );
- } else {
- $link['linkdate'] = $link['created']->format(LinkDB::LINK_DATE_FORMAT);
- }
-
- $data = array(
- 'link' => $link,
- 'link_is_new' => $link_is_new,
- 'http_referer' => (isset($_SERVER['HTTP_REFERER']) ? escape($_SERVER['HTTP_REFERER']) : ''),
- 'source' => (isset($_GET['source']) ? $_GET['source'] : ''),
- 'tags' => $LINKSDB->linksCountPerTag(),
- 'default_private_links' => $conf->get('privacy.default_private_links', false),
- );
- $pluginManager->executeHooks('render_editlink', $data);
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- $PAGE->assign('pagetitle', t('Shaare') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('editlink');
- exit;
- }
-
- if ($targetPage == Router::$PAGE_PINLINK) {
- if (! isset($_GET['id']) || empty($LINKSDB[$_GET['id']])) {
- // FIXME! Use a proper error system.
- $msg = t('Invalid link ID provided');
- echo '';
- exit;
- }
- if (! $sessionManager->checkToken($_GET['token'])) {
- die('Wrong token.');
- }
-
- $link = $LINKSDB[$_GET['id']];
- $link['sticky'] = ! $link['sticky'];
- $LINKSDB[(int) $_GET['id']] = $link;
- $LINKSDB->save($conf->get('resource.page_cache'));
- header('Location: '.index_url($_SERVER));
- exit;
- }
-
- if ($targetPage == Router::$PAGE_EXPORT) {
- // Export links as a Netscape Bookmarks file
-
- if (empty($_GET['selection'])) {
- $PAGE->assign('pagetitle', t('Export') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('export');
- exit;
- }
-
- // export as bookmarks_(all|private|public)_YYYYmmdd_HHMMSS.html
- $selection = $_GET['selection'];
- if (isset($_GET['prepend_note_url'])) {
- $prependNoteUrl = $_GET['prepend_note_url'];
- } else {
- $prependNoteUrl = false;
- }
-
- try {
- $PAGE->assign(
- 'links',
- NetscapeBookmarkUtils::filterAndFormat(
- $LINKSDB,
- $selection,
- $prependNoteUrl,
- index_url($_SERVER)
- )
- );
- } catch (Exception $exc) {
- header('Content-Type: text/plain; charset=utf-8');
- echo $exc->getMessage();
- exit;
- }
- $now = new DateTime();
- header('Content-Type: text/html; charset=utf-8');
- header(
- 'Content-disposition: attachment; filename=bookmarks_'
- .$selection.'_'.$now->format(LinkDB::LINK_DATE_FORMAT).'.html'
- );
- $PAGE->assign('date', $now->format(DateTime::RFC822));
- $PAGE->assign('eol', PHP_EOL);
- $PAGE->assign('selection', $selection);
- $PAGE->renderPage('export.bookmarks');
- exit;
- }
-
- if ($targetPage == Router::$PAGE_IMPORT) {
- // Upload a Netscape bookmark dump to import its contents
-
- if (! isset($_POST['token']) || ! isset($_FILES['filetoupload'])) {
- // Show import dialog
- $PAGE->assign(
- 'maxfilesize',
- get_max_upload_size(
- ini_get('post_max_size'),
- ini_get('upload_max_filesize'),
- false
- )
- );
- $PAGE->assign(
- 'maxfilesizeHuman',
- get_max_upload_size(
- ini_get('post_max_size'),
- ini_get('upload_max_filesize'),
- true
- )
- );
- $PAGE->assign('pagetitle', t('Import') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('import');
- exit;
- }
-
- // Import bookmarks from an uploaded file
- if (isset($_FILES['filetoupload']['size']) && $_FILES['filetoupload']['size'] == 0) {
- // The file is too big or some form field may be missing.
- $msg = sprintf(
- t(
- 'The file you are trying to upload is probably bigger than what this webserver can accept'
- .' (%s). Please upload in smaller chunks.'
- ),
- get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
- );
- echo '';
- exit;
- }
- if (! $sessionManager->checkToken($_POST['token'])) {
- die('Wrong token.');
- }
- $status = NetscapeBookmarkUtils::import(
- $_POST,
- $_FILES,
- $LINKSDB,
- $conf,
- $history
- );
- echo '';
- exit;
- }
-
- // Plugin administration page
- if ($targetPage == Router::$PAGE_PLUGINSADMIN) {
- $pluginMeta = $pluginManager->getPluginsMeta();
-
- // Split plugins into 2 arrays: ordered enabled plugins and disabled.
- $enabledPlugins = array_filter($pluginMeta, function ($v) {
- return $v['order'] !== false;
- });
- // Load parameters.
- $enabledPlugins = load_plugin_parameter_values($enabledPlugins, $conf->get('plugins', array()));
- uasort(
- $enabledPlugins,
- function ($a, $b) {
- return $a['order'] - $b['order'];
- }
- );
- $disabledPlugins = array_filter($pluginMeta, function ($v) {
- return $v['order'] === false;
- });
-
- $PAGE->assign('enabledPlugins', $enabledPlugins);
- $PAGE->assign('disabledPlugins', $disabledPlugins);
- $PAGE->assign('pagetitle', t('Plugin administration') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('pluginsadmin');
- exit;
- }
-
- // Plugin administration form action
- if ($targetPage == Router::$PAGE_SAVE_PLUGINSADMIN) {
- try {
- if (isset($_POST['parameters_form'])) {
- $pluginManager->executeHooks('save_plugin_parameters', $_POST);
- unset($_POST['parameters_form']);
- foreach ($_POST as $param => $value) {
- $conf->set('plugins.'. $param, escape($value));
- }
- } else {
- $conf->set('general.enabled_plugins', save_plugin_config($_POST));
- }
- $conf->write($loginManager->isLoggedIn());
- $history->updateSettings();
- } catch (Exception $e) {
- error_log(
- 'ERROR while saving plugin configuration:.' . PHP_EOL .
- $e->getMessage()
- );
-
- // TODO: do not handle exceptions/errors in JS.
- echo '';
- exit;
- }
- header('Location: ?do='. Router::$PAGE_PLUGINSADMIN);
- exit;
- }
-
- // Get a fresh token
- if ($targetPage == Router::$GET_TOKEN) {
- header('Content-Type:text/plain');
- echo $sessionManager->generateToken($conf);
- exit;
- }
-
- // -------- Thumbnails Update
- if ($targetPage == Router::$PAGE_THUMBS_UPDATE) {
- $ids = [];
- foreach ($LINKSDB as $link) {
- // A note or not HTTP(S)
- if (is_note($link['url']) || ! startsWith(strtolower($link['url']), 'http')) {
- continue;
- }
- $ids[] = $link['id'];
- }
- $PAGE->assign('ids', $ids);
- $PAGE->assign('pagetitle', t('Thumbnails update') .' - '. $conf->get('general.title', 'Shaarli'));
- $PAGE->renderPage('thumbnails');
- exit;
- }
-
- // -------- Single Thumbnail Update
- if ($targetPage == Router::$AJAX_THUMB_UPDATE) {
- if (! isset($_POST['id']) || ! ctype_digit($_POST['id'])) {
- http_response_code(400);
- exit;
- }
- $id = (int) $_POST['id'];
- if (empty($LINKSDB[$id])) {
- http_response_code(404);
- exit;
- }
- $thumbnailer = new Thumbnailer($conf);
- $link = $LINKSDB[$id];
- $link['thumbnail'] = $thumbnailer->get($link['url']);
- $LINKSDB[$id] = $link;
- $LINKSDB->save($conf->get('resource.page_cache'));
-
- echo json_encode($link);
- exit;
- }
-
- // -------- Otherwise, simply display search form and links:
- showLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager);
- exit;
-}
-
-/**
- * Template for the list of links ()
- * This function fills all the necessary fields in the $PAGE for the template 'linklist.html'
- *
- * @param pageBuilder $PAGE pageBuilder instance.
- * @param LinkDB $LINKSDB LinkDB instance.
- * @param ConfigManager $conf Configuration Manager instance.
- * @param PluginManager $pluginManager Plugin Manager instance.
- * @param LoginManager $loginManager LoginManager instance
- */
-function buildLinkList($PAGE, $LINKSDB, $conf, $pluginManager, $loginManager)
-{
- // Used in templates
- if (isset($_GET['searchtags'])) {
- if (! empty($_GET['searchtags'])) {
- $searchtags = escape(normalize_spaces($_GET['searchtags']));
- } else {
- $searchtags = false;
- }
- } else {
- $searchtags = '';
- }
- $searchterm = !empty($_GET['searchterm']) ? escape(normalize_spaces($_GET['searchterm'])) : '';
-
- // Smallhash filter
- if (! empty($_SERVER['QUERY_STRING'])
- && preg_match('/^[a-zA-Z0-9-_@]{6}($|&|#)/', $_SERVER['QUERY_STRING'])) {
- try {
- $linksToDisplay = $LINKSDB->filterHash($_SERVER['QUERY_STRING']);
- } catch (LinkNotFoundException $e) {
- $PAGE->render404($e->getMessage());
- exit;
- }
- } else {
- // Filter links according search parameters.
- $visibility = ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '';
- $request = [
- 'searchtags' => $searchtags,
- 'searchterm' => $searchterm,
- ];
- $linksToDisplay = $LINKSDB->filterSearch($request, false, $visibility, !empty($_SESSION['untaggedonly']));
- }
-
- // ---- Handle paging.
- $keys = array();
- foreach ($linksToDisplay as $key => $value) {
- $keys[] = $key;
- }
-
- // Select articles according to paging.
- $pagecount = ceil(count($keys) / $_SESSION['LINKS_PER_PAGE']);
- $pagecount = $pagecount == 0 ? 1 : $pagecount;
- $page= empty($_GET['page']) ? 1 : intval($_GET['page']);
- $page = $page < 1 ? 1 : $page;
- $page = $page > $pagecount ? $pagecount : $page;
- // Start index.
- $i = ($page-1) * $_SESSION['LINKS_PER_PAGE'];
- $end = $i + $_SESSION['LINKS_PER_PAGE'];
-
- $thumbnailsEnabled = $conf->get('thumbnails.mode', Thumbnailer::MODE_NONE) !== Thumbnailer::MODE_NONE;
- if ($thumbnailsEnabled) {
- $thumbnailer = new Thumbnailer($conf);
- }
-
- $linkDisp = array();
- while ($i<$end && $igetTimestamp();
- if (! empty($link['updated'])) {
- $link['updated_timestamp'] = $link['updated']->getTimestamp();
- } else {
- $link['updated_timestamp'] = '';
- }
- $taglist = preg_split('/\s+/', $link['tags'], -1, PREG_SPLIT_NO_EMPTY);
- uasort($taglist, 'strcasecmp');
- $link['taglist'] = $taglist;
-
- // Logged in, thumbnails enabled, not a note,
- // and (never retrieved yet or no valid cache file)
- if ($loginManager->isLoggedIn() && $thumbnailsEnabled && $link['url'][0] != '?'
- && (! isset($link['thumbnail']) || ($link['thumbnail'] !== false && ! is_file($link['thumbnail'])))
- ) {
- $elem = $LINKSDB[$keys[$i]];
- $elem['thumbnail'] = $thumbnailer->get($link['url']);
- $LINKSDB[$keys[$i]] = $elem;
- $updateDB = true;
- $link['thumbnail'] = $elem['thumbnail'];
- }
-
- // Check for both signs of a note: starting with ? and 7 chars long.
- if ($link['url'][0] === '?' && strlen($link['url']) === 7) {
- $link['url'] = index_url($_SERVER) . $link['url'];
- }
-
- $linkDisp[$keys[$i]] = $link;
- $i++;
- }
-
- // If we retrieved new thumbnails, we update the database.
- if (!empty($updateDB)) {
- $LINKSDB->save($conf->get('resource.page_cache'));
- }
-
- // Compute paging navigation
- $searchtagsUrl = $searchtags === '' ? '' : '&searchtags=' . urlencode($searchtags);
- $searchtermUrl = empty($searchterm) ? '' : '&searchterm=' . urlencode($searchterm);
- $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;
- }
-
- // Fill all template fields.
- $data = array(
- 'previous_page_url' => $previous_page_url,
- 'next_page_url' => $next_page_url,
- 'page_current' => $page,
- 'page_max' => $pagecount,
- 'result_count' => count($linksToDisplay),
- 'search_term' => $searchterm,
- 'search_tags' => $searchtags,
- 'visibility' => ! empty($_SESSION['visibility']) ? $_SESSION['visibility'] : '',
- 'links' => $linkDisp,
- );
-
- // If there is only a single link, we change on-the-fly the title of the page.
- if (count($linksToDisplay) == 1) {
- $data['pagetitle'] = $linksToDisplay[$keys[0]]['title'] .' - '. $conf->get('general.title');
- } elseif (! empty($searchterm) || ! empty($searchtags)) {
- $data['pagetitle'] = t('Search: ');
- $data['pagetitle'] .= ! empty($searchterm) ? $searchterm .' ' : '';
- $bracketWrap = function ($tag) {
- return '['. $tag .']';
- };
- $data['pagetitle'] .= ! empty($searchtags)
- ? implode(' ', array_map($bracketWrap, preg_split('/\s+/', $searchtags))).' '
- : '';
- $data['pagetitle'] .= '- '. $conf->get('general.title');
- }
-
- $pluginManager->executeHooks('render_linklist', $data, array('loggedin' => $loginManager->isLoggedIn()));
-
- foreach ($data as $key => $value) {
- $PAGE->assign($key, $value);
- }
-
- return;
-}
-
-/**
- * Installation
- * This function should NEVER be called if the file data/config.php exists.
- *
- * @param ConfigManager $conf Configuration Manager instance.
- * @param SessionManager $sessionManager SessionManager instance
- * @param LoginManager $loginManager LoginManager instance
- */
-function install($conf, $sessionManager, $loginManager)
-{
- // On free.fr host, make sure the /sessions directory exists, otherwise login will not work.
- if (endsWith($_SERVER['HTTP_HOST'], '.free.fr') && !is_dir($_SERVER['DOCUMENT_ROOT'].'/sessions')) {
- mkdir($_SERVER['DOCUMENT_ROOT'].'/sessions', 0705);
- }
-
-
- // This part makes sure sessions works correctly.
- // (Because on some hosts, session.save_path may not be set correctly,
- // or we may not have write access to it.)
- if (isset($_GET['test_session'])
- && ( !isset($_SESSION) || !isset($_SESSION['session_tested']) || $_SESSION['session_tested']!='Working')) {
- // Step 2: Check if data in session is correct.
- $msg = t(
- '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.
'
- );
- $msg = sprintf($msg, session_save_path());
- echo $msg;
- echo '
'. t('Click to try again.') .'
';
- die;
- }
- if (!isset($_SESSION['session_tested'])) {
- // Step 1 : Try to store data in session and reload page.
- $_SESSION['session_tested'] = 'Working'; // Try to set a variable in session.
- header('Location: '.index_url($_SERVER).'?test_session'); // Redirect to check stored data.
- }
- if (isset($_GET['test_session'])) {
- // Step 3: Sessions are OK. Remove test parameter from URL.
- header('Location: '.index_url($_SERVER));
- }
-
-
- if (!empty($_POST['setlogin']) && !empty($_POST['setpassword'])) {
- $tz = 'UTC';
- if (!empty($_POST['continent']) && !empty($_POST['city'])
- && isTimeZoneValid($_POST['continent'], $_POST['city'])
- ) {
- $tz = $_POST['continent'].'/'.$_POST['city'];
- }
- $conf->set('general.timezone', $tz);
- $login = $_POST['setlogin'];
- $conf->set('credentials.login', $login);
- $salt = sha1(uniqid('', true) .'_'. mt_rand());
- $conf->set('credentials.salt', $salt);
- $conf->set('credentials.hash', sha1($_POST['setpassword'] . $login . $salt));
- if (!empty($_POST['title'])) {
- $conf->set('general.title', escape($_POST['title']));
- } else {
- $conf->set('general.title', 'Shared links on '.escape(index_url($_SERVER)));
- }
- $conf->set('translation.language', escape($_POST['language']));
- $conf->set('updates.check_updates', !empty($_POST['updateCheck']));
- $conf->set('api.enabled', !empty($_POST['enableApi']));
- $conf->set(
- 'api.secret',
- generate_api_secret(
- $conf->get('credentials.login'),
- $conf->get('credentials.salt')
- )
- );
- try {
- // Everything is ok, let's create config file.
- $conf->write($loginManager->isLoggedIn());
- } catch (Exception $e) {
- error_log(
- 'ERROR while writing config file after installation.' . PHP_EOL .
- $e->getMessage()
- );
-
- // TODO: do not handle exceptions/errors in JS.
- echo '';
- exit;
- }
- echo '';
- exit;
- }
-
- $PAGE = new PageBuilder($conf, $_SESSION, null, $sessionManager->generateToken());
- list($continents, $cities) = generateTimeZoneData(timezone_identifiers_list(), date_default_timezone_get());
- $PAGE->assign('continents', $continents);
- $PAGE->assign('cities', $cities);
- $PAGE->assign('languages', Languages::getAvailableLanguages());
- $PAGE->renderPage('install');
- exit;
-}
-
-if (isset($_SERVER['QUERY_STRING']) && startsWith($_SERVER['QUERY_STRING'], 'do=dailyrss')) {
- showDailyRSS($conf, $loginManager);
- exit;
-}
-
-if (!isset($_SESSION['LINKS_PER_PAGE'])) {
- $_SESSION['LINKS_PER_PAGE'] = $conf->get('general.links_per_page', 20);
-}
-
-try {
- $history = new History($conf->get('resource.history'));
-} catch (Exception $e) {
- die($e->getMessage());
-}
-
-$linkDb = new LinkDB(
- $conf->get('resource.datastore'),
- $loginManager->isLoggedIn(),
- $conf->get('privacy.hide_public_links')
+$loginManager->checkLoginState(client_ip_id($_SERVER));
+
+$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);
-$container = new \Slim\Container();
-$container['conf'] = $conf;
-$container['plugins'] = $pluginManager;
-$container['history'] = $history;
-$app = new \Slim\App($container);
+// Main Shaarli routes
+$app->group('', function () {
+ $this->get('/install', '\Shaarli\Front\Controller\Visitor\InstallController:index')->setName('displayInstall');
+ $this->get('/install/session-test', '\Shaarli\Front\Controller\Visitor\InstallController:sessionTest');
+ $this->post('/install', '\Shaarli\Front\Controller\Visitor\InstallController:save')->setName('saveInstall');
+
+ /* -- PUBLIC --*/
+ $this->get('/', '\Shaarli\Front\Controller\Visitor\BookmarkListController:index');
+ $this->get('/shaare/{hash}', '\Shaarli\Front\Controller\Visitor\BookmarkListController:permalink');
+ $this->get('/login', '\Shaarli\Front\Controller\Visitor\LoginController:index')->setName('login');
+ $this->post('/login', '\Shaarli\Front\Controller\Visitor\LoginController:login')->setName('processLogin');
+ $this->get('/picture-wall', '\Shaarli\Front\Controller\Visitor\PictureWallController:index');
+ $this->get('/tags/cloud', '\Shaarli\Front\Controller\Visitor\TagCloudController:cloud');
+ $this->get('/tags/list', '\Shaarli\Front\Controller\Visitor\TagCloudController:list');
+ $this->get('/daily', '\Shaarli\Front\Controller\Visitor\DailyController:index');
+ $this->get('/daily-rss', '\Shaarli\Front\Controller\Visitor\DailyController:rss')->setName('rss');
+ $this->get('/feed/atom', '\Shaarli\Front\Controller\Visitor\FeedController:atom')->setName('atom');
+ $this->get('/feed/rss', '\Shaarli\Front\Controller\Visitor\FeedController:rss');
+ $this->get('/open-search', '\Shaarli\Front\Controller\Visitor\OpenSearchController:index');
+
+ $this->get('/add-tag/{newTag}', '\Shaarli\Front\Controller\Visitor\TagController:addTag');
+ $this->get('/remove-tag/{tag}', '\Shaarli\Front\Controller\Visitor\TagController:removeTag');
+ $this->get('/links-per-page', '\Shaarli\Front\Controller\Visitor\PublicSessionFilterController:linksPerPage');
+ $this->get('/untagged-only', '\Shaarli\Front\Controller\Visitor\PublicSessionFilterController:untaggedOnly');
+})->add('\Shaarli\Front\ShaarliMiddleware');
+
+$app->group('/admin', function () {
+ $this->get('/logout', '\Shaarli\Front\Controller\Admin\LogoutController:index');
+ $this->get('/tools', '\Shaarli\Front\Controller\Admin\ToolsController:index');
+ $this->get('/password', '\Shaarli\Front\Controller\Admin\PasswordController:index');
+ $this->post('/password', '\Shaarli\Front\Controller\Admin\PasswordController:change');
+ $this->get('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:index');
+ $this->post('/configure', '\Shaarli\Front\Controller\Admin\ConfigureController:save');
+ $this->get('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:index');
+ $this->post('/tags', '\Shaarli\Front\Controller\Admin\ManageTagController:save');
+ $this->post('/tags/change-separator', '\Shaarli\Front\Controller\Admin\ManageTagController:changeSeparator');
+ $this->get('/add-shaare', '\Shaarli\Front\Controller\Admin\ShaareAddController:addShaare');
+ $this->get('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateForm');
+ $this->get('/shaare/{id:[0-9]+}', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayEditForm');
+ $this->get('/shaare/private/{hash}', '\Shaarli\Front\Controller\Admin\ShaareManageController:sharePrivate');
+ $this->post('/shaare-batch', '\Shaarli\Front\Controller\Admin\ShaarePublishController:displayCreateBatchForms');
+ $this->post('/shaare', '\Shaarli\Front\Controller\Admin\ShaarePublishController:save');
+ $this->get('/shaare/delete', '\Shaarli\Front\Controller\Admin\ShaareManageController:deleteBookmark');
+ $this->get('/shaare/visibility', '\Shaarli\Front\Controller\Admin\ShaareManageController:changeVisibility');
+ $this->post('/shaare/update-tags', '\Shaarli\Front\Controller\Admin\ShaareManageController:addOrDeleteTags');
+ $this->get('/shaare/{id:[0-9]+}/pin', '\Shaarli\Front\Controller\Admin\ShaareManageController:pinBookmark');
+ $this->patch(
+ '/shaare/{id:[0-9]+}/update-thumbnail',
+ '\Shaarli\Front\Controller\Admin\ThumbnailsController:ajaxUpdate'
+ );
+ $this->get('/export', '\Shaarli\Front\Controller\Admin\ExportController:index');
+ $this->post('/export', '\Shaarli\Front\Controller\Admin\ExportController:export');
+ $this->get('/import', '\Shaarli\Front\Controller\Admin\ImportController:index');
+ $this->post('/import', '\Shaarli\Front\Controller\Admin\ImportController:import');
+ $this->get('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:index');
+ $this->post('/plugins', '\Shaarli\Front\Controller\Admin\PluginsController:save');
+ $this->get('/token', '\Shaarli\Front\Controller\Admin\TokenController:getToken');
+ $this->get('/server', '\Shaarli\Front\Controller\Admin\ServerController:index');
+ $this->get('/clear-cache', '\Shaarli\Front\Controller\Admin\ServerController:clearCache');
+ $this->get('/thumbnails', '\Shaarli\Front\Controller\Admin\ThumbnailsController:index');
+ $this->get('/metadata', '\Shaarli\Front\Controller\Admin\MetadataController:ajaxRetrieveTitle');
+ $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 () {
@@ -1973,21 +198,12 @@ function install($conf, $sessionManager, $loginManager)
$this->get('/history', '\Shaarli\Api\Controllers\HistoryController:getHistory')->setName('getHistory');
})->add('\Shaarli\Api\ApiMiddleware');
-$response = $app->run(true);
-
-// Hack to make Slim and Shaarli router work together:
-// If a Slim route isn't found and NOT API call, we call renderPage().
-if ($response->getStatusCode() == 404 && strpos($_SERVER['REQUEST_URI'], '/api/v1') === false) {
- // We use UTF-8 for proper international characters handling.
- header('Content-Type: text/html; charset=utf-8');
- renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager, $loginManager);
-} else {
- $response = $response
- ->withHeader('Access-Control-Allow-Origin', '*')
- ->withHeader(
- 'Access-Control-Allow-Headers',
- 'X-Requested-With, Content-Type, Accept, Origin, Authorization'
- )
- ->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+try {
+ $response = $app->run(true);
$app->respond($response);
+} catch (Throwable $e) {
+ die(nl2br(
+ 'An unexpected error happened, and the error template could not be displayed.' . PHP_EOL . PHP_EOL .
+ exception2text($e)
+ ));
}
diff --git a/init.php b/init.php
new file mode 100644
index 00000000..367ecc15
--- /dev/null
+++ b/init.php
@@ -0,0 +1,86 @@
+getMessage();
+ exit;
+}
+
+// Force cookie path (but do not change lifetime)
+$cookie = session_get_cookie_params();
+$cookiedir = '';
+if (dirname($_SERVER['SCRIPT_NAME']) != '/') {
+ $cookiedir = dirname($_SERVER["SCRIPT_NAME"]).'/';
+}
+// Set default cookie expiration and path.
+session_set_cookie_params($cookie['lifetime'], $cookiedir, $_SERVER['SERVER_NAME']);
+// Set session parameters on server side.
+// Use cookies to store session.
+ini_set('session.use_cookies', 1);
+// Force cookies for session (phpsessionID forbidden in URL).
+ini_set('session.use_only_cookies', 1);
+// Prevent PHP form using sessionID in URL if cookies are disabled.
+ini_set('session.use_trans_sid', false);
+
+define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
+define('SHAARLI_MUTEX_FILE', __FILE__);
+
+session_name('shaarli');
+// Start session if needed (Some server auto-start sessions).
+if (session_status() == PHP_SESSION_NONE) {
+ session_start();
+}
+
+// Regenerate session ID if invalid or not defined in cookie.
+if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
+ session_regenerate_id(true);
+ $_COOKIE['shaarli'] = session_id();
+}
+
+// LC_MESSAGES isn't defined without php-intl, in this case use LC_COLLATE locale instead.
+if (! defined('LC_MESSAGES')) {
+ define('LC_MESSAGES', LC_COLLATE);
+}
+
+// Prevent caching on client side or proxy: (yes, it's ugly)
+header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
+header("Cache-Control: no-store, no-cache, must-revalidate");
+header("Cache-Control: post-check=0, pre-check=0", false);
+header("Pragma: no-cache");
diff --git a/package.json b/package.json
index f3d9b51e..b879b223 100644
--- a/package.json
+++ b/package.json
@@ -7,26 +7,28 @@
"awesomplete": "^1.1.2",
"blazy": "^1.8.2",
"fork-awesome": "^1.1.7",
+ "he": "^1.2.0",
"pure-extras": "^1.0.0",
"purecss": "^1.0.0"
},
"devDependencies": {
- "babel-core": "^6.26.0",
- "babel-loader": "^7.1.2",
- "babel-minify-webpack-plugin": "^0.2.0",
- "babel-preset-env": "^1.6.1",
- "css-loader": "^0.28.9",
- "eslint": "^4.16.0",
- "eslint-config-airbnb-base": "^12.1.0",
- "eslint-plugin-import": "^2.8.0",
- "extract-text-webpack-plugin": "^3.0.2",
+ "@babel/core": "^7.11.6",
+ "@babel/preset-env": "^7.11.5",
+ "babel-loader": "^8.1.0",
+ "css-loader": "^4.3.0",
+ "eslint": "^7.9.0",
+ "eslint-config-airbnb-base": "^14.2.0",
+ "eslint-plugin-import": "^2.22.0",
"file-loader": "^1.1.6",
- "node-sass": "^4.12.0",
- "sass-lint": "^1.12.1",
- "sass-loader": "^6.0.6",
- "style-loader": "^0.19.1",
- "url-loader": "^0.6.2",
- "webpack": "^3.10.0"
+ "mini-css-extract-plugin": "^0.11.2",
+ "sass": "^1.26.11",
+ "sass-loader": "^10.0.2",
+ "stylelint": "^13.7.1",
+ "stylelint-config-standard": "^20.0.0",
+ "stylelint-scss": "^3.18.0",
+ "terser-webpack-plugin": "^4.2.2",
+ "webpack": "^4.44.2",
+ "webpack-cli": "^3.3.12"
},
"scripts": {
"build": "webpack",
diff --git a/phpcs.xml b/phpcs.xml
index 29b95d56..a2749b57 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -12,6 +12,18 @@
-
-
+
+
+
+
+
+ index.php
+ plugins/*
+ tests/bootstrap.php
+ tests/utils/RainTPL.php
+
+
+
+ tests/utils/RainTPL.php
+
diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php
index 8bf4ed46..80b1dd95 100644
--- a/plugins/addlink_toolbar/addlink_toolbar.php
+++ b/plugins/addlink_toolbar/addlink_toolbar.php
@@ -5,7 +5,7 @@
* Adds the addlink input on the linklist page.
*/
-use Shaarli\Router;
+use Shaarli\Render\TemplatePage;
/**
* When linklist is displayed, add play videos to header's toolbar.
@@ -16,27 +16,27 @@
*/
function hook_addlink_toolbar_render_header($data)
{
- if ($data['_PAGE_'] == Router::$PAGE_LINKLIST && $data['_LOGGEDIN_'] === true) {
- $form = array(
- 'attr' => array(
+ if ($data['_PAGE_'] == TemplatePage::LINKLIST && $data['_LOGGEDIN_'] === true) {
+ $form = [
+ 'attr' => [
'method' => 'GET',
- 'action' => '',
+ 'action' => $data['_BASE_PATH_'] . '/admin/shaare',
'name' => 'addform',
'class' => 'addform',
- ),
- 'inputs' => array(
- array(
+ ],
+ 'inputs' => [
+ [
'type' => 'text',
'name' => 'post',
'placeholder' => t('URI'),
- ),
- array(
+ ],
+ [
'type' => 'submit',
'value' => t('Add link'),
'class' => 'bigbutton',
- ),
- ),
- );
+ ],
+ ],
+ ];
$data['fields_toolbar'][] = $form;
}
diff --git a/plugins/archiveorg/archiveorg.html b/plugins/archiveorg/archiveorg.html
index ad501f47..e37d887e 100644
--- a/plugins/archiveorg/archiveorg.html
+++ b/plugins/archiveorg/archiveorg.html
@@ -1,5 +1,5 @@
-
+
diff --git a/plugins/archiveorg/archiveorg.php b/plugins/archiveorg/archiveorg.php
index 0ee1c73c..88f2b653 100644
--- a/plugins/archiveorg/archiveorg.php
+++ b/plugins/archiveorg/archiveorg.php
@@ -1,4 +1,5 @@
get('plugins.'. $placeholder, ''));
+ $value = trim($conf->get('plugins.' . $placeholder, ''));
+ if (strlen($value) > 0) {
+ $params[$placeholder] = $value;
+ }
}
if (empty($params)) {
- $error = t('Default colors plugin error: '.
+ $error = t('Default colors plugin error: ' .
'This plugin is active and no custom color is configured.');
- return array($error);
+ return [$error];
+ }
+
+ // Colors are defined but the custom CSS file does not exist -> generate it
+ if (!file_exists(PluginManager::$PLUGINS_PATH . DEFAULT_COLORS_CSS_FILE)) {
+ default_colors_generate_css_file($params);
}
}
/**
* When plugin parameters are saved, we regenerate the custom CSS file with provided settings.
*
- * @param array $data $_POST array
+ * @param array $data $_POST array
*
* @return array Updated $_POST array
*/
function hook_default_colors_save_plugin_parameters($data)
{
- $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css';
- $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css.template');
- $content = '';
- foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) {
- $content .= ! empty($data[$rule])
- ? default_colors_format_css_rule($data, $rule) .';'. PHP_EOL
- : '';
- }
-
- if (! empty($content)) {
- file_put_contents($file, sprintf($template, $content));
- }
+ default_colors_generate_css_file($data);
return $data;
}
@@ -71,13 +70,34 @@ function hook_default_colors_save_plugin_parameters($data)
function hook_default_colors_render_includes($data)
{
$file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css';
- if (file_exists($file )) {
+ if (file_exists($file)) {
$data['css_files'][] = $file ;
}
return $data;
}
+/**
+ * Regenerate the custom CSS file with provided settings.
+ *
+ * @param array $params Plugin configuration (CSS rules)
+ */
+function default_colors_generate_css_file($params): void
+{
+ $file = PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css';
+ $template = file_get_contents(PluginManager::$PLUGINS_PATH . '/default_colors/default_colors.css.template');
+ $content = '';
+ foreach (DEFAULT_COLORS_PLACEHOLDERS as $rule) {
+ $content .= !empty($params[$rule])
+ ? default_colors_format_css_rule($params, $rule) . ';' . PHP_EOL
+ : '';
+ }
+
+ if (! empty($content)) {
+ file_put_contents($file, sprintf($template, $content));
+ }
+}
+
/**
* Create a valid CSS rule from parameters settings and plugin parameter.
*
@@ -93,8 +113,8 @@ function default_colors_format_css_rule($data, $parameter)
}
$key = str_replace('DEFAULT_COLORS_', '', $parameter);
- $key = str_replace('_', '-', strtolower($key)) .'-color';
- return ' --'. $key .': '. $data[$parameter];
+ $key = str_replace('_', '-', strtolower($key)) . '-color';
+ return ' --' . $key . ': ' . $data[$parameter];
}
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 71ba7495..d89765cf 100644
--- a/plugins/demo_plugin/demo_plugin.php
+++ b/plugins/demo_plugin/demo_plugin.php
@@ -1,11 +1,14 @@
'GET',
+ 'route' => '/custom',
+ 'callable' => 'Shaarli\DemoPlugin\DemoPluginController:index',
+ ],
+ ];
+}
+
/**
* Hook render_header.
- * Executed on every page redering.
+ * Executed on every page render.
*
* Template placeholders:
* - buttons_toolbar
@@ -74,7 +89,7 @@ function demo_plugin_init($conf)
function hook_demo_plugin_render_header($data)
{
// Only execute when linklist is rendered.
- if ($data['_PAGE_'] == Router::$PAGE_LINKLIST) {
+ if ($data['_PAGE_'] == TemplatePage::LINKLIST) {
// If loggedin
if ($data['_LOGGEDIN_'] === true) {
/*
@@ -82,14 +97,14 @@ function hook_demo_plugin_render_header($data)
* A link is an array of its attributes (key="value"),
* and a mandatory `html` key, which contains its value.
*/
- $button = array(
- 'attr' => array (
+ $button = [
+ 'attr' => [
'href' => '#',
'class' => 'mybutton',
'title' => 'hover me',
- ),
+ ],
'html' => 'DEMO buttons toolbar',
- );
+ ];
$data['buttons_toolbar'][] = $button;
}
@@ -115,29 +130,29 @@ function hook_demo_plugin_render_header($data)
*
*
*/
- $form = array(
- 'attr' => array(
+ $form = [
+ 'attr' => [
'method' => 'GET',
- 'action' => '?',
+ 'action' => $data['_BASE_PATH_'] . '/',
'class' => 'addform',
- ),
- 'inputs' => array(
- array(
+ ],
+ 'inputs' => [
+ [
'type' => 'text',
'name' => 'demo',
'placeholder' => 'demo',
- )
- )
- );
+ ]
+ ]
+ ];
$data['fields_toolbar'][] = $form;
}
// Another button always displayed
- $button = array(
- 'attr' => array(
+ $button = [
+ 'attr' => [
'href' => '#',
- ),
+ ],
'html' => 'Demo',
- );
+ ];
$data['buttons_toolbar'][] = $button;
return $data;
@@ -145,7 +160,7 @@ function hook_demo_plugin_render_header($data)
/**
* Hook render_includes.
- * Executed on every page redering.
+ * Executed on every page render.
*
* Template placeholders:
* - css_files
@@ -169,7 +184,7 @@ function hook_demo_plugin_render_includes($data)
/**
* Hook render_footer.
- * Executed on every page redering.
+ * Executed on every page render.
*
* Template placeholders:
* - text
@@ -186,8 +201,8 @@ function hook_demo_plugin_render_includes($data)
*/
function hook_demo_plugin_render_footer($data)
{
- // footer text
- $data['text'][] = '
'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
+ // Footer text
+ $data['text'][] = '
' . demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
// Free elements at the end of the page.
$data['endofpage'][] = '