--- /dev/null
+# EditorConfig: http://EditorConfig.org
+
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+indent_style = space
+indent_size = 4
+
+[*.{htaccess,html,xml}]
+indent_size = 2
+
+[*.php]
+max_line_length = 100
+
+[Dockerfile]
+max_line_length = 80
+
+[Makefile]
+indent_style = tab
*.ttf binary
*.min.css binary
*.min.js binary
+*.mo binary
# Exclude from Git archives
+.editorconfig export-ignore
.gitattributes export-ignore
.github export-ignore
.gitignore export-ignore
VirtualTam <virtualtam@flibidi.net> <tamisier.aurelien@gmail.com>
VirtualTam <virtualtam@flibidi.net> <virtualtam+github@flibidi.net>
VirtualTam <virtualtam@flibidi.net> <virtualtam@flibidi.org>
+Willi Eggeling <thewilli@gmail.com> <mail@wje-online.de>
+Willi Eggeling <thewilli@gmail.com> <thewilli@users.noreply.github.com>
# Release archives
*.tar.gz
*.zip
+inc/languages/*/LC_MESSAGES/shaarli.mo
# Development and test resources
coverage
sudo: false
-dist: precise
+dist: trusty
language: php
-addons:
- apt:
- packages:
- - locales
- - language-pack-de
- - language-pack-fr
cache:
directories:
- $HOME/.composer/cache
install:
- composer self-update
- composer install --prefer-dist
+ - locale -a
+before_script:
+ - PATH=${PATH//:\.\/node_modules\/\.bin/}
script:
- make clean
- make check_permissions
- 518 ArthurHoaro <arthur@hoa.ro>
- 231 VirtualTam <virtualtam@flibidi.net>
- 147 nodiscc <nodiscc@gmail.com>
+ 537 ArthurHoaro <arthur@hoa.ro>
+ 252 VirtualTam <virtualtam@flibidi.net>
+ 148 nodiscc <nodiscc@gmail.com>
56 Sébastien Sauvage <sebsauvage@sebsauvage.net>
15 Florian Eula <eula.florian@gmail.com>
13 Emilien Klein <emilien@klein.st>
12 Nicolas Danelon <hi@nicolasmd.com.ar>
+ 9 Willi Eggeling <thewilli@gmail.com>
8 Christophe HENRY <christophe.henry@sbgodin.fr>
+ 6 B. van Berkum <dev@dotmpe.com>
5 Lucas Cimon <lucas.cimon@gmail.com>
4 Alexandre Alapetite <alexandre@alapetite.fr>
4 David Sferruzza <david.sferruzza@gmail.com>
1 Kevin Canévet <kevin@streamroot.io>
1 Knah Tsaeb <knah-tsaeb@knah-tsaeb.org>
1 Lionel Martin <renarddesmers@gmail.com>
+ 1 Mark Gerarts <mark.gerarts@gmail.com>
1 Marsup <marsup@gmail.com>
1 Sbgodin <Sbgodin@users.noreply.github.com>
1 TsT <tst2005@gmail.com>
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [v0.9.2](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2) - 2017-10-07
+
+**Major security issue fixed. Please update.**
+
+### Added
+- Tag search now supports wildcards `*`
+- New setting `privacy.force_login` which can be used with `privacy.hide_public_links` to redirect anonymous users to the login page.
+- New setting `general.default_note_title` used to override default `Note:` title prefix for notes.
+- Add a version hash for asset loading to prevent browser's cache issue
+
+### Changed
+- The "Remember me" checkbox is unchecked by default
+- The default value of the "Remember me" checkbox can be configured under `data/config.json.php`
+
+### Removed
+- Remove obsolete PHP magic quote support
+
+### Fixed
+- Generates a permalink URL if the URL is set to blank
+- Replace links to the old GitHub wiki with ReadTheDocs URIs
+- Use single quotes in the note bookmarklet
+- Daily page if there is no link
+- Bulk link deletion with a single link
+- HTTPS detection behind a reverse proxy
+- Travis tests environment and localization
+- Improve template paths robustness (trailing slash)
+- Robustness: safer gzinflate/zlib usage
+- Description links parsing with parenthesis (without Markdown)
+- Templates:
+ - Sort the tag cloud alphabetically
+ - Firefox social title
+ - Improved visited link color
+ - Fix jumpy textarea with long content in post edit
+
+### Security
+
+- Fixed reflected XSS vulnerability introduced in v0.9.1, discovered by @chb9 ([CVE-2017-15215](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15215)).
+
## [v0.9.1](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1) - 2017-08-23
The documentation has been migrated to ReadTheDocs:
all: static_analysis_summary check_permissions test
+##
+# Docker test adapter
+#
+# Shaarli sources and vendored libraries are copied from a shared volume
+# to a user-owned directory to enable running tests as a non-root user.
+##
+docker_%:
+ rsync -az /shaarli/ ~/shaarli/
+ cd ~/shaarli && make $*
+
##
# Concise status of the project
# These targets are non-blocking: || exit 0
@echo "----------------------"
@echo "Check file permissions"
@echo "----------------------"
- @for file in `git ls-files`; do \
+ @for file in `git ls-files | grep -v docker`; do \
if [ -x $$file ]; then \
errors=true; \
echo "$${file} is executable"; \
# See phpunit.xml for configuration
# https://phpunit.de/manual/current/en/appendixes.configuration.html
##
-test:
+test: translate
@echo "-------"
@echo "PHPUNIT"
@echo "-------"
@mkdir -p sandbox coverage
- @$(BIN)/phpunit --coverage-php coverage/main.cov --testsuite unit-tests
+ @$(BIN)/phpunit --coverage-php coverage/main.cov --bootstrap tests/bootstrap.php --testsuite unit-tests
locale_test_%:
@UT_LOCALE=$*.utf8 \
composer install --no-dev --prefer-dist
find vendor/ -name ".git" -type d -exec rm -rf {} +
-### generate a release tarball and include 3rd-party dependencies
-release_tar: composer_dependencies htmldoc
+### generate a release tarball and include 3rd-party dependencies and translations
+release_tar: composer_dependencies htmldoc translate
git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).tar HEAD
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^vendor|$(ARCHIVE_PREFIX)vendor|" vendor/
tar rvf $(ARCHIVE_VERSION).tar --transform "s|^doc/html|$(ARCHIVE_PREFIX)doc/html|" doc/html/
gzip $(ARCHIVE_VERSION).tar
-### generate a release zip and include 3rd-party dependencies
-release_zip: composer_dependencies htmldoc
+### generate a release zip and include 3rd-party dependencies and translations
+release_zip: composer_dependencies htmldoc translate
git archive --prefix=$(ARCHIVE_PREFIX) -o $(ARCHIVE_VERSION).zip -9 HEAD
mkdir -p $(ARCHIVE_PREFIX)/{doc,vendor}
rsync -a doc/html/ $(ARCHIVE_PREFIX)doc/html/
mkdocs build'
find doc/html/ -type f -exec chmod a-x '{}' \;
rm -r venv
+
+
+### Generate Shaarli's translation compiled file (.mo)
+translate:
+ @find inc/languages/ -name shaarli.po -execdir msgfmt shaarli.po -o shaarli.mo \;
\ No newline at end of file
[![](https://img.shields.io/badge/stable-v0.8.4-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.8.4)
[![](https://img.shields.io/travis/shaarli/Shaarli/stable.svg?label=stable)](https://travis-ci.org/shaarli/Shaarli)
•
-[![](https://img.shields.io/badge/latest-v0.9.1-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.1)
+[![](https://img.shields.io/badge/latest-v0.9.2-blue.svg)](https://github.com/shaarli/Shaarli/releases/tag/v0.9.2)
[![](https://img.shields.io/travis/shaarli/Shaarli/latest.svg?label=latest)](https://travis-ci.org/shaarli/Shaarli)
•
[![](https://img.shields.io/badge/master-v0.9.x-blue.svg)](https://github.com/shaarli/Shaarli)
public static function checkPHPVersion($minVersion, $curVersion)
{
if (version_compare($curVersion, $minVersion) < 0) {
- throw new Exception(
+ $msg = t(
'Your PHP version is obsolete!'
- .' Shaarli requires at least PHP '.$minVersion.', and thus cannot run.'
- .' Your PHP version has known security vulnerabilities and should be'
- .' updated as soon as possible.'
+ . ' 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));
}
}
public static function checkResourcePermissions($conf)
{
$errors = array();
+ $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
// Check script and template directories are readable
foreach (array(
'application',
'inc',
'plugins',
- $conf->get('resource.raintpl_tpl'),
- $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme'),
+ $rainTplDir,
+ $rainTplDir.'/'.$conf->get('resource.theme'),
) as $path) {
if (! is_readable(realpath($path))) {
- $errors[] = '"'.$path.'" directory is not readable';
+ $errors[] = '"'.$path.'" '. t('directory is not readable');
}
}
$conf->get('resource.raintpl_tmp'),
) as $path) {
if (! is_readable(realpath($path))) {
- $errors[] = '"'.$path.'" directory is not readable';
+ $errors[] = '"'.$path.'" '. t('directory is not readable');
}
if (! is_writable(realpath($path))) {
- $errors[] = '"'.$path.'" directory is not writable';
+ $errors[] = '"'.$path.'" '. t('directory is not writable');
}
}
}
if (! is_readable(realpath($path))) {
- $errors[] = '"'.$path.'" file is not readable';
+ $errors[] = '"'.$path.'" '. t('file is not readable');
}
if (! is_writable(realpath($path))) {
- $errors[] = '"'.$path.'" file is not writable';
+ $errors[] = '"'.$path.'" '. t('file is not writable');
}
}
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);
+ }
}
function purgeCachedPages($pageCacheDir)
{
if (! is_dir($pageCacheDir)) {
- $error = 'Cannot purge '.$pageCacheDir.': no directory';
+ $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
error_log($error);
return $error;
}
$link['url'] = $pageaddr . $link['url'];
}
if ($this->usePermalinks === true) {
- $permalink = '<a href="'. $link['url'] .'" title="Direct link">Direct link</a>';
+ $permalink = '<a href="'. $link['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
} else {
- $permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>';
+ $permalink = '<a href="'. $link['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
}
$link['description'] = format_description($link['description'], '', $pageaddr);
$link['description'] .= PHP_EOL .'<br>— '. $permalink;
/**
* Read data from a file containing Shaarli database format content.
- * If the file isn't readable or doesn't exists, default data will be returned.
+ *
+ * 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.
{
// Note that gzinflate is faster than gzuncompress.
// See: http://www.php.net/manual/en/function.gzdeflate.php#96439
- if (is_readable($file)) {
- return unserialize(
- gzinflate(
- base64_decode(
- substr(file_get_contents($file), strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
- )
- )
- );
+ if (! is_readable($file)) {
+ return $default;
+ }
+
+ $data = file_get_contents($file);
+ if ($data == '') {
+ return $default;
}
- return $default;
+ return unserialize(
+ gzinflate(
+ base64_decode(
+ substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
+ )
+ )
+ );
}
}
* - UPDATED: link updated
* - DELETED: link deleted
* - SETTINGS: the settings have been updated through the UI.
+ * - IMPORT: bulk links import
*
* Note: new events are put at the beginning of the file and history array.
*/
*/
const SETTINGS = 'SETTINGS';
+ /**
+ * @var string Action key: a bulk import has been processed.
+ */
+ const IMPORT = 'IMPORT';
+
/**
* @var string History file path.
*/
$this->addEvent(self::SETTINGS);
}
+ /**
+ * Add Event: bulk import.
+ *
+ * Note: we don't store links add/update one by one since it can have a huge impact on performances.
+ */
+ public function importLinks()
+ {
+ $this->addEvent(self::IMPORT);
+ }
+
/**
* Save a new event and write it in the history file.
*
}
if (! is_writable($this->historyFilePath)) {
- throw new Exception('History file isn\'t readable or writable');
+ throw new Exception(t('History file isn\'t readable or writable'));
}
}
{
$this->history = FileUtils::readFlatDB($this->historyFilePath, []);
if ($this->history === false) {
- throw new Exception('Could not parse history file');
+ throw new Exception(t('Could not parse history file'));
}
}
<?php
+namespace Shaarli;
+
+use Gettext\GettextTranslator;
+use Gettext\Merge;
+use Gettext\Translations;
+use Gettext\Translator;
+use Gettext\TranslatorInterface;
+use Shaarli\Config\ConfigManager;
+
/**
- * Wrapper function for translation which match the API
- * of gettext()/_() and ngettext().
+ * Class Languages
+ *
+ * Load Shaarli translations using 'gettext/gettext'.
+ * This class allows to either use PHP gettext extension, or a PHP implementation of gettext,
+ * with a fixed language, or dynamically using autoLocale().
*
- * Not doing translation for now.
+ * Translation files PO/MO files follow gettext standard and must be placed under:
+ * <translation path>/<language>/LC_MESSAGES/<domain>.[po|mo]
*
- * @param string $text Text to translate.
- * @param string $nText The plural message ID.
- * @param int $nb The number of items for plural forms.
+ * Pros/cons:
+ * - gettext extension is faster
+ * - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded)
*
- * @return String Text translated.
+ * Settings:
+ * - translation.mode:
+ * - auto: use default setting (PHP implementation)
+ * - php: use PHP implementation
+ * - gettext: use gettext wrapper
+ * - translation.language:
+ * - auto: use autoLocale() and the language change according to user HTTP headers
+ * - fixed language: e.g. 'fr'
+ * - translation.extensions:
+ * - domain => translation_path: allow plugins and themes to extend the defaut extension
+ * The domain must be unique, and translation path must be relative, and contains the tree mentioned above.
+ *
+ * @package Shaarli
*/
-function t($text, $nText = '', $nb = 0) {
- if (empty($nText)) {
- return $text;
+class Languages
+{
+ /**
+ * Core translations domain
+ */
+ const DEFAULT_DOMAIN = 'shaarli';
+
+ /**
+ * @var TranslatorInterface
+ */
+ protected $translator;
+
+ /**
+ * @var string
+ */
+ protected $language;
+
+ /**
+ * @var ConfigManager
+ */
+ protected $conf;
+
+ /**
+ * Languages constructor.
+ *
+ * @param string $language lang determined by autoLocale(), can be overridden.
+ * @param ConfigManager $conf instance.
+ */
+ public function __construct($language, $conf)
+ {
+ $this->conf = $conf;
+ $confLanguage = $this->conf->get('translation.language', 'auto');
+ if ($confLanguage === 'auto' || ! $this->isValidLanguage($confLanguage)) {
+ $this->language = substr($language, 0, 5);
+ } else {
+ $this->language = $confLanguage;
+ }
+
+ if (! extension_loaded('gettext')
+ || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
+ ) {
+ $this->initPhpTranslator();
+ } else {
+ $this->initGettextTranslator();
+ }
+
+ // Register default functions (e.g. '__()') to use our Translator
+ $this->translator->register();
+ }
+
+ /**
+ * Initialize the translator using php gettext extension (gettext dependency act as a wrapper).
+ */
+ protected function initGettextTranslator ()
+ {
+ $this->translator = new GettextTranslator();
+ $this->translator->setLanguage($this->language);
+ $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
+
+ foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
+ if ($domain !== self::DEFAULT_DOMAIN) {
+ $this->translator->loadDomain($domain, $translationPath, false);
+ }
+ }
+ }
+
+ /**
+ * Initialize the translator using a PHP implementation of gettext.
+ *
+ * Note that if language po file doesn't exist, errors are ignored (e.g. not installed language).
+ */
+ protected function initPhpTranslator()
+ {
+ $this->translator = new Translator();
+ $translations = new Translations();
+ // Core translations
+ try {
+ /** @var Translations $translations */
+ $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
+ $translations->setDomain('shaarli');
+ $this->translator->loadTranslations($translations);
+ } catch (\InvalidArgumentException $e) {}
+
+
+ // Extension translations (plugins, themes, etc.).
+ foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
+ if ($domain === self::DEFAULT_DOMAIN) {
+ continue;
+ }
+
+ try {
+ /** @var Translations $extension */
+ $extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po');
+ $extension->setDomain($domain);
+ $this->translator->loadTranslations($extension);
+ } catch (\InvalidArgumentException $e) {}
+ }
+ }
+
+ /**
+ * Checks if a language string is valid.
+ *
+ * @param string $language e.g. 'fr' or 'en_US'
+ *
+ * @return bool true if valid, false otherwise
+ */
+ protected function isValidLanguage($language)
+ {
+ return preg_match('/^[a-z]{2}(_[A-Z]{2})?/', $language) === 1;
+ }
+
+ /**
+ * Get the list of available languages for Shaarli.
+ *
+ * @return array List of available languages, with their label.
+ */
+ public static function getAvailableLanguages()
+ {
+ return [
+ 'auto' => t('Automatic'),
+ 'en' => t('English'),
+ 'fr' => t('French'),
+ ];
}
- $actualForm = $nb > 1 ? $nText : $text;
- return sprintf($actualForm, $nb);
}
{
// TODO: use exceptions instead of "die"
if (!$this->loggedIn) {
- die('You are not authorized to add a link.');
+ die(t('You are not authorized to add a link.'));
}
if (!isset($value['id']) || empty($value['url'])) {
- die('Internal Error: A link should always have an id and 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('You must specify an integer as a key.');
+ die(t('You must specify an integer as a key.'));
}
if ($offset !== null && $offset !== $value['id']) {
- die('Array offset and link ID must be equal.');
+ die(t('Array offset and link ID must be equal.'));
}
// If the link exists, we reuse the real offset, otherwise new entry
$this->links = array();
$link = array(
'id' => 1,
- 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone',
+ 'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'),
'url'=>'https://shaarli.readthedocs.io',
- 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
+ '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 "Help/documentation" at the bottom of this page.
+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.',
+You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'),
'private'=>0,
'created'=> new DateTime(),
'tags'=>'opensource software'
$link = array(
'id' => 0,
- 'title'=>'My secret stuff... - Pastebin.com',
+ 'title'=> t('My secret stuff... - Pastebin.com'),
'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
- 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.',
+ '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',
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
*
*/
public function filterTags($tags, $casesensitive = false, $visibility = 'all')
{
- // Implode if array for clean up.
- $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags;
- if (empty($tags)) {
+ // 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);
}
- $searchtags = self::tagsStrToArray($tags, $casesensitive);
- $filtered = array();
- if (empty($searchtags)) {
- return $filtered;
+ // build regex from all tags
+ $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
+ if(!$casesensitive) {
+ // make regex case insensitive
+ $re .= 'i';
}
+ // create resulting array
+ $filtered = array();
+
+ // iterate over each link
foreach ($this->links as $key => $link) {
- // ignore non private links when 'privatonly' is on.
+ // check level of visibility
+ // ignore non private links when 'privateonly' is on.
if ($visibility !== 'all') {
if (! $link['private'] && $visibility === 'private') {
continue;
continue;
}
}
-
- $linktags = self::tagsStrToArray($link['tags'], $casesensitive);
-
- $found = true;
- for ($i = 0 ; $i < count($searchtags) && $found; $i++) {
- // Exclusive search, quit if tag found.
- // Or, tag not found in the link, quit.
- if (($searchtags[$i][0] == '-'
- && $this->searchTagAndHashTag(substr($searchtags[$i], 1), $linktags, $link['description']))
- || ($searchtags[$i][0] != '-')
- && ! $this->searchTagAndHashTag($searchtags[$i], $linktags, $link['description'])
- ) {
- $found = false;
+ $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 = array();
+ // find all tags in the form of #tag in the description
+ preg_match_all(
+ '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
+ $link['description'],
+ $descTags
+ );
+ if(count($descTags[1])){
+ // there were some tags in the description, add them to the search string
+ $search .= ' ' . implode(' ', $descTags[1]);
}
+ };
+ // match regular expression with search string
+ if(!preg_match($re, $search)){
+ // this entry does _not_ match our regex
+ continue;
}
-
- if ($found) {
- $filtered[$key] = $link;
- }
+ $filtered[$key] = $link;
}
return $filtered;
}
return array_reverse($filtered, true);
}
- /**
- * Check if a tag is found in the taglist, or as an hashtag in the link description.
- *
- * @param string $tag Tag to search.
- * @param array $taglist List of tags for the current link.
- * @param string $description Link description.
- *
- * @return bool True if found, false otherwise.
- */
- protected function searchTagAndHashTag($tag, $taglist, $description)
- {
- if (in_array($tag, $taglist)) {
- return true;
- }
-
- if (preg_match('/(^| )#'. $tag .'([^'. self::$HASHTAG_CHARS .']|$)/mui', $description) > 0) {
- return true;
- }
-
- return false;
- }
-
/**
* Convert a list of tags (str) to an array. Also
* - handle case sensitivity.
class LinkNotFoundException extends Exception
{
- protected $message = 'The link you are trying to reach does not exist or has been deleted.';
+ /**
+ * LinkNotFoundException constructor.
+ */
+ public function __construct()
+ {
+ $this->message = t('The link you are trying to reach does not exist or has been deleted.');
+ }
}
*/
function text2clickable($text, $redirector = '')
{
- $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si';
+ $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
if (empty($redirector)) {
return preg_replace($regex, '<a href="$1">$1</a>', $text);
{
// see tpl/export.html for possible values
if (! in_array($selection, array('all', 'public', 'private'))) {
- throw new Exception('Invalid export selection: "'.$selection.'"');
+ throw new Exception(t('Invalid export selection:') .' "'.$selection.'"');
}
$bookmarkLinks = array();
-
foreach ($linkDb as $link) {
if ($link['private'] != 0 && $selection == 'public') {
continue;
* @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 $duration how many seconds did the import take
*
* @return string Summary of the bookmark import status
*/
$filesize,
$importCount=0,
$overwriteCount=0,
- $skipCount=0
+ $skipCount=0,
+ $duration=0
)
{
- $status = 'File '.$filename.' ('.$filesize.' bytes) ';
+ $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
- $status .= 'has an unknown file format. Nothing was imported.';
+ $status .= t('has an unknown file format. Nothing was imported.');
} else {
- $status .= 'was successfully processed: '.$importCount.' links imported, ';
- $status .= $overwriteCount.' links overwritten, ';
- $status .= $skipCount.' links skipped.';
+ $status .= vsprintf(
+ t('was successfully processed in %d seconds: %d links imported, %d links overwritten, %d links skipped.'),
+ [$duration, $importCount, $overwriteCount, $skipCount]
+ );
}
return $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']);
$linkDb[$existingLink['id']] = $newLink;
$importCount++;
$overwriteCount++;
- $history->updateLink($newLink);
continue;
}
$newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
$linkDb[$newLink['id']] = $newLink;
$importCount++;
- $history->addLink($newLink);
}
$linkDb->save($conf->get('resource.page_cache'));
+ $history->importLinks();
+
+ $duration = time() - $start;
return self::importStatus(
$filename,
$filesize,
$importCount,
$overwriteCount,
- $skipCount
+ $skipCount,
+ $duration
);
}
}
*
* @param ConfigManager $conf Configuration Manager instance (reference).
* @param LinkDB $linkDB instance.
+ * @param string $token Session token
*/
- public function __construct(&$conf, $linkDB = null)
+ public function __construct(&$conf, $linkDB = null, $token = null)
{
$this->tpl = false;
$this->conf = $conf;
$this->linkDB = $linkDB;
+ $this->token = $token;
}
/**
try {
$version = ApplicationUtils::checkUpdate(
- shaarli_version,
+ SHAARLI_VERSION,
$this->conf->get('resource.update_check'),
$this->conf->get('updates.check_updates_interval'),
$this->conf->get('updates.check_updates'),
}
$this->tpl->assign('searchcrits', $searchcrits);
$this->tpl->assign('source', index_url($_SERVER));
- $this->tpl->assign('version', shaarli_version);
+ $this->tpl->assign('version', SHAARLI_VERSION);
+ $this->tpl->assign(
+ 'version_hash',
+ ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt'))
+ );
$this->tpl->assign('scripturl', index_url($_SERVER));
$this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links?
$this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly']));
$this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true));
$this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss');
$this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false));
- $this->tpl->assign('token', getToken($this->conf));
+ $this->tpl->assign('token', $this->token);
+
if ($this->linkDB !== null) {
$this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
}
*
* @param string $message A messate to display what is not found
*/
- public function render404($message = 'The page you are trying to reach does not exist or has been deleted.')
+ public function render404($message = '')
{
- header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found');
+ 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');
}
$metaData[$plugin] = parse_ini_file($metaFile);
$metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
+ if (isset($metaData[$plugin]['description'])) {
+ $metaData[$plugin]['description'] = t($metaData[$plugin]['description']);
+ }
// Read parameters and format them into an array.
if (isset($metaData[$plugin]['parameters'])) {
$params = explode(';', $metaData[$plugin]['parameters']);
$metaData[$plugin]['parameters'][$param]['value'] = '';
// Optional parameter description in parameter.PARAM_NAME=
if (isset($metaData[$plugin]['parameter.'. $param])) {
- $metaData[$plugin]['parameters'][$param]['desc'] = $metaData[$plugin]['parameter.'. $param];
+ $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.'. $param]);
}
}
}
*/
public function __construct($pluginName)
{
- $this->message = 'Plugin "'. $pluginName .'" files not found.';
+ $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
}
}
--- /dev/null
+<?php
+namespace Shaarli;
+
+/**
+ * Manages the server-side session
+ */
+class SessionManager
+{
+ protected $session = [];
+
+ /**
+ * Constructor
+ *
+ * @param array $session The $_SESSION array (reference)
+ * @param ConfigManager $conf ConfigManager instance (reference)
+ */
+ public function __construct(& $session, & $conf)
+ {
+ $this->session = &$session;
+ $this->conf = &$conf;
+ }
+
+ /**
+ * Generates a session token
+ *
+ * @return string token
+ */
+ public function generateToken()
+ {
+ $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
+ $this->session['tokens'][$token] = 1;
+ return $token;
+ }
+
+ /**
+ * Checks the validity of a session token, and destroys it afterwards
+ *
+ * @param string $token The token to check
+ *
+ * @return bool true if the token is valid, else false
+ */
+ public function checkToken($token)
+ {
+ if (! isset($this->session['tokens'][$token])) {
+ // the token is wrong, or has already been used
+ return false;
+ }
+
+ // destroy the token to prevent future use
+ unset($this->session['tokens'][$token]);
+ return true;
+ }
+
+ /**
+ * Validate session ID to prevent Full Path Disclosure.
+ *
+ * See #298.
+ * The session ID's format depends on the hash algorithm set in PHP settings
+ *
+ * @param string $sessionId Session ID
+ *
+ * @return true if valid, false otherwise.
+ *
+ * @see http://php.net/manual/en/function.hash-algos.php
+ * @see http://php.net/manual/en/session.configuration.php
+ */
+ public static function checkId($sessionId)
+ {
+ if (empty($sessionId)) {
+ return false;
+ }
+
+ if (!$sessionId) {
+ return false;
+ }
+
+ if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
+ return false;
+ }
+
+ return true;
+ }
+}
*/
public static function getThemes($tplDir)
{
+ $tplDir = rtrim($tplDir, '/');
$allTheme = glob($tplDir.'/*', GLOB_ONLYDIR);
$themes = [];
foreach ($allTheme as $value) {
}
if ($this->methods === null) {
- throw new UpdaterException('Couldn\'t retrieve Updater class methods.');
+ throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.'));
}
foreach ($this->methods as $method) {
*/
public function updateMethodCheckUpdateRemoteBranch()
{
- if (shaarli_version === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
+ if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
return true;
}
$latestMajor = $matches[1];
// Get current major version digit
- preg_match('/(\d+)\.\d+$/', shaarli_version, $matches);
+ preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
$currentMajor = $matches[1];
if ($currentMajor === $latestMajor) {
}
if (! empty($this->method)) {
- $out .= 'An error occurred while running the update '. $this->method . PHP_EOL;
+ $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
}
if (! empty($this->previous)) {
function write_updates_file($updatesFilepath, $updates)
{
if (empty($updatesFilepath)) {
- throw new Exception('Updates file path is not set, can\'t write updates.');
+ throw new Exception(t('Updates file path is not set, can\'t write updates.'));
}
$res = file_put_contents($updatesFilepath, implode(';', $updates));
if ($res === false) {
- throw new Exception('Unable to write updates in '. $updatesFilepath . '.');
+ throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
}
}
return $finalReferer;
}
-/**
- * Validate session ID to prevent Full Path Disclosure.
- *
- * See #298.
- * The session ID's format depends on the hash algorithm set in PHP settings
- *
- * @param string $sessionId Session ID
- *
- * @return true if valid, false otherwise.
- *
- * @see http://php.net/manual/en/function.hash-algos.php
- * @see http://php.net/manual/en/session.configuration.php
- */
-function is_session_id_valid($sessionId)
-{
- if (empty($sessionId)) {
- return false;
- }
-
- if (!$sessionId) {
- return false;
- }
-
- if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
- return false;
- }
-
- return true;
-}
-
/**
* Sniff browser language to set the locale automatically.
* Note that is may not work on your server if the corresponding locale is not installed.
*/
function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
{
- $callback = function($a, $b) use ($reverse) {
+ $callback = function ($a, $b) use ($reverse) {
// Collator is part of PHP intl.
if (class_exists('Collator')) {
$collator = new Collator(setlocale(LC_COLLATE, 0));
usort($data, $callback);
}
}
+
+/**
+ * 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).
+ *
+ * @return string Text translated.
+ */
+function t($text, $nText = '', $nb = 1, $domain = 'shaarli') {
+ return dn__($domain, $text, $nText, $nb);
+}
$data = json_decode($data, true);
if ($data === null) {
$errorCode = json_last_error();
- $error = 'An error occurred while parsing JSON configuration file ('. $filepath .'): error code #';
- $error .= $errorCode. '<br>➜ <code>' . json_last_error_msg() .'</code>';
+ $error = sprintf(
+ 'An error occurred while parsing JSON configuration file (%s): error code #%d',
+ $filepath,
+ $errorCode
+ );
+ $error .= '<br>➜ <code>' . json_last_error_msg() .'</code>';
if ($errorCode === JSON_ERROR_SYNTAX) {
- $error .= '<br>Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
+ $error .= '<br>';
+ $error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
$error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.';
}
throw new \Exception($error);
if (!file_put_contents($filepath, $data)) {
throw new \IOException(
$filepath,
- 'Shaarli could not create the config file.
- Please make sure Shaarli has the right to write in the folder is it installed in.'
+ t('Shaarli could not create the config file. '.
+ 'Please make sure Shaarli has the right to write in the folder is it installed in.')
);
}
}
public function set($setting, $value, $write = false, $isLoggedIn = false)
{
if (empty($setting) || ! is_string($setting)) {
- throw new \Exception('Invalid setting key parameter. String expected, got: '. gettype($setting));
+ throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
}
// During the ConfigIO transition, map legacy settings to the new ones.
$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('updates.check_updates', false);
$this->setEmpty('updates.check_updates_branch', 'stable');
$this->setEmpty('privacy.default_private_links', false);
$this->setEmpty('privacy.hide_public_links', false);
+ $this->setEmpty('privacy.force_login', false);
$this->setEmpty('privacy.hide_timestamps', false);
// default state of the 'remember me' checkbox of the login form
$this->setEmpty('privacy.remember_user_default', true);
$this->setEmpty('redirector.url', '');
$this->setEmpty('redirector.encode_url', true);
+ $this->setEmpty('translation.language', 'auto');
+ $this->setEmpty('translation.mode', 'php');
+ $this->setEmpty('translation.extensions', []);
+
$this->setEmpty('plugins', array());
}
) {
throw new \IOException(
$filepath,
- 'Shaarli could not create the config file.
- Please make sure Shaarli has the right to write in the folder is it installed in.'
+ t('Shaarli could not create the config file. '.
+ 'Please make sure Shaarli has the right to write in the folder is it installed in.')
);
}
}
public function __construct($field)
{
$this->field = $field;
- $this->message = 'Configuration value is required for '. $this->field;
+ $this->message = sprintf(t('Configuration value is required for %s'), $this->field);
}
}
*/
public function __construct()
{
- $this->message = 'An error occurred while trying to save plugins loading order.';
+ $this->message = t('An error occurred while trying to save plugins loading order.');
}
}
*/
public function __construct()
{
- $this->message = 'You are not authorized to alter config.';
+ $this->message = t('You are not authorized to alter config.');
}
}
public function __construct($path, $message = '')
{
$this->path = $path;
- $this->message = empty($message) ? 'Error accessing' : $message;
+ $this->message = empty($message) ? t('Error accessing') : $message;
$this->message .= ' "' . $this->path .'"';
}
}
"shaarli/netscape-bookmark-parser": "^2.0",
"erusev/parsedown": "1.6",
"slim/slim": "^3.0",
- "pubsubhubbub/publisher": "dev-master"
+ "pubsubhubbub/publisher": "dev-master",
+ "gettext/gettext": "^4.4"
},
"require-dev": {
"phpmd/phpmd" : "@stable",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
- "content-hash": "68beedbfa104c788029b079800cfd6e8",
+ "content-hash": "13b7e1e474fe9264b098ba86face0feb",
"packages": [
{
"name": "container-interop/container-interop",
],
"time": "2015-10-04T16:44:32+00:00"
},
+ {
+ "name": "gettext/gettext",
+ "version": "v4.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/oscarotero/Gettext.git",
+ "reference": "4f57f004635cc6311a20815ebfdc0757cb337113"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/oscarotero/Gettext/zipball/4f57f004635cc6311a20815ebfdc0757cb337113",
+ "reference": "4f57f004635cc6311a20815ebfdc0757cb337113",
+ "shasum": ""
+ },
+ "require": {
+ "gettext/languages": "^2.3",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "illuminate/view": "*",
+ "phpunit/phpunit": "^4.8|^5.7",
+ "squizlabs/php_codesniffer": "^3.0",
+ "symfony/yaml": "~2",
+ "twig/extensions": "*",
+ "twig/twig": "^1.31|^2.0"
+ },
+ "suggest": {
+ "illuminate/view": "Is necessary if you want to use the Blade extractor",
+ "symfony/yaml": "Is necessary if you want to use the Yaml extractor/generator",
+ "twig/extensions": "Is necessary if you want to use the Twig extractor",
+ "twig/twig": "Is necessary if you want to use the Twig extractor"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Gettext\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Oscar Otero",
+ "email": "oom@oscarotero.com",
+ "homepage": "http://oscarotero.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "PHP gettext manager",
+ "homepage": "https://github.com/oscarotero/Gettext",
+ "keywords": [
+ "JS",
+ "gettext",
+ "i18n",
+ "mo",
+ "po",
+ "translation"
+ ],
+ "time": "2017-08-09T16:59:46+00:00"
+ },
+ {
+ "name": "gettext/languages",
+ "version": "2.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mlocati/cldr-to-gettext-plural-rules.git",
+ "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mlocati/cldr-to-gettext-plural-rules/zipball/49c39e51569963cc917a924b489e7025bfb9d8c7",
+ "reference": "49c39e51569963cc917a924b489e7025bfb9d8c7",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4"
+ },
+ "bin": [
+ "bin/export-plural-rules",
+ "bin/export-plural-rules.php"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Gettext\\Languages\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Michele Locati",
+ "email": "mlocati@gmail.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "gettext languages with plural rules",
+ "homepage": "https://github.com/mlocati/cldr-to-gettext-plural-rules",
+ "keywords": [
+ "cldr",
+ "i18n",
+ "internationalization",
+ "l10n",
+ "language",
+ "languages",
+ "localization",
+ "php",
+ "plural",
+ "plural rules",
+ "plurals",
+ "translate",
+ "translations",
+ "unicode"
+ ],
+ "time": "2017-03-23T17:02:28+00:00"
+ },
{
"name": "katzgrau/klogger",
"version": "1.2.1",
"source": {
"type": "git",
"url": "https://github.com/pubsubhubbub/php-publisher.git",
- "reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7"
+ "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/a5d6a0e1cc9d49101c3904480e5b06cbb8addba7",
- "reference": "a5d6a0e1cc9d49101c3904480e5b06cbb8addba7",
+ "url": "https://api.github.com/repos/pubsubhubbub/php-publisher/zipball/0d224daebd504ab61c22fee4db58f8d1fc18945f",
+ "reference": "0d224daebd504ab61c22fee4db58f8d1fc18945f",
"shasum": ""
},
"require": {
"publishers",
"pubsubhubbub"
],
- "time": "2016-11-15T06:24:01+00:00"
+ "time": "2017-10-08T10:59:41+00:00"
},
{
"name": "shaarli/netscape-bookmark-parser",
},
{
"name": "phpdocumentor/reflection-common",
- "version": "1.0",
+ "version": "1.0.1",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionCommon.git",
- "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c"
+ "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
- "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
+ "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6",
"shasum": ""
},
"require": {
"reflection",
"static analysis"
],
- "time": "2015-12-27T11:43:31+00:00"
+ "time": "2017-09-11T18:02:19+00:00"
},
{
"name": "phpdocumentor/reflection-docblock",
- "version": "3.2.1",
+ "version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
- "reference": "183824db76118b9dddffc7e522b91fa175f75119"
+ "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/183824db76118b9dddffc7e522b91fa175f75119",
- "reference": "183824db76118b9dddffc7e522b91fa175f75119",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157",
+ "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157",
"shasum": ""
},
"require": {
}
],
"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-08-04T20:55:59+00:00"
+ "time": "2017-08-08T06:39:58+00:00"
},
{
"name": "phpdocumentor/type-resolver",
},
{
"name": "phpspec/prophecy",
- "version": "v1.7.0",
+ "version": "v1.7.2",
"source": {
"type": "git",
"url": "https://github.com/phpspec/prophecy.git",
- "reference": "93d39f1f7f9326d746203c7c056f300f7f126073"
+ "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073",
- "reference": "93d39f1f7f9326d746203c7c056f300f7f126073",
+ "url": "https://api.github.com/repos/phpspec/prophecy/zipball/c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
+ "reference": "c9b8c6088acd19d769d4cc0ffa60a9fe34344bd6",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": "^5.3|^7.0",
- "phpdocumentor/reflection-docblock": "^2.0|^3.0.2",
+ "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0",
"sebastian/comparator": "^1.1|^2.0",
"sebastian/recursion-context": "^1.0|^2.0|^3.0"
},
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.6.x-dev"
+ "dev-master": "1.7.x-dev"
}
},
"autoload": {
"spy",
"stub"
],
- "time": "2017-03-02T20:05:34+00:00"
+ "time": "2017-09-04T11:05:03+00:00"
},
{
"name": "phpunit/php-code-coverage",
},
{
"name": "symfony/config",
- "version": "v3.3.6",
+ "version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
- "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297"
+ "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297",
- "reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297",
+ "url": "https://api.github.com/repos/symfony/config/zipball/4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
+ "reference": "4ab62407bff9cd97c410a7feaef04c375aaa5cfd",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
+ "php": "^5.5.9|>=7.0.8",
"symfony/filesystem": "~2.8|~3.0"
},
"conflict": {
],
"description": "Symfony Config Component",
"homepage": "https://symfony.com",
- "time": "2017-07-19T07:37:29+00:00"
+ "time": "2017-10-04T18:56:58+00:00"
},
{
"name": "symfony/console",
- "version": "v2.8.26",
+ "version": "v2.8.28",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd"
+ "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/32a3c6b3398de5db8ed381f4ef92970c59c2fcdd",
- "reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd",
+ "url": "https://api.github.com/repos/symfony/console/zipball/f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
+ "reference": "f81549d2c5fdee8d711c9ab3c7e7362353ea5853",
"shasum": ""
},
"require": {
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
- "time": "2017-07-29T21:26:04+00:00"
+ "time": "2017-10-01T21:00:16+00:00"
},
{
"name": "symfony/debug",
},
{
"name": "symfony/dependency-injection",
- "version": "v3.3.6",
+ "version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
- "reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0"
+ "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8d70987f991481e809c63681ffe8ce3f3fde68a0",
- "reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0",
+ "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/8ebad929aee3ca185b05f55d9cc5521670821ad1",
+ "reference": "8ebad929aee3ca185b05f55d9cc5521670821ad1",
"shasum": ""
},
"require": {
- "php": ">=5.5.9",
+ "php": "^5.5.9|>=7.0.8",
"psr/container": "^1.0"
},
"conflict": {
],
"description": "Symfony DependencyInjection Component",
"homepage": "https://symfony.com",
- "time": "2017-07-28T15:27:31+00:00"
+ "time": "2017-10-04T17:15:30+00:00"
},
{
"name": "symfony/filesystem",
- "version": "v3.3.6",
+ "version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "427987eb4eed764c3b6e38d52a0f87989e010676"
+ "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676",
- "reference": "427987eb4eed764c3b6e38d52a0f87989e010676",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/90bc45abf02ae6b7deb43895c1052cb0038506f1",
+ "reference": "90bc45abf02ae6b7deb43895c1052cb0038506f1",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^5.5.9|>=7.0.8"
},
"type": "library",
"extra": {
],
"description": "Symfony Filesystem Component",
"homepage": "https://symfony.com",
- "time": "2017-07-11T07:17:58+00:00"
+ "time": "2017-10-03T13:33:10+00:00"
},
{
"name": "symfony/finder",
- "version": "v3.3.6",
+ "version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
+ "reference": "773e19a491d97926f236942484cb541560ce862d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4",
- "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/773e19a491d97926f236942484cb541560ce862d",
+ "reference": "773e19a491d97926f236942484cb541560ce862d",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^5.5.9|>=7.0.8"
},
"type": "library",
"extra": {
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
- "time": "2017-06-01T21:01:25+00:00"
+ "time": "2017-10-02T06:42:24+00:00"
},
{
"name": "symfony/polyfill-mbstring",
- "version": "v1.4.0",
+ "version": "v1.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
- "reference": "f29dca382a6485c3cbe6379f0c61230167681937"
+ "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937",
- "reference": "f29dca382a6485c3cbe6379f0c61230167681937",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
+ "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296",
"shasum": ""
},
"require": {
"type": "library",
"extra": {
"branch-alias": {
- "dev-master": "1.4-dev"
+ "dev-master": "1.6-dev"
}
},
"autoload": {
"portable",
"shim"
],
- "time": "2017-06-09T14:24:12+00:00"
+ "time": "2017-10-11T12:05:26+00:00"
},
{
"name": "symfony/yaml",
- "version": "v3.3.6",
+ "version": "v3.3.10",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
- "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed"
+ "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
- "reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
+ "reference": "8c7bf1e7d5d6b05a690b715729cb4cd0c0a99c46",
"shasum": ""
},
"require": {
- "php": ">=5.5.9"
+ "php": "^5.5.9|>=7.0.8"
},
"require-dev": {
"symfony/console": "~2.8|~3.0"
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
- "time": "2017-07-23T12:43:26+00:00"
+ "time": "2017-10-05T14:43:42+00:00"
},
{
"name": "theseer/fdomdocument",
<IfModule version_module>
<IfVersion >= 2.4>
- Require all denied
+ Require all denied
+ <Files "user.css">
+ Require all granted
+ </Files>
</IfVersion>
<IfVersion < 2.4>
- Allow from none
- Deny from all
+ Allow from none
+ Deny from all
+ <Files "user.css">
+ Allow from all
+ </Files>
</IfVersion>
</IfModule>
Also, please make sure your server meets the [requirements](Server-requirements)
and is properly [configured](Server-configuration).
-Several releases are available:
+Multiple releases branches are available:
+
+- latest (last release)
+- stable (previous major release)
+- master (development)
+
+Using one of the following methods:
- by downloading full release archives including all dependencies
- by downloading Github archives
- by cloning the Git repository
+- using Docker: [see the documentation](docker/shaarli-images)
---
$ mv Shaarli /path/to/shaarli/
```
-In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page. Cloning using `git` or downloading Github branches as zip files requires additional steps (see below).|
+In most cases, download Shaarli from the [releases](https://github.com/shaarli/Shaarli/releases) page.
+Cloning using `git` or downloading Github branches as zip files requires additional steps (see below).
### Using git
```
$ mkdir -p /path/to/shaarli && cd /path/to/shaarli/
-$ git clone -b v0.9 https://github.com/shaarli/Shaarli.git .
+$ git clone -b latest https://github.com/shaarli/Shaarli.git .
$ composer install --no-dev --prefer-dist
+$ make translate
```
## Stable version
# install/update third-party dependencies
$ cd /path/to/shaarli
$ composer install --no-dev --prefer-dist
+$ make translate
```
## Finish Installation
Once Shaarli is downloaded and files have been placed at the correct location, open it this location your favorite browser.
-![install screenshot](http://i.imgur.com/wuMpDSN.png)
+![install screenshot](images/install-shaarli.png)
Setup your Shaarli installation, and it's ready to use!
[`php-gd`](http://php.net/manual/en/book.image.php) | optional | thumbnail resizing
[`php-intl`](http://php.net/manual/en/book.intl.php) | optional | localized text sorting (e.g. `e->è->f`)
[`php-curl`](http://php.net/manual/en/book.curl.php) | optional | using cURL for fetching webpages and thumbnails in a more robust way
+[`php-gettext`](http://php.net/manual/en/book.gettext.php) | optional | Use the translation system in gettext mode (faster)
- **links_per_page**: Number of shaares displayed per page.
- **timezone**: See [the list of supported timezones](http://php.net/manual/en/timezones.php).
- **enabled_plugins**: List of enabled plugins.
+- **default_note_title**: Default title of a new note.
### Security
- **page_cache**: Shaarli's internal cache directory.
- **ban_file**: Banned IP file path.
+### Translation
+
+- **language**: translation language (also see [Translations](Translations))
+ - **auto** (default): The translation language is chosen from the browser locale.
+ It means that the language can be different for 2 different visitors depending on their locale.
+ - **en**: Use the English translation.
+ - **fr**: Use the French translation.
+- **mode**:
+ - **auto** or **php** (default): Use the PHP implementation of gettext (slower)
+ - **gettext**: Use PHP builtin gettext extension
+ (faster, but requires `php-gettext` to be installed and to reload the web server on update)
+- **extension**: Translation extensions for custom themes or plugins.
+Must be an associative array: `translation domain => translation path`.
+
### Updates
- **check_updates**: Enable or disable update check to the git repository.
- **default_private_links**: Check the private checkbox by default for every new link.
- **hide_public_links**: All links are hidden while logged out.
+- **force_login**: if **hide_public_links** and this are set to `true`, all anonymous users are redirected to the login page.
- **hide_timestamps**: Timestamps are hidden.
- **remember_user_default**: Default state of the login page's *remember me* checkbox
- `true`: checked by default, `false`: unchecked by default
"privacy": {
"default_private_links": true,
"hide_public_links": false,
+ "force_login": false,
"hide_timestamps": false,
"remember_user_default": true
},
"plugins": {
"WALLABAG_URL": "http://demo.wallabag.org",
"WALLABAG_VERSION": "1"
+ },
+ "translation": {
+ "language": "fr",
+ "mode": "php",
+ "extensions": {
+ "demo": "plugins/demo_plugin/languages/"
+ }
}
} ?>
```
--- /dev/null
+## Translations
+
+Shaarli supports [gettext](https://www.gnu.org/software/gettext/manual/gettext.html) translations
+since `>= v0.9.2`.
+
+Note that only the `default` theme supports translations.
+
+### Contributing
+
+We encourage the community to contribute to Shaarli's translation either by improving existing
+translations or submitting a new language.
+
+Contributing to the translation does not require development skill.
+
+Please submit a pull request with the `.po` file updated/created. Note that the compiled file (`.mo`)
+is not stored on the repository, and is generated during the release process.
+
+### How to
+
+First, install [Poedit](https://poedit.net/) tool.
+
+Poedit will extract strings to translate from the PHP source code.
+
+**Important**: due to the usage of a template engine, it's important to generate PHP cache files to extract
+every translatable string.
+
+You can either use [this script](https://gist.github.com/ArthurHoaro/5d0323f758ab2401ef444a53f54e9a07) (recommended)
+or visit every template page in your browser to generate cache files, while logged in.
+
+Here is a list :
+
+```
+http://<replace_domain>/
+http://<replace_domain>/?nonope
+http://<replace_domain>/?do=addlink
+http://<replace_domain>/?do=changepasswd
+http://<replace_domain>/?do=changetag
+http://<replace_domain>/?do=configure
+http://<replace_domain>/?do=tools
+http://<replace_domain>/?do=daily
+http://<replace_domain>/?post
+http://<replace_domain>/?do=export
+http://<replace_domain>/?do=import
+http://<replace_domain>/?do=login
+http://<replace_domain>/?do=picwall
+http://<replace_domain>/?do=pluginadmin
+http://<replace_domain>/?do=tagcloud
+http://<replace_domain>/?do=taglist
+```
+
+#### Improve existing translation
+
+In Poedit, click on "Edit a Translation", and from Shaarli's directory open
+`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
+
+The existing list of translatable strings should have been loaded, then click on the "Update" button.
+
+You can start editing the translation.
+
+![poedit-screenshot](images/poedit-1.jpg)
+
+Save when you're done, then you can submit a pull request containing the updated `shaarli.po`.
+
+#### Add a new language
+
+Open Poedit and select "Create New Translation", then from Shaarli's directory open
+`inc/languages/<lang>/LC_MESSAGES/shaarli.po`.
+
+Then select the language you want to create.
+
+Click on `File > Save as...`, and save your file in `<shaarli directory>/inc/language/<new language>/LC_MESSAGES/shaarli.po`.
+`<new language>` here should be the language code respecting the [ISO 3166-1 alpha-2](https://en.wikipedia.org/wiki/ISO_3166-2)
+format in lowercase (e.g. `de` for German).
+
+Then click on the "Update" button, and you can start to translate every available string.
+
+Save when you're done, then you can submit a pull request containing the new `shaarli.po`.
+
+### Extend Shaarli's translation
+
+If you're writing a custom theme, or a non official plugin, you might want to use the translation system,
+but you won't be able to able to override Shaarli's translation.
+
+However, you can add your own translation domain which extends the main translation list.
+
+> Note that you can find a live example of translation extension in the `demo_plugin`.
+
+First, create your translation files tree directory:
+
+```
+<your_module>/languages/<ISO 3166-1 alpha-2 language code>/LC_MESSAGES/
+```
+
+Your `.po` files must be named like your domain. E.g. if your translation domain is `my_theme`, then your file will be
+`my_theme.po`.
+
+Users have to register your extension in their configuration with the parameter
+`translation.extensions.<domain>: <translation files path>`.
+
+Example:
+
+```php
+if (! $conf->exists('translation.extensions.my_theme')) {
+ $conf->set('translation.extensions.my_theme', '<your_module>/languages/');
+ $conf->write(true);
+}
+```
+
+> Note that the page needs to be reloaded after the registration.
+
+It is then recommended to create a custom translation function which will call the `t()` function with your domain.
+For example :
+
+```php
+function my_theme_t($text, $nText = '', $nb = 1)
+{
+ return t($text, $nText, $nb, 'my_theme'); // the last parameter is your translation domain.
+}
+```
+
+All strings which can be translated should be processed through your function:
+
+```php
+my_theme_t('Comment');
+my_theme_t('Comment', 'Comments', 2);
+```
+
+Or in templates:
+
+```php
+{'Comment'|my_theme_t}
+{function="my_theme_t('Comment', 'Comments', 2)"}
+```
+
+> Note than in template, you need to visit your page at least once to generate a cache file.
+
+When you're done, open Poedit and load translation strings from sources:
+
+ 1. `File > New`
+ 2. Choose your language
+ 3. Save your `PO` file in `<your_module>/languages/<language code>/LC_MESSAGES/my_theme.po`.
+ 4. Go to `Catalog > Properties...`
+ 5. Fill the `Translation Properties` tab
+ 6. Add your source path in the `Sources Paths` tab
+ 7. In the `Sources Keywords` tab uncheck "Also use default keywords" and add the following lines:
+
+```
+my_theme_t
+my_theme_t:1,2
+```
+
+Click on the "Update" button and you're free to start your translations!
--- /dev/null
+## Running tests inside Docker containers
+
+Read first:
+
+- [Docker 101](docker/docker-101.md)
+- [Docker resources](docker/resources.md)
+- [Unit tests](Unit-tests.md)
+
+### Docker test images
+
+Test Dockerfiles are located under `docker/tests/<distribution>/Dockerfile`,
+and can be used to build Docker images to run Shaarli test suites under common
+Linux environments.
+
+Dockerfiles are provided for the following environments:
+
+- `alpine36` - [Alpine 3.6](https://www.alpinelinux.org/downloads/)
+- `debian8` - [Debian 8 Jessie](https://www.debian.org/DebianJessie) (oldstable)
+- `debian9` - [Debian 9 Stretch](https://wiki.debian.org/DebianStretch) (stable)
+- `ubuntu16` - [Ubuntu 16.04 Xenial Xerus](http://releases.ubuntu.com/16.04/) (LTS)
+
+What's behind the curtains:
+
+- each image provides:
+ - a base Linux OS
+ - Shaarli PHP dependencies (OS packages)
+ - test PHP dependencies (OS packages)
+ - Composer
+- the local workspace is mapped to the container's `/shaarli/` directory,
+- the files are rsync'd to so tests are run using a standard Linux user account
+ (running tests as `root` would bypass permission checks and may hide issues)
+- the tests are run inside the container.
+
+### Building test images
+
+```bash
+# build the Debian 9 Docker image
+$ cd /path/to/shaarli
+$ cd docker/test/debian9
+$ docker build -t shaarli-test:debian9 .
+```
+
+### Running tests
+
+```bash
+$ cd /path/to/shaarli
+
+# install/update 3rd-party test dependencies
+$ composer install --prefer-dist
+
+# run tests using the freshly built image
+$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_test
+
+# run the full test campaign
+$ docker run -v $PWD:/shaarli shaarli-test:debian9 docker_all_tests
+```
- `data/ipbans.php` - banned IP addresses
- `data/updates.txt` - contains all automatic update to the configuration and datastore files already run
-See [Shaarli configuration](Shaarli configuration) for more information about Shaarli resources.
+See [Shaarli configuration](Shaarli-configuration) for more information about Shaarli resources.
It is recommended to backup this repository _before_ starting updating/upgrading Shaarli:
- backup the `data` directory
- install or update Shaarli:
- - fresh installation - see [Download and installation](Download and installation)
+ - fresh installation - see [Download and installation](Download-and-installation)
- update - see the following sections
- check or restore the `data` directory
All tagged revisions can be downloaded as tarballs or ZIP archives from the [releases](https://github.com/shaarli/Shaarli/releases) page.
-We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download and installation) for `git` complete instructions.
+We recommend that you use the latest release tarball with the `-full` suffix. It contains the dependencies, please read [Download and installation](Download-and-installation) for `git` complete instructions.
Once downloaded, extract the archive locally and update your remote installation (e.g. via FTP) -be sure you keep the content of the `data` directory!
+If you use translations in gettext mode - meaning you manually changed the default mode -,
+reload your web server.
+
After upgrading, access your fresh Shaarli installation from a web browser; the configuration and data store will then be automatically updated, and new settings added to `data/config.json.php` (see [Shaarli configuration](Shaarli configuration) for more details).
## Upgrading with Git
Downloading: 100%
```
+Shaarli >= `v0.9.2` supports translations:
+
+```bash
+$ make translate
+```
+
+If you use translations in gettext mode, reload your web server.
+
### Migrating and upgrading from Sebsauvage's repository
If you have installed Shaarli from [Sebsauvage's original Git repository](https://github.com/sebsauvage/Shaarli), you can use [Git remotes](https://git-scm.com/book/en/v2/Git-Basics-Working-with-Remotes) to update your working copy.
Downloading: 100%
```
+Shaarli >= `v0.9.2` supports translations:
+
+```bash
+$ make translate
+```
+
+If you use translations in gettext mode, reload your web server.
+
Optionally, you can delete information related to the legacy version:
```bash
#### Step 3: configuration
-After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli configuration) for more details).
+After migrating, access your fresh Shaarli installation from a web browser; the configuration will then be automatically updated, and new settings added to `data/config.php` (see [Shaarli configuration](Shaarli-configuration) for more details).
## Troubleshooting
Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe
Status: Downloaded newer image for debian:wheezy
```
+
+Docker re-uses layers already downloaded. In other words if you have images based on Alpine or some Ubuntu version for example, those can share disk space.
+
+### Start a container
+A container is an instance created from an image, that can be run and that keeps running until its main process exits. Or until the user stops the container.
+
+The simplest way to start a container from image is ``docker run``. It also pulls the image for you if it is not locally available. For more advanced use, refer to ``docker create``.
+
+Stopped containers are not destroyed, unless you specify ``--rm``. To view all created, running and stopped containers, enter:
+```bash
+$ docker ps -a
+```
+
+Some containers may be designed or configured to be restarted, others are not. Also remember both network ports and volumes of a container are created on start, and not editable later.
+
+### Access a running container
+A running container is accessible using ``docker exec``, or ``docker copy``. You can use ``exec`` to start a root shell in the Shaarli container:
+```bash
+$ docker exec -ti <container-name-or-id> bash
+```
+Note the names and ID's of containers are listed in ``docker ps``. You can even type only one or two letters of the ID, given they are unique.
+
+Access can also be through one or more network ports, or disk volumes. Both are specified on and fixed on ``docker create`` or ``run``.
+
+You can view the console output of the main container process too:
+```bash
+$ docker logs -f <container-name-or-id>
+```
+
+### Docker disk use
+Trying out different images can fill some gigabytes of disk quickly. Besides images, the docker volumes usually take up most disk space.
+
+If you care only about trying out docker and not about what is running or saved, the following commands should help you out quickly if you run low on disk space:
+
+```bash
+$ docker rmi -f $(docker images -aq) # remove or mark all images for disposal
+$ docker volume rm $(docker volume ls -q) # remove all volumes
+```
+
+### Systemd config
+Systemd is the process manager of choice on Debian-based distributions. Once you have a ``docker`` service installed, you can use the following steps to set up Shaarli to run on system start.
+
+```bash
+systemctl enable /etc/systemd/system/docker.shaarli.service
+systemctl start docker.shaarli
+systemctl status docker.*
+journalctl -f # inspect system log if needed
+```
+
+You will need sudo or a root terminal to perform some or all of the steps above. Here are the contents for the service file:
+```
+[Unit]
+Description=Shaarli Bookmark Manager Container
+After=docker.service
+Requires=docker.service
+
+
+[Service]
+Restart=always
+
+# Put any environment you want in an included file, like $host- or $domainname in this example
+EnvironmentFile=/etc/sysconfig/box-environment
+
+# It's just an example..
+ExecStart=/usr/bin/docker run \
+ -p 28010:80 \
+ --name ${hostname}-shaarli \
+ --hostname shaarli.${domainname} \
+ -v /srv/docker-volumes-local/shaarli-data:/var/www/shaarli/data:rw \
+ -v /etc/localtime:/etc/localtime:ro \
+ shaarli/shaarli:latest
+
+ExecStop=/usr/bin/docker rm -f ${hostname}-shaarli
+
+
+[Install]
+WantedBy=multi-user.target
+```
+## Foreword
+
+This guide assumes that:
+
+- Shaarli runs in a Docker container
+- The host's `10080` port is mapped to the container's `80` port
+- Shaarli's Fully Qualified Domain Name (FQDN) is `shaarli.domain.tld`
+- HTTP traffic is redirected to HTTPS
+
+## Apache
+
+- [Apache 2.4 documentation](https://httpd.apache.org/docs/2.4/)
+ - [mod_proxy](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html)
+ - [Reverse Proxy Request Headers](https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers)
+
+The following HTTP headers are set by using the `ProxyPass` directive:
+
+- `X-Forwarded-For`
+- `X-Forwarded-Host`
+- `X-Forwarded-Server`
+
+```apache
+<VirtualHost *:80>
+ ServerName shaarli.domain.tld
+ Redirect permanent / https://shaarli.domain.tld
+</VirtualHost>
+
+<VirtualHost *:443>
+ ServerName shaarli.domain.tld
+
+ SSLEngine on
+ SSLCertificateFile /path/to/cert
+ SSLCertificateKeyFile /path/to/certkey
+
+ LogLevel warn
+ ErrorLog /var/log/apache2/shaarli-error.log
+ CustomLog /var/log/apache2/shaarli-access.log combined
+
+ RequestHeader set X-Forwarded-Proto "https"
+
+ ProxyPass / http://127.0.0.1:10080/
+ ProxyPassReverse / http://127.0.0.1:10080/
+</VirtualHost>
+```
-TODO, see https://github.com/shaarli/Shaarli/issues/888
## HAProxy
+- [HAProxy documentation](https://cbonte.github.io/haproxy-dconv/)
+
+```conf
+global
+ [...]
+
+defaults
+ [...]
+
+frontend http-in
+ bind :80
+ redirect scheme https code 301 if !{ ssl_fc }
+
+ bind :443 ssl crt /path/to/cert.pem
+
+ default_backend shaarli
+
+
+backend shaarli
+ mode http
+ option http-server-close
+ option forwardfor
+ reqadd X-Forwarded-Proto: https
+
+ server shaarli1 127.0.0.1:10080
+```
+
+
## Nginx
+
+- [Nginx documentation](https://nginx.org/en/docs/)
+
+```nginx
+http {
+ [...]
+
+ index index.html index.php;
+
+ root /home/john/web;
+ access_log /var/log/nginx/access.log;
+ error_log /var/log/nginx/error.log;
+
+ server {
+ listen 80;
+ server_name shaarli.domain.tld;
+ return 301 https://shaarli.domain.tld$request_uri;
+ }
+
+ server {
+ listen 443 ssl http2;
+ server_name shaarli.domain.tld;
+
+ ssl_certificate /path/to/cert
+ ssl_certificate_key /path/to/certkey
+
+ location / {
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header X-Forwarded-Host $host;
+
+ proxy_pass http://localhost:10080/;
+ proxy_set_header Host $host;
+ proxy_connect_timeout 30s;
+ proxy_read_timeout 120s;
+
+ access_log /var/log/nginx/shaarli.access.log;
+ error_log /var/log/nginx/shaarli.error.log;
+ }
+ }
+}
+```
repository.
### Available image tags
-- `latest`: master branch (tarball release)
+- `latest`: latest branch (tarball release)
+- `master`: master branch (tarball release)
- `stable`: stable branch (tarball release)
-All images rely on:
+The `latest` and `master` images rely on:
+
+- [Alpine Linux](https://www.alpinelinux.org/)
+- [PHP7-FPM](http://php-fpm.org/)
+- [Nginx](http://nginx.org/)
+
+The `stable` image relies on:
+
- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
- [PHP5-FPM](http://php-fpm.org/)
- [Nginx](http://nginx.org/)
+
### Download from DockerHub
```bash
$ docker pull shaarli/shaarli
Login: `demo`; Password: `demo`
+Docker users can start a personal instance from an [autobuild image](https://hub.docker.com/r/shaarli/shaarli/). For example to start a temporary Shaarli at ``localhost:8000``, and keep session data (config, storage):
+```
+MY_SHAARLI_VOLUME=$(cd /path/to/shaarli/data/ && pwd -P)
+docker run -ti --rm \
+ -p 8000:80 \
+ -v $MY_SHAARLI_VOLUME:/var/www/shaarli/data \
+ shaarli/shaarli
+```
+
+A brief guide on getting starting using docker is given in [Docker 101](docker/docker-101).
+To learn more about user data and how to keep it across versions, please see [Upgrade and Migration](Upgrade-and-migration) documentation.
## Features
--- /dev/null
+FROM alpine:3.6
+MAINTAINER Shaarli Community
+
+RUN apk --update --no-cache add \
+ ca-certificates \
+ curl \
+ nginx \
+ php7 \
+ php7-ctype \
+ php7-curl \
+ php7-fpm \
+ php7-gd \
+ php7-iconv \
+ php7-intl \
+ php7-json \
+ php7-mbstring \
+ php7-openssl \
+ php7-phar \
+ php7-session \
+ php7-xml \
+ php7-zlib \
+ s6
+
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY php-fpm.conf /etc/php7/php-fpm.conf
+COPY services.d /etc/services.d
+
+RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
+ && rm -rf /etc/php7/php-fpm.d/www.conf \
+ && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
+ && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
+
+
+WORKDIR /var/www
+RUN curl -L https://github.com/shaarli/Shaarli/archive/latest.tar.gz | tar xzf - \
+ && mv Shaarli-latest shaarli \
+ && cd shaarli \
+ && composer --prefer-dist --no-dev install \
+ && rm -rf ~/.composer \
+ && chown -R nginx:nginx .
+
+VOLUME /var/www/shaarli/data
+
+EXPOSE 80
+
+ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
+CMD []
--- /dev/null
+FROM alpine:3.6
+MAINTAINER Shaarli Community
+
+RUN apk --update --no-cache add \
+ ca-certificates \
+ curl \
+ nginx \
+ php7 \
+ php7-ctype \
+ php7-curl \
+ php7-fpm \
+ php7-gd \
+ php7-iconv \
+ php7-intl \
+ php7-json \
+ php7-mbstring \
+ php7-openssl \
+ php7-phar \
+ php7-session \
+ php7-xml \
+ php7-zlib \
+ s6
+
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY php-fpm.conf /etc/php7/php-fpm.conf
+COPY services.d /etc/services.d
+
+RUN curl -sS https://getcomposer.org/installer | php7 -- --install-dir=/usr/local/bin --filename=composer \
+ && rm -rf /etc/php7/php-fpm.d/www.conf \
+ && sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php7/php.ini \
+ && sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php7/php.ini
+
+
+WORKDIR /var/www
+RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
+ && mv Shaarli-master shaarli \
+ && cd shaarli \
+ && composer --prefer-dist --no-dev install \
+ && rm -rf ~/.composer \
+ && chown -R nginx:nginx .
+
+VOLUME /var/www/shaarli/data
+
+EXPOSE 80
+
+ENTRYPOINT ["/bin/s6-svscan", "/etc/services.d"]
+CMD []
--- /dev/null
+## Alpine images
+- [Alpine Linux](https://www.alpinelinux.org/)
+- [PHP-FPM](http://php-fpm.org/)
+- [Nginx](http://nginx.org/)
+
+### `shaarli/shaarli:latest`
+- [Shaarli](https://github.com/shaarli/Shaarli), `latest` branch
+
+### `shaarli/shaarli:master`
+- [Shaarli](https://github.com/shaarli/Shaarli), `master` branch
-user www-data www-data;
+user nginx nginx;
daemon off;
worker_processes 4;
+pid /var/run/nginx.pid;
events {
worker_connections 768;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# filter and proxy PHP requests to PHP-FPM
- fastcgi_pass unix:/var/run/php5-fpm.sock;
+ fastcgi_pass unix:/var/run/php-fpm.sock;
fastcgi_index index.php;
include fastcgi.conf;
}
--- /dev/null
+[global]
+daemonize = no
+
+[www]
+user = nginx
+group = nginx
+listen.owner = nginx
+listen.group = nginx
+catch_workers_output = yes
+listen = /var/run/php-fpm.sock
+pm = dynamic
+pm.max_children = 20
+pm.start_servers = 1
+pm.min_spare_servers = 1
+pm.max_spare_servers = 3
+pm.max_requests = 2048
--- /dev/null
+#!/bin/sh
+/bin/true
--- /dev/null
+#!/bin/execlineb -P
+nginx
--- /dev/null
+#!/bin/execlineb -P
+php-fpm7 -F
+++ /dev/null
-FROM debian:jessie
-MAINTAINER Shaarli Community
-
-ENV TERM dumb
-RUN apt-get update \
- && apt-get install --no-install-recommends -y \
- ca-certificates \
- curl \
- nginx-light \
- php5-curl \
- php5-fpm \
- php5-gd \
- php5-intl \
- supervisor \
- && apt-get clean
-
-RUN sed -i 's/post_max_size.*/post_max_size = 10M/' /etc/php5/fpm/php.ini
-RUN sed -i 's/upload_max_filesize.*/upload_max_filesize = 10M/' /etc/php5/fpm/php.ini
-COPY nginx.conf /etc/nginx/nginx.conf
-COPY supervised.conf /etc/supervisor/conf.d/supervised.conf
-
-ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
-RUN chmod 755 /usr/local/bin/composer
-
-WORKDIR /var/www
-RUN curl -L https://github.com/shaarli/Shaarli/archive/master.tar.gz | tar xzf - \
- && mv Shaarli-master shaarli \
- && cd shaarli \
- && composer --prefer-dist --no-dev install
-RUN rm -rf html \
- && chown -R www-data:www-data .
-
-VOLUME /var/www/shaarli/data
-
-EXPOSE 80
-
-CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/supervisord.conf"]
+++ /dev/null
-## shaarli:latest
-- [Debian 8 Jessie](https://hub.docker.com/_/debian/)
-- [PHP5-FPM](http://php-fpm.org/)
-- [Nginx](http://nginx.org/)
-- [Shaarli](https://github.com/shaarli/Shaarli)
+++ /dev/null
-[program:php5-fpm]
-command=/usr/sbin/php5-fpm -F
-priority=5
-autostart=true
-autorestart=true
-
-[program:nginx]
-command=/usr/sbin/nginx
-priority=10
-autostart=true
-autorestart=true
-stdout_events_enabled=true
-stderr_events_enabled=true
--- /dev/null
+FROM alpine:3.6
+MAINTAINER Shaarli Community
+
+RUN apk --update --no-cache add \
+ ca-certificates \
+ curl \
+ make \
+ php7 \
+ php7-ctype \
+ php7-curl \
+ php7-dom \
+ php7-gd \
+ php7-iconv \
+ php7-intl \
+ php7-json \
+ php7-mbstring \
+ php7-openssl \
+ php7-phar \
+ php7-session \
+ php7-simplexml \
+ php7-tokenizer \
+ php7-xdebug \
+ php7-xml \
+ php7-zlib \
+ rsync
+
+RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
+
+RUN mkdir /shaarli
+WORKDIR /shaarli
+VOLUME /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
--- /dev/null
+FROM debian:jessie
+MAINTAINER Shaarli Community
+
+ENV TERM dumb
+ENV DEBIAN_FRONTEND noninteractive
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
+
+RUN apt-get update \
+ && apt-get install --no-install-recommends -y \
+ ca-certificates \
+ curl \
+ locales \
+ make \
+ php5 \
+ php5-curl \
+ php5-gd \
+ php5-intl \
+ php5-xdebug \
+ rsync \
+ && apt-get clean
+
+RUN locale-gen en_US.UTF-8 \
+ && locale-gen de_DE.UTF-8 \
+ && locale-gen fr_FR.UTF-8
+
+ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
+RUN chmod 755 /usr/local/bin/composer
+
+RUN mkdir /shaarli
+WORKDIR /shaarli
+VOLUME /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
--- /dev/null
+FROM debian:stretch
+MAINTAINER Shaarli Community
+
+ENV TERM dumb
+ENV DEBIAN_FRONTEND noninteractive
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
+
+RUN apt-get update \
+ && apt-get install --no-install-recommends -y \
+ ca-certificates \
+ curl \
+ locales \
+ make \
+ php7.0 \
+ php7.0-curl \
+ php7.0-gd \
+ php7.0-intl \
+ php7.0-xml \
+ php-xdebug \
+ rsync \
+ && apt-get clean
+
+RUN locale-gen en_US.UTF-8 \
+ && locale-gen de_DE.UTF-8 \
+ && locale-gen fr_FR.UTF-8
+
+ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
+RUN chmod 755 /usr/local/bin/composer
+
+RUN mkdir /shaarli
+WORKDIR /shaarli
+VOLUME /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
--- /dev/null
+FROM ubuntu:16.04
+MAINTAINER Shaarli Community
+
+ENV TERM dumb
+ENV DEBIAN_FRONTEND noninteractive
+ENV LANG en_US.UTF-8
+ENV LANGUAGE en_US:en
+
+RUN apt-get update \
+ && apt-get install --no-install-recommends -y \
+ ca-certificates \
+ curl \
+ language-pack-de \
+ language-pack-en \
+ language-pack-fr \
+ locales \
+ make \
+ php7.0 \
+ php7.0-curl \
+ php7.0-gd \
+ php7.0-intl \
+ php7.0-xml \
+ php-xdebug \
+ rsync \
+ && apt-get clean
+
+ADD https://getcomposer.org/composer.phar /usr/local/bin/composer
+RUN chmod 755 /usr/local/bin/composer
+
+RUN useradd -m dev \
+ && mkdir /shaarli
+USER dev
+WORKDIR /shaarli
+
+ENTRYPOINT ["make"]
+CMD []
--- /dev/null
+msgid ""
+msgstr ""
+"Project-Id-Version: Shaarli\n"
+"POT-Creation-Date: 2017-10-22 13:13+0200\n"
+"PO-Revision-Date: 2017-10-22 13:14+0200\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.0.4\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"
+
+#: application/ApplicationUtils.php:153
+#, 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 ""
+"Votre version de PHP est obsolète ! Shaarli nécessite au moins PHP %s, et ne "
+"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:183 application/ApplicationUtils.php:195
+msgid "directory is not readable"
+msgstr "le répertoire n'est pas accessible en lecture"
+
+#: application/ApplicationUtils.php:198
+msgid "directory is not writable"
+msgstr "le répertoire n'est pas accessible en écriture"
+
+#: application/ApplicationUtils.php:216
+msgid "file is not readable"
+msgstr "le fichier n'est pas accessible en lecture"
+
+#: application/ApplicationUtils.php:219
+msgid "file is not writable"
+msgstr "le fichier n'est pas accessible en écriture"
+
+#: application/Cache.php:16
+#, php-format
+msgid "Cannot purge %s: no directory"
+msgstr "Impossible de purger %s: le répertoire n'existe pas"
+
+#: application/FeedBuilder.php:151
+msgid "Direct link"
+msgstr "Liens directs"
+
+#: application/FeedBuilder.php:153
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:88
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:178
+msgid "Permalink"
+msgstr "Permalien"
+
+#: application/History.php:174
+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:185
+msgid "Could not parse history file"
+msgstr "Format incorrect pour le fichier d'historique"
+
+#: application/Languages.php:159
+msgid "Automatic"
+msgstr "Automatique"
+
+#: application/Languages.php:160
+msgid "English"
+msgstr "Anglais"
+
+#: application/Languages.php:161
+msgid "French"
+msgstr "Français"
+
+#: application/LinkDB.php:136
+msgid "You are not authorized to add a link."
+msgstr "Vous n'êtes pas autorisé à ajouter un lien."
+
+#: application/LinkDB.php:139
+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/LinkDB.php:142
+msgid "You must specify an integer as a key."
+msgstr "Vous devez utiliser un entier comme clé."
+
+#: application/LinkDB.php:145
+msgid "Array offset and link ID must be equal."
+msgstr "La clé du tableau et l'ID du lien doivent être égaux."
+
+#: 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 ""
+"Le gestionnaire de marque-page personnel, minimaliste, et sans base de "
+"données"
+
+#: application/LinkDB.php:253
+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 ""
+"Bienvenue sur Shaarli ! Ceci est votre premier marque-page public. Pour me "
+"modifier ou me supprimer, vous devez d'abord vous connecter.\n"
+"\n"
+"Pour apprendre comment utiliser Shaarli, consultez le lien « Documentation » "
+"en bas de page.\n"
+"\n"
+"Vous utilisez la version supportée par la communauté du projet original "
+"Shaarli, de Sébastien Sauvage."
+
+#: application/LinkDB.php:267
+msgid "My secret stuff... - Pastebin.com"
+msgstr "Mes trucs secrets... - Pastebin.com"
+
+#: application/LinkDB.php:269
+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/LinkFilter.php:452
+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/NetscapeBookmarkUtils.php:35
+msgid "Invalid export selection:"
+msgstr "Sélection d'export invalide :"
+
+#: application/NetscapeBookmarkUtils.php:81
+#, php-format
+msgid "File %s (%d bytes) "
+msgstr "Le fichier %s (%d octets) "
+
+#: application/NetscapeBookmarkUtils.php:83
+msgid "has an unknown file format. Nothing was imported."
+msgstr "a un format inconnu. Rien n'a été importé."
+
+#: application/NetscapeBookmarkUtils.php:86
+#, php-format
+msgid ""
+"was successfully processed in %d seconds: %d links imported, %d links "
+"overwritten, %d links skipped."
+msgstr ""
+"a été importé avec succès en %d secondes : %d liens importés, %d liens "
+"écrasés, %d liens ignorés."
+
+#: application/PageBuilder.php:165
+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/PageBuilder.php:167
+msgid "404 Not Found"
+msgstr "404 Introuvable"
+
+#: application/PluginManager.php:243
+#, php-format
+msgid "Plugin \"%s\" files not found."
+msgstr "Les fichiers de l'extension \"%s\" sont introuvables."
+
+#: application/Updater.php:76
+msgid "Couldn't retrieve Updater class methods."
+msgstr "Impossible de récupérer les méthodes de la classe Updater."
+
+#: application/Updater.php:485
+msgid "An error occurred while running the update "
+msgstr "Une erreur s'est produite lors de l'exécution de la mise à jour "
+
+#: application/Updater.php:525
+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.php:530
+msgid "Unable to write updates in "
+msgstr "Impossible d'écrire les mises à jour dans "
+
+#: application/Utils.php:406 tests/UtilsTest.php:398
+msgid "Setting not set"
+msgstr "Paramètre non défini"
+
+#: application/Utils.php:413 tests/UtilsTest.php:396 tests/UtilsTest.php:397
+msgid "Unlimited"
+msgstr "Illimité"
+
+#: application/Utils.php:416 tests/UtilsTest.php:393 tests/UtilsTest.php:394
+#: tests/UtilsTest.php:408
+msgid "B"
+msgstr "o"
+
+#: application/Utils.php:416 tests/UtilsTest.php:387 tests/UtilsTest.php:388
+#: tests/UtilsTest.php:395
+msgid "kiB"
+msgstr "ko"
+
+#: application/Utils.php:416 tests/UtilsTest.php:389 tests/UtilsTest.php:390
+#: tests/UtilsTest.php:406 tests/UtilsTest.php:407
+msgid "MiB"
+msgstr "Mo"
+
+#: application/Utils.php:416 tests/UtilsTest.php:391 tests/UtilsTest.php:392
+msgid "GiB"
+msgstr "Go"
+
+#: 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 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:135
+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:19
+msgid "Error accessing"
+msgstr "Une erreur s'est produite en accédant à "
+
+#: index.php:133
+msgid "Shared links on "
+msgstr "Liens partagés sur "
+
+#: index.php:155
+msgid "Insufficient permissions:"
+msgstr "Permissions insuffisantes :"
+
+#: index.php:382
+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:447
+msgid "Wrong login/password."
+msgstr "Nom d'utilisateur ou mot de passe incorrects."
+
+#: index.php:1107
+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:1112 index.php:1153 index.php:1229 index.php:1259 index.php:1359
+msgid "Wrong token."
+msgstr "Jeton invalide."
+
+#: index.php:1117
+msgid "The old password is not correct."
+msgstr "L'ancien mot de passe est incorrect."
+
+#: index.php:1137
+msgid "Your password has been changed"
+msgstr "Votre mot de passe a été modifié"
+
+#: index.php:1190
+msgid "Configuration was saved."
+msgstr "La configuration a été sauvegardé."
+
+#: index.php:1241
+#, 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:1242
+#, 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:1458
+msgid "Note: "
+msgstr "Note : "
+
+#: index.php:1567
+#, 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:1983
+#, php-format
+msgid ""
+"<pre>Sessions do not seem to work correctly on your server.<br>Make sure the "
+"variable \"session.save_path\" is set correctly in your PHP config, and that "
+"you have write access to it.<br>It currently points to %s.<br>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.<br>"
+msgstr ""
+"<pre>Les sesssions ne semble pas fonctionner sur ce serveur.<br>Assurez vous "
+"que la variable « session.save_path » est correctement définie dans votre "
+"fichier de configuration PHP, et que vous y avez les droits d'écriture."
+"<br>Ce paramètre pointe actuellement sur %s.<br>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 <em>Fully Qualified Domain Name</em>.<br>"
+
+#: index.php:1993
+msgid "Click to try again."
+msgstr "Cliquer ici pour réessayer."
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:29
+msgid "URI"
+msgstr "URI"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:33
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "Add link"
+msgstr "Shaare"
+
+#: plugins/addlink_toolbar/addlink_toolbar.php:50
+msgid "Adds the addlink input on the linklist page."
+msgstr "Ajout le formulaire d'ajout de liens sur la page principale."
+
+#: plugins/archiveorg/archiveorg.php:23
+msgid "View on archive.org"
+msgstr "Voir sur archive.org"
+
+#: plugins/archiveorg/archiveorg.php:36
+msgid "For each link, add an Archive.org icon."
+msgstr "Pour chaque lien, ajoute une icône pour Archive.org."
+
+#: plugins/demo_plugin/demo_plugin.php:469
+msgid ""
+"A demo plugin covering all use cases for template designers and plugin "
+"developers."
+msgstr ""
+"Une extension de démonstration couvrant tous les cas d'utilisation pour les "
+"designers et les développeurs."
+
+#: plugins/isso/isso.php:20
+msgid ""
+"Isso plugin error: Please define the \"ISSO_SERVER\" setting in the plugin "
+"administration page."
+msgstr ""
+"Erreur de l'extension Isso : Merci de définir le paramètre « ISSO_SERVER » "
+"dans la page d'administration des extensions."
+
+#: plugins/isso/isso.php:63
+msgid "Let visitor comment your shaares on permalinks with Isso."
+msgstr ""
+"Permet aux visiteurs de commenter vos shaares sur les permaliens avec Isso."
+
+#: plugins/isso/isso.php:64
+msgid "Isso server URL (without 'http://')"
+msgstr "URL du serveur Isso (sans 'http://')"
+
+#: plugins/markdown/markdown.php:159
+msgid "Description will be rendered with"
+msgstr "La description sera générée avec"
+
+#: plugins/markdown/markdown.php:160
+msgid "Markdown syntax documentation"
+msgstr "Documentation sur la syntaxe Markdown"
+
+#: plugins/markdown/markdown.php:161
+msgid "Markdown syntax"
+msgstr "la syntaxe Markdown"
+
+#: plugins/markdown/markdown.php:340
+msgid ""
+"Render shaare description with Markdown syntax.<br><strong>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 <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
+"markdown#html-rendering\">README</a>."
+msgstr ""
+"Utilise la syntaxe Markdown pour la description des liens."
+"<br><strong>Attention</strong> :\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 <a href=\"https://github.com/shaarli/Shaarli/tree/master/plugins/"
+"markdown#html-rendering\">README</a>."
+
+#: plugins/piwik/piwik.php:21
+msgid ""
+"Piwik plugin error: Please define PIWIK_URL and PIWIK_SITEID in the plugin "
+"administration page."
+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:70
+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:71
+msgid "Piwik URL"
+msgstr "URL de Piwik"
+
+#: plugins/piwik/piwik.php:72
+msgid "Piwik site ID"
+msgstr "Site ID de Piwik"
+
+#: plugins/playvideos/playvideos.php:22
+msgid "Video player"
+msgstr "Lecteur vidéo"
+
+#: plugins/playvideos/playvideos.php:25
+msgid "Play Videos"
+msgstr "Jouer les vidéos"
+
+#: plugins/playvideos/playvideos.php:56
+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."
+
+#: plugins/playvideos/youtube_playlist.js:214
+msgid "plugins/playvideos/jquery-1.11.2.min.js"
+msgstr ""
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:69
+#, php-format
+msgid "Could not publish to PubSubHubbub: %s"
+msgstr "Impossible de publier vers PubSubHubbub : %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:95
+#, php-format
+msgid "Could not post to %s"
+msgstr "Impossible de publier vers %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:99
+#, php-format
+msgid "Bad response from the hub %s"
+msgstr "Mauvaise réponse du hub %s"
+
+#: plugins/pubsubhubbub/pubsubhubbub.php:110
+msgid "Enable PubSubHubbub feed publishing."
+msgstr "Active la publication de flux vers PubSubHubbub."
+
+#: plugins/qrcode/qrcode.php:69 plugins/wallabag/wallabag.php:68
+msgid "For each link, add a QRCode icon."
+msgstr "Pour chaque liens, ajouter une icône de QRCode."
+
+#: plugins/wallabag/wallabag.php:21
+msgid ""
+"Wallabag plugin error: Please define the \"WALLABAG_URL\" setting in the "
+"plugin administration page."
+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
+msgid "Save to wallabag"
+msgstr "Sauvegarder dans Wallabag"
+
+#: plugins/wallabag/wallabag.php:69
+msgid "Wallabag API URL"
+msgstr "URL de l'API Wallabag"
+
+#: plugins/wallabag/wallabag.php:70
+msgid "Wallabag API version (1 or 2)"
+msgstr "Version de l'API Wallabag (1 ou 2)"
+
+#: tests/LanguagesTest.php:188 tests/LanguagesTest.php:201
+#: 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
+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."
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+msgid "Shaare a new link"
+msgstr "Partager un nouveau lien"
+
+#: tmp/addlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "URL or leave empty to post a note"
+msgstr "URL ou laisser vide pour créer une note"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Change password"
+msgstr "Modification du mot de passe"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Current password"
+msgstr "Mot de passe actuel"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "New password"
+msgstr "Nouveau mot de passe"
+
+#: tmp/changepassword.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Change"
+msgstr "Changer"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:13
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Manage tags"
+msgstr "Gérer les tags"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
+msgid "Tag"
+msgstr "Tag"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "New name"
+msgstr "Nouveau nom"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
+msgid "Case sensitive"
+msgstr "Sensible à la casse"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+msgid "Rename"
+msgstr "Renommer"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:79
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:172
+msgid "Delete"
+msgstr "Supprimer"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
+msgid "You can also edit tags in the"
+msgstr "Vous pouvez aussi modifier les tags dans la"
+
+#: tmp/changetag.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
+msgid "tag list"
+msgstr "liste des tags"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "Configure"
+msgstr "Configurer"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "title"
+msgstr "titre"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+msgid "Home link"
+msgstr "Lien vers l'accueil"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+msgid "Default value"
+msgstr "Valeur par défaut"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "Theme"
+msgstr "Thème"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:87
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:78
+msgid "Language"
+msgstr "Langue"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:116
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:102
+msgid "Timezone"
+msgstr "Fuseau horaire"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
+msgid "Continent"
+msgstr "Continent"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:103
+msgid "City"
+msgstr "Ville"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:163
+msgid "Redirector"
+msgstr "Redirecteur"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
+msgid "e. g."
+msgstr "ex :"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:164
+msgid "will mask the HTTP_REFERER"
+msgstr "masque le HTTP_REFERER"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:179
+msgid "Disable session cookie hijacking protection"
+msgstr "Désactiver la protection contre le détournement de cookies"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:181
+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:198
+msgid "Private links by default"
+msgstr "Liens privés par défaut"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:199
+msgid "All new links are private by default"
+msgstr "Tous les nouveaux liens sont privés par défaut"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:214
+msgid "RSS direct links"
+msgstr "Liens directs dans le flux RSS"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:215
+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:230
+msgid "Hide public links"
+msgstr "Cacher les liens publics"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:231
+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:246
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
+msgid "Check updates"
+msgstr "Vérifier les mises à jour"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:247
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:152
+msgid "Notify me when a new release is ready"
+msgstr "Me notifier lorsqu'une nouvelle version est disponible"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:262
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+msgid "Enable REST API"
+msgstr "Activer l'API REST"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:263
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
+msgid "Allow third party software to use Shaarli such as mobile application"
+msgstr ""
+"Permets aux applications tierces d'utiliser Shaarli, par exemple les "
+"applications mobiles"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:278
+msgid "API secret"
+msgstr "Clé d'API secrète"
+
+#: tmp/configure.b91ef64efc3688266305ea9b42e5017e.rtpl.php:289
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:139
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:192
+msgid "Save"
+msgstr "Enregistrer"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+msgid "The Daily Shaarli"
+msgstr "Le Quotidien Shaarli"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
+msgid "1 RSS entry per day"
+msgstr "1 entrée RSS par jour"
+
+#: tmp/daily.b91ef64efc3688266305ea9b42e5017e.rtpl.php:37
+msgid "Previous day"
+msgstr "Jour précédent"
+
+#: 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:51
+msgid "Next day"
+msgstr "Jour suivant"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:170
+msgid "Edit"
+msgstr "Modifier"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:26
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:26
+msgid "Shaare"
+msgstr "Shaare"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:25
+msgid "Created:"
+msgstr "Création :"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "URL"
+msgstr "URL"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+msgid "Title"
+msgstr "Titre"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+#: 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 "Description"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+msgid "Tags"
+msgstr "Tags"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:59
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:168
+msgid "Private"
+msgstr "Privé"
+
+#: tmp/editlink.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+msgid "Apply Changes"
+msgstr "Appliquer les changements"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Export Database"
+msgstr "Exporter les données"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:24
+msgid "Selection"
+msgstr "Choisir"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
+msgid "All"
+msgstr "Tous"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+msgid "Public"
+msgstr "Publics"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:52
+msgid "Prepend note permalinks with this Shaarli instance's URL"
+msgstr "Préfixer les liens de notes avec l'URL de l'instance de Shaarli"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:53
+msgid "Useful to import bookmarks in a web browser"
+msgstr "Utile pour importer les marques-pages dans un navigateur"
+
+#: tmp/export.b91ef64efc3688266305ea9b42e5017e.rtpl.php:65
+msgid "Export"
+msgstr "Exporter"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Import Database"
+msgstr "Importer des données"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+msgid "Maximum size allowed:"
+msgstr "Taille maximum autorisée :"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Visibility"
+msgstr "Visibilité"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:36
+msgid "Use values from the imported file, default to public"
+msgstr ""
+"Utiliser les valeurs présentes dans le fichier d'import, public par défaut"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+msgid "Import all bookmarks as private"
+msgstr "Importer tous les liens comme privés"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+msgid "Import all bookmarks as public"
+msgstr "Importer tous les liens comme publics"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:57
+msgid "Overwrite existing bookmarks"
+msgstr "Remplacer les liens existants"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:58
+msgid "Duplicates based on URL"
+msgstr "Les doublons s'appuient sur les URL"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
+msgid "Add default tags"
+msgstr "Ajouter des tags par défaut"
+
+#: tmp/import.b91ef64efc3688266305ea9b42e5017e.rtpl.php:83
+msgid "Import"
+msgstr "Importer"
+
+#: 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:147
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:147
+msgid "Username"
+msgstr "Nom d'utilisateur"
+
+#: 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
+msgid "Password"
+msgstr "Mot de passe"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:63
+msgid "Shaarli title"
+msgstr "Titre du Shaarli"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:69
+msgid "My links"
+msgstr "Mes liens"
+
+#: tmp/install.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
+msgid "Install"
+msgstr "Installer"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:80
+msgid "shaare"
+msgid_plural "shaares"
+msgstr[0] "shaare"
+msgstr[1] "shaares"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:84
+msgid "private link"
+msgid_plural "private links"
+msgstr[0] "lien privé"
+msgstr[1] "liens privés"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:31
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:117
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:117
+msgid "Search text"
+msgstr "Recherche texte"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:38
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:124
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:124
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:61
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:33
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
+msgid "Filter by tag"
+msgstr "Filtrer par tag"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:111
+msgid "Nothing found."
+msgstr "Aucun résultat."
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:119
+#, php-format
+msgid "%s result"
+msgid_plural "%s results"
+msgstr[0] "%s résultat"
+msgstr[1] "%s résultats"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:123
+msgid "for"
+msgstr "pour"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:130
+msgid "tagged"
+msgstr "taggé"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
+msgid "Remove tag"
+msgstr "Retirer le tag"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:143
+msgid "with status"
+msgstr "avec le statut"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
+msgid "without any tag"
+msgstr "sans tag"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:174
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:42
+msgid "Fold"
+msgstr "Replier"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
+msgid "Edited: "
+msgstr "Modifié : "
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:180
+msgid "permalink"
+msgstr "permalien"
+
+#: tmp/linklist.b91ef64efc3688266305ea9b42e5017e.rtpl.php:182
+msgid "Add tag"
+msgstr "Ajouter un tag"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:7
+msgid "Filters"
+msgstr "Filtres"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:12
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:12
+msgid "Filter private links"
+msgstr "Filtrer par liens privés"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:18
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:18
+msgid "Filter untagged links"
+msgstr "Filtrer par liens privés"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:22
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:74
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:22
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:74
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:43
+msgid "Fold all"
+msgstr "Replier tout"
+
+#: tmp/linklist.paging.b91ef64efc3688266305ea9b42e5017e.rtpl.php:67
+#: tmp/linklist.paging.cedf684561d925457130839629000a81.rtpl.php:67
+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'échec d'authentification. Merci de "
+"réessayer plus tard."
+
+#: 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 "Connexion"
+
+#: tmp/loginform.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:151
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:151
+msgid "Remember me"
+msgstr "Rester connecté"
+
+#: 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 "by the Shaarli community"
+msgstr "par la communauté Shaarli"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:15
+msgid "Documentation"
+msgstr "Documentation"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:44
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:44
+msgid "Expand"
+msgstr "Déplier"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:45
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:45
+msgid "Expand all"
+msgstr "Déplier tout"
+
+#: tmp/page.footer.b91ef64efc3688266305ea9b42e5017e.rtpl.php:46
+#: tmp/page.footer.cedf684561d925457130839629000a81.rtpl.php:46
+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:31
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:31
+msgid "Tools"
+msgstr "Outils"
+
+#: 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"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:39
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:39
+msgid "Picture wall"
+msgstr "Mur d'images"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:42
+msgid "Daily"
+msgstr "Quotidien"
+
+#: 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
+msgid "RSS Feed"
+msgstr "Flux RSS"
+
+#: 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
+msgid "Logout"
+msgstr "Déconnexion"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:169
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:169
+msgid "is available"
+msgstr "est disponible"
+
+#: tmp/page.header.b91ef64efc3688266305ea9b42e5017e.rtpl.php:176
+#: tmp/page.header.cedf684561d925457130839629000a81.rtpl.php:176
+msgid "Error"
+msgstr "Erreur"
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Picture Wall"
+msgstr "Mur d'images"
+
+#: tmp/picwall.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "pics"
+msgstr "images"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:15
+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 extensions"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:29
+msgid "Enabled Plugins"
+msgstr "Extensions activées"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:34
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:155
+msgid "No plugin enabled."
+msgstr "Aucune extension activée."
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:40
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:73
+msgid "Disable"
+msgstr "Désactiver"
+
+#: 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 "Nom"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:43
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:76
+msgid "Order"
+msgstr "Ordre"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:86
+msgid "Disabled Plugins"
+msgstr "Extensions désactivées"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:91
+msgid "No plugin disabled."
+msgstr "Aucune extension désactivée."
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:97
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:122
+msgid "Enable"
+msgstr "Activer"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
+msgid "More plugins available"
+msgstr "Plus d'extensions disponibles"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:136
+msgid "in the documentation"
+msgstr "dans la documentation"
+
+#: tmp/pluginsadmin.b91ef64efc3688266305ea9b42e5017e.rtpl.php:150
+msgid "Plugin configuration"
+msgstr "Configuration des extensions"
+
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:19
+msgid "tags"
+msgstr "tags"
+
+#: tmp/tag.cloud.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+#: tmp/tag.list.b91ef64efc3688266305ea9b42e5017e.rtpl.php:23
+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 "List des tags"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:3
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:3
+msgid "Sort by:"
+msgstr "Trier par :"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:5
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:5
+msgid "Cloud"
+msgstr "Nuage"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:6
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:6
+msgid "Most used"
+msgstr "Plus utilisés"
+
+#: tmp/tag.sort.b91ef64efc3688266305ea9b42e5017e.rtpl.php:7
+#: tmp/tag.sort.cedf684561d925457130839629000a81.rtpl.php:7
+msgid "Alphabetical"
+msgstr "Alphabétique"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:14
+msgid "Settings"
+msgstr "Paramètres"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:16
+msgid "Change Shaarli settings: title, timezone, etc."
+msgstr "Changer les paramètres de Shaarli : titre, fuseau horaire, etc."
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:17
+msgid "Configure your Shaarli"
+msgstr "Conguration de Shaarli"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:21
+msgid "Enable, disable and configure plugins"
+msgstr "Activer, désactiver et configurer les extensions"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:28
+msgid "Change your password"
+msgstr "Modification du mot de passe"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:35
+msgid "Rename or delete a tag in all links"
+msgstr "Rename or delete a tag in all links"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:41
+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...)"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:42
+msgid "Import links"
+msgstr "Importer des liens"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:47
+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...)"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:48
+msgid "Export database"
+msgstr "Exporter les données"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:71
+msgid ""
+"Drag one of these button to your bookmarks toolbar or right-click it and "
+"\"Bookmark This Link\""
+msgstr ""
+"Glisser un de ces bouttons dans votre barre de favoris ou cliquer droit "
+"dessus et « Ajouter aux favoris »"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:72
+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:76
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:100
+msgid ""
+"Drag this link to your bookmarks toolbar or right-click it and Bookmark This "
+"Link"
+msgstr ""
+"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
+"Ajouter aux favoris »"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:77
+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:86
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:108
+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:96
+msgid "Shaare link"
+msgstr "Shaare"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:101
+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:117
+msgid "Add Note"
+msgstr "Ajouter une Note"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:129
+msgid ""
+"You need to browse your Shaarli over <strong>HTTPS</strong> to use this "
+"functionality."
+msgstr ""
+"Vous devez utiliser Shaarli en <strong>HTTPS</strong> pour utiliser cette "
+"fonctionalité."
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:134
+msgid "Add to"
+msgstr "Ajouter à "
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:145
+msgid "3rd party"
+msgstr "Applications tierces"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:147
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:153
+msgid "Plugin"
+msgstr "Extension"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:148
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:154
+msgid "plugin"
+msgstr "extension"
+
+#: tmp/tools.b91ef64efc3688266305ea9b42e5017e.rtpl.php:175
+msgid ""
+"Drag this link to your bookmarks toolbar, or right-click it and choose "
+"Bookmark This Link"
+msgstr ""
+"Glisser ce lien dans votre barre de favoris ou cliquer droit dessus et « "
+"Ajouter aux favoris »"
+
+#~ msgid ""
+#~ "An error occurred while parsing JSON configuration file (%s): error code #"
+#~ "%d"
+#~ msgstr ""
+#~ "Une erreur s'est produite lors de la lecture du fichier de configuration "
+#~ "JSON (%s) : code d'erreur #%d"
+
+#~ msgid ""
+#~ "Please check your JSON syntax (without PHP comment tags) using a JSON "
+#~ "lint tool such as "
+#~ msgstr ""
+#~ "Merci de vérifier la syntaxe JSON (sans les balises de commentaires PHP) "
+#~ "en utilisant un validateur de JSON tel que "
+
+#~ msgid ""
+#~ "Error: missing Composer dependencies\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"
+#~ msgstr ""
+#~ "Erreur : les dépendances Composer sont manquantes\n"
+#~ "\n"
+#~ "Si vous avez installé Shaarli avec Git ou depuis la branche de "
+#~ "développement\n"
+#~ "merci de consulter la documentation d'installation pour installer les "
+#~ "dépendances Composer :\n"
+#~ "\n"
+
+#~ msgid "Sessions do not seem to work correctly on your server."
+#~ msgstr "Les sessions ne semblent "
+
+#~ msgid "Tag was renamed in "
+#~ msgstr "Le tag a été renommé dans "
+
+#, fuzzy
+#~| msgid "My links"
+#~ msgid " links"
+#~ msgstr "Mes liens"
+
+#, fuzzy
+#~| msgid ""
+#~| "Error: missing Composer configuration\n"
+#~| "\n"
+#~ msgid "Error: missing Composer configuration"
+#~ msgstr ""
+#~ "Erreur : la configuration Composer est manquante\n"
+#~ "\n"
+
+#, fuzzy
+#~| msgid ""
+#~| "Shaarli could not create the config file. Please make sure Shaarli has "
+#~| "the right to write in the folder is it installed in."
+#~ msgid ""
+#~ "Shaarli could not create the config file. \n"
+#~ " 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é."
+
+#, fuzzy
+#~| msgid "Plugin"
+#~ msgid "Plugin \""
+#~ msgstr "Extension"
+
+#~ msgid "Your PHP version is obsolete!"
+#~ msgstr "Votre version de PHP est obsolète !"
+
+#~ msgid " Shaarli requires at least PHP "
+#~ msgstr "Shaarli nécessite au moins PHP"
require_once 'application/FileUtils.php';
require_once 'application/History.php';
require_once 'application/HttpUtils.php';
-require_once 'application/Languages.php';
require_once 'application/LinkDB.php';
require_once 'application/LinkFilter.php';
require_once 'application/LinkUtils.php';
require_once 'application/PluginManager.php';
require_once 'application/Router.php';
require_once 'application/Updater.php';
+use \Shaarli\Languages;
use \Shaarli\ThemeUtils;
use \Shaarli\Config\ConfigManager;
+use \Shaarli\SessionManager;
// Ensure the PHP version is supported
try {
exit;
}
-define('shaarli_version', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
+define('SHAARLI_VERSION', ApplicationUtils::getVersion(__DIR__ .'/'. ApplicationUtils::$VERSION_FILE));
// Force cookie path (but do not change lifetime)
$cookie = session_get_cookie_params();
}
// Regenerate session ID if invalid or not defined in cookie.
-if (isset($_COOKIE['shaarli']) && !is_session_id_valid($_COOKIE['shaarli'])) {
+if (isset($_COOKIE['shaarli']) && !SessionManager::checkId($_COOKIE['shaarli'])) {
session_regenerate_id(true);
$_COOKIE['shaarli'] = session_id();
}
$conf = new ConfigManager();
+$sessionManager = new SessionManager($_SESSION, $conf);
+
+// Sniff browser language and set date format accordingly.
+if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
+ autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
+}
+
+new Languages(setlocale(LC_MESSAGES, 0), $conf);
+
$conf->setEmpty('general.timezone', date_default_timezone_get());
-$conf->setEmpty('general.title', 'Shared links on '. escape(index_url($_SERVER)));
+$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
$errors = ApplicationUtils::checkResourcePermissions($conf);
if ($errors != array()) {
- $message = '<p>Insufficient permissions:</p><ul>';
+ $message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
foreach ($errors as $error) {
$message .= '<li>'.$error.'</li>';
}
// Display the installation form if no existing config is found
- install($conf);
+ install($conf, $sessionManager);
}
// a token depending of deployment salt, user password, and the current ip
define('STAY_SIGNED_IN_TOKEN', sha1($conf->get('credentials.hash') . $_SERVER['REMOTE_ADDR'] . $conf->get('credentials.salt')));
-// Sniff browser language and set date format accordingly.
-if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
- autoLocale($_SERVER['HTTP_ACCEPT_LANGUAGE']);
-}
-
/**
* Checking session state (i.e. is the user still logged in)
*
// Process login form: Check if login/password is correct.
if (isset($_POST['login']))
{
- if (!ban_canLogin($conf)) die('I said: NO. You are banned for the moment. Go away.');
+ if (!ban_canLogin($conf)) die(t('I said: NO. You are banned for the moment. Go away.'));
if (isset($_POST['password'])
- && tokenOk($_POST['token'])
+ && $sessionManager->checkToken($_POST['token'])
&& (check_auth($_POST['login'], $_POST['password'], $conf))
) { // Login/password is OK.
ban_loginOk($conf);
}
}
}
- echo '<script>alert("Wrong login/password.");document.location=\'?do=login'.$redir.'\';</script>'; // Redirect to login screen.
+ // Redirect to login screen.
+ echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
exit;
}
}
// 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.
-/**
- * Returns a token.
- *
- * @param ConfigManager $conf Configuration Manager instance.
- *
- * @return string token.
- */
-function getToken($conf)
-{
- $rnd = sha1(uniqid('', true) .'_'. mt_rand() . $conf->get('credentials.salt')); // We generate a random string.
- $_SESSION['tokens'][$rnd]=1; // Store it on the server side.
- return $rnd;
-}
-
-// Tells if a token is OK. Using this function will destroy the token.
-// true=token is OK.
-function tokenOk($token)
-{
- if (isset($_SESSION['tokens'][$token]))
- {
- unset($_SESSION['tokens'][$token]); // Token is used: destroy it.
- return true; // Token is OK.
- }
- return false; // Wrong token, or already used.
-}
-
/**
* Daily RSS feed: 1 RSS entry per day giving all the links on that day.
* Gives the last 7 days (which have links).
/**
* 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 ConfigManager $conf Configuration Manager instance.
+ * @param PluginManager $pluginManager Plugin Manager instance,
+ * @param LinkDB $LINKSDB
+ * @param History $history instance
+ * @param SessionManager $sessionManager SessionManager instance
*/
-function renderPage($conf, $pluginManager, $LINKSDB, $history)
+function renderPage($conf, $pluginManager, $LINKSDB, $history, $sessionManager)
{
$updater = new Updater(
read_updates_file($conf->get('resource.updates')),
die($e->getMessage());
}
- $PAGE = new PageBuilder($conf, $LINKSDB);
+ $PAGE = new PageBuilder($conf, $LINKSDB, $sessionManager->generateToken());
$PAGE->assign('linkcount', count($LINKSDB));
$PAGE->assign('privateLinkcount', count_private($LINKSDB));
$PAGE->assign('plugin_errors', $pluginManager->getErrors());
$query = (isset($_SERVER['QUERY_STRING'])) ? $_SERVER['QUERY_STRING'] : '';
$targetPage = Router::findPage($query, $_GET, isLoggedIn());
+ if (
+ // if the user isn't logged in
+ !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(
}
$data = array(
- 'search_tags' => implode(' ', $filteringTags),
+ 'search_tags' => implode(' ', escape($filteringTags)),
'tags' => $tagList,
);
$pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn()));
}
$data = [
- 'search_tags' => implode(' ', $filteringTags),
+ 'search_tags' => implode(' ', escape($filteringTags)),
'tags' => $tags,
];
$pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]);
if ($targetPage == Router::$PAGE_CHANGEPASSWORD)
{
if ($conf->get('security.open_shaarli')) {
- die('You are not supposed to change a password on an Open Shaarli.');
+ die(t('You are not supposed to change a password on an Open Shaarli.'));
}
if (!empty($_POST['setpassword']) && !empty($_POST['oldpassword']))
{
- if (!tokenOk($_POST['token'])) die('Wrong token.'); // Go away!
+ 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 '<script>alert("The old password is not correct.");document.location=\'?do=changepasswd\';</script>'; exit; }
+ if ($oldhash!= $conf->get('credentials.hash')) {
+ echo '<script>alert("'. t('The old password is not correct.') .'");document.location=\'?do=changepasswd\';</script>';
+ exit;
+ }
// Save new password
// Salt renders rainbow-tables attacks useless.
$conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
exit;
}
- echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>';
+ echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
exit;
}
else // show the change password form.
{
if (!empty($_POST['title']) )
{
- if (!tokenOk($_POST['token'])) {
- die('Wrong token.'); // Go away!
+ if (!$sessionManager->checkToken($_POST['token'])) {
+ die(t('Wrong token.')); // Go away!
}
$tz = 'UTC';
if (!empty($_POST['continent']) && !empty($_POST['city'])
$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']));
+
try {
$conf->write(isLoggedIn());
$history->updateSettings();
echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
exit;
}
- echo '<script>alert("Configuration was saved.");document.location=\'?do=configure\';</script>';
+ echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
exit;
}
else // Show the configuration form.
$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('language', $conf->get('translation.language'));
$PAGE->renderPage('configure');
exit;
}
exit;
}
- if (!tokenOk($_POST['token'])) {
- die('Wrong token.');
+ if (!$sessionManager->checkToken($_POST['token'])) {
+ die(t('Wrong token.'));
}
$alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag']));
}
$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 links.'), count($alteredLinks))
- : sprintf(t('The tag was renamed in %d links.'), count($alteredLinks));
+ ? 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 '<script>alert("'. $alert .'");document.location=\'?'. $redirect .'\';</script>';
exit;
}
if (isset($_POST['save_edit']))
{
// Go away!
- if (! tokenOk($_POST['token'])) {
- die('Wrong token.');
+ if (! $sessionManager->checkToken($_POST['token'])) {
+ die(t('Wrong token.'));
}
// lf_id should only be present if the link exists.
// -------- User clicked the "Delete" button when editing a link: Delete link from database.
if ($targetPage == Router::$PAGE_DELETELINK)
{
- if (! tokenOk($_GET['token'])) {
- die('Wrong token.');
+ if (! $sessionManager->checkToken($_GET['token'])) {
+ die(t('Wrong token.'));
}
$ids = trim($_GET['lf_linkdate']);
if ($url == '') {
$url = '?' . smallHash($linkdate . $LINKSDB->getNextId());
- $title = 'Note: ';
+ $title = $conf->get('general.default_note_title', t('Note: '));
}
$url = escape($url);
$title = escape($title);
// 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.
- echo '<script>alert("The file you are trying to upload is probably'
- .' bigger than what this webserver can accept ('
- .get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').'
- .' Please upload in smaller chunks.");document.location=\'?do='
- .Router::$PAGE_IMPORT .'\';</script>';
+ $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 '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
exit;
}
- if (! tokenOk($_POST['token'])) {
+ if (! $sessionManager->checkToken($_POST['token'])) {
die('Wrong token.');
}
$status = NetscapeBookmarkUtils::import(
// Get a fresh token
if ($targetPage == Router::$GET_TOKEN) {
header('Content-Type:text/plain');
- echo getToken($conf);
+ echo $sessionManager->generateToken($conf);
exit;
}
* Installation
* This function should NEVER be called if the file data/config.php exists.
*
- * @param ConfigManager $conf Configuration Manager instance.
+ * @param ConfigManager $conf Configuration Manager instance.
+ * @param SessionManager $sessionManager SessionManager instance
*/
-function install($conf)
-{
+function install($conf, $sessionManager) {
// 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);
// (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.
- echo '<pre>Sessions do not seem to work correctly on your server.<br>';
- echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.<br>';
- echo 'It currently points to '.session_save_path().'<br>';
- echo 'Check that the hostname used to access Shaarli contains a dot. 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.<br>';
- echo '<br><a href="?">Click to try again.</a></pre>';
+ {
+ // Step 2: Check if data in session is correct.
+ $msg = t(
+ '<pre>Sessions do not seem to work correctly on your server.<br>'.
+ 'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
+ 'and that you have write access to it.<br>'.
+ 'It currently points to %s.<br>'.
+ '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.<br>'
+ );
+ $msg = sprintf($msg, session_save_path());
+ echo $msg;
+ echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
die;
}
if (!isset($_SESSION['session_tested']))
} 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(
exit;
}
- $PAGE = new PageBuilder($conf);
+ $PAGE = new PageBuilder($conf, 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 ($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);
+ renderPage($conf, $pluginManager, $linkDb, $history, $sessionManager);
} else {
$app->respond($response);
}
- Versioning and Branches: Versioning-and-Branches.md
- Security: Security.md
- Static analysis: Static-analysis.md
+ - Translations: Translations.md
- Theming: Theming.md
- Unit tests: Unit-tests.md
+ - Unit tests inside Docker: Unit-tests-Docker.md
- About:
- FAQ: FAQ.md
- Community & Related software: Community-&-Related-software.md
+++ /dev/null
-https://github.com/shaarli/Shaarli/issues/181 - Add Disqus or Isso comments box on a permalink page
-
- * http://posativ.org/isso/
- * install debian package https://packages.debian.org/sid/isso
- * configure server http://posativ.org/isso/docs/configuration/server/
- * configure client http://posativ.org/isso/docs/configuration/client/
- * http://posativ.org/isso/docs/quickstart/ and add `<script data-isso="//comments.example.tld/" src="//comments.example.tld/js/embed.min.js"></script>` to includes.html template; then add `<section id="isso-thread"></section>` in the linklist template where you want the comments (in the linklist_plugins loop for example)
-
-
-Problem: by default, Isso thread ID is guessed from the current url (only one thread per page).
-if we want multiple threads on a single page (shaarli linklist), we must use : the `data-isso-id` client config,
-with data-isso-id being the permalink of an item.
-
-`<section data-isso-id="aH7klxW" id="isso-thread"></section>`
-`data-isso-id: Set a custom thread id, defaults to current URI.`
-
-Problem: feature is currently broken https://github.com/posativ/isso/issues/27
-
-Another option, only display isso threads when current URL is a permalink (`\?(A-Z|a-z|0-9|-){7}`) (only show thread
-when displaying only this link), and just display a "comments" button on each linklist item. Optionally show the comment
-count on each item using the API (http://posativ.org/isso/docs/extras/api/#get-comment-count). API requests can be done
-by raintpl `{function` or client-side with js. The former should be faster if isso and shaarli are on ther same server.
-
-Showing all full isso threads in the linklist would destroy layout
-
------------------------------------------------------------
-
-http://www.git-attitude.fr/2014/11/04/git-rerere/ for the merge
array(
'type' => 'text',
'name' => 'post',
- 'placeholder' => 'URI',
+ 'placeholder' => t('URI'),
),
array(
'type' => 'submit',
- 'value' => 'Add link',
+ 'value' => t('Add link'),
'class' => 'bigbutton',
),
),
return $data;
}
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function addlink_toolbar_dummy_translation()
+{
+ // meta
+ t('Adds the addlink input on the linklist page.');
+}
-<span><a href="https://web.archive.org/web/%s"><img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="View on archive.org" alt="archive.org" /></a></span>
+<span>
+ <a href="https://web.archive.org/web/%s">
+ <img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
+ </a>
+</span>
if($value['private'] && preg_match('/^\?[a-zA-Z0-9-_@]{6}($|&|#)/', $value['real_url'])) {
continue;
}
- $archive = sprintf($archive_html, $value['url']);
+ $archive = sprintf($archive_html, $value['url'], t('View on archive.org'));
$value['link_plugin'][] = $archive;
}
return $data;
}
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function archiveorg_dummy_translation()
+{
+ // meta
+ t('For each link, add an Archive.org icon.');
+}
* and check user status with _LOGGEDIN_.
*/
+use Shaarli\Config\ConfigManager;
+
+/**
+ * In the footer hook, there is a working example of a translation extension for Shaarli.
+ *
+ * The extension must be attached to a new translation domain (i.e. NOT 'shaarli').
+ * Use case: any custom theme or non official plugin can use the translation system.
+ *
+ * See the documentation for more information.
+ */
+const EXT_TRANSLATION_DOMAIN = 'demo';
+
+/*
+ * This is not necessary, but it's easier if you don't want Poedit to mix up your translations.
+ */
+function demo_plugin_t($text, $nText = '', $nb = 1)
+{
+ return t($text, $nText, $nb, EXT_TRANSLATION_DOMAIN);
+}
+
/**
* Initialization function.
* It will be called when the plugin is loaded.
{
$conf->get('toto', 'nope');
+ if (! $conf->exists('translation.extensions.demo')) {
+ // Custom translation with the domain 'demo'
+ $conf->set('translation.extensions.demo', 'plugins/demo_plugin/languages/');
+ $conf->write(true);
+ }
+
$errors[] = 'This a demo init error.';
return $errors;
}
function hook_demo_plugin_render_footer($data)
{
// footer text
- $data['text'][] = 'Shaarli is now enhanced by the awesome demo_plugin.';
+ $data['text'][] = '<br>'. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.');
// Free elements at the end of the page.
$data['endofpage'][] = '<marquee id="demo_marquee">' .
}
return $data;
}
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function demo_dummy_translation()
+{
+ // meta
+ t('A demo plugin covering all use cases for template designers and plugin developers.');
+}
--- /dev/null
+msgid ""
+msgstr ""
+"Project-Id-Version: Demo plugin\n"
+"POT-Creation-Date: 2017-08-19 10:45+0200\n"
+"PO-Revision-Date: 2017-08-19 11:28+0200\n"
+"Last-Translator: \n"
+"Language-Team: demo\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"X-Generator: Poedit 2.0.2\n"
+"X-Poedit-Basepath: ../../..\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Poedit-KeywordsList: ;demo_plugin_t:1,2;demo_plugin_t\n"
+"X-Poedit-SourceCharset: UTF-8\n"
+"X-Poedit-SearchPath-0: .\n"
+
+#: demo_plugin.php:173
+msgid "Shaarli is now enhanced by the awesome demo_plugin."
+msgstr "Shaarli est maintenant amélioré avec le fantastique demo_plugin."
* Plugin Isso.
*/
+use Shaarli\Config\ConfigManager;
+
/**
* Display an error everywhere if the plugin is enabled without configuration.
*
- * @param $data array List of links
* @param $conf ConfigManager instance
*
* @return mixed - linklist data with Isso plugin.
{
$issoUrl = $conf->get('plugins.ISSO_SERVER');
if (empty($issoUrl)) {
- $error = 'Isso plugin error: '.
- 'Please define the "ISSO_SERVER" setting in the plugin administration page.';
+ $error = t('Isso plugin error: '.
+ 'Please define the "ISSO_SERVER" setting in the plugin administration page.');
return array($error);
}
}
return $data;
}
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function isso_dummy_translation()
+{
+ // meta
+ t('Let visitor comment your shaares on permalinks with Isso.');
+ t('Isso server URL (without \'http://\')');
+}
<div class="md_help">
- Description will be rendered with
- <a href="http://daringfireball.net/projects/markdown/syntax" title="Markdown syntax documentation">
- Markdown syntax</a>.
+ %s
+ <a href="http://daringfireball.net/projects/markdown/syntax" title="%s">
+ %s</a>.
</div>
function hook_markdown_render_editlink($data)
{
// Load help HTML into a string
- $data['edit_link_plugin'][] = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
-
+ $txt = file_get_contents(PluginManager::$PLUGINS_PATH .'/markdown/help.html');
+ $translations = [
+ t('Description will be rendered with'),
+ t('Markdown syntax documentation'),
+ t('Markdown syntax'),
+ ];
+ $data['edit_link_plugin'][] = vsprintf($txt, $translations);
// Add no markdown 'meta-tag' in tag list if it was never used, for autocompletion.
if (! in_array(NO_MD_TAG, $data['tags'])) {
$data['tags'][NO_MD_TAG] = 0;
return $processedDescription;
}
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function markdown_dummy_translation()
+{
+ // meta
+ t('Render shaare description with Markdown syntax.<br><strong>Warning</strong>:
+If your shaared descriptions contained HTML tags before enabling the markdown plugin,
+enabling it might break your page.
+See the <a href="https://github.com/shaarli/Shaarli/tree/master/plugins/markdown#html-rendering">README</a>.');
+}
$piwikUrl = $conf->get('plugins.PIWIK_URL');
$piwikSiteid = $conf->get('plugins.PIWIK_SITEID');
if (empty($piwikUrl) || empty($piwikSiteid)) {
- $error = 'Piwik plugin error: ' .
- 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.';
+ $error = t('Piwik plugin error: ' .
+ 'Please define PIWIK_URL and PIWIK_SITEID in the plugin administration page.');
return array($error);
}
}
return $data;
}
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function piwik_dummy_translation()
+{
+ // meta
+ t('A plugin that adds Piwik tracking code to Shaarli pages.');
+ t('Piwik URL');
+ t('Piwik site ID');
+}
$playvideo = array(
'attr' => array(
'href' => '#',
- 'title' => 'Video player',
+ 'title' => t('Video player'),
'id' => 'playvideos',
),
- 'html' => 'â–º Play Videos'
+ 'html' => 'â–º '. t('Play Videos')
);
$data['buttons_toolbar'][] = $playvideo;
}
return $data;
}
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function playvideos_dummy_translation()
+{
+ // meta
+ t('Add a button in the toolbar allowing to watch all videos.');
+}
*/
use pubsubhubbub\publisher\Publisher;
+use Shaarli\Config\ConfigManager;
/**
* Plugin init function - set the hub to the default appspot one.
$p = new Publisher($conf->get('plugins.PUBSUBHUB_URL'));
$p->publish_update($feeds, $httpPost);
} catch (Exception $e) {
- error_log('Could not publish to PubSubHubbub: ' . $e->getMessage());
+ error_log(sprintf(t('Could not publish to PubSubHubbub: %s'), $e->getMessage()));
}
return $data;
$context = stream_context_create($params);
$fp = @fopen($url, 'rb', false, $context);
if (!$fp) {
- throw new Exception('Could not post to '. $url);
+ throw new Exception(sprintf(t('Could not post to %s'), $url));
}
$response = @stream_get_contents($fp);
if ($response === false) {
- throw new Exception('Bad response from the hub '. $url);
+ throw new Exception(sprintf(t('Bad response from the hub %s'), $url));
}
return $response;
}
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function pubsubhubbub_dummy_translation()
+{
+ // meta
+ t('Enable PubSubHubbub feed publishing.');
+}
-description="For each link, add a QRCode icon ."
+description="For each link, add a QRCode icon."
return $data;
}
+
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function qrcode_dummy_translation()
+{
+ // meta
+ t('For each link, add a QRCode icon.');
+}
-<span><a href="%s%s" target="_blank"><img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="Save to wallabag" alt="wallabag" /></a></span>
+<span>
+ <a href="%s%s" target="_blank">
+ <img class="linklist-plugin-icon" src="%s/wallabag/wallabag.png" title="%s" alt="wallabag" />
+ </a>
+</span>
*/
require_once 'WallabagInstance.php';
+use Shaarli\Config\ConfigManager;
/**
* Init function, return an error if the server is not set.
{
$wallabagUrl = $conf->get('plugins.WALLABAG_URL');
if (empty($wallabagUrl)) {
- $error = 'Wallabag plugin error: '.
- 'Please define the "WALLABAG_URL" setting in the plugin administration page.';
+ $error = t('Wallabag plugin error: '.
+ 'Please define the "WALLABAG_URL" setting in the plugin administration page.');
return array($error);
}
}
$wallabagHtml = file_get_contents(PluginManager::$PLUGINS_PATH . '/wallabag/wallabag.html');
+ $linkTitle = t('Save to wallabag');
foreach ($data['links'] as &$value) {
$wallabag = sprintf(
$wallabagHtml,
$wallabagInstance->getWallabagUrl(),
urlencode($value['url']),
- PluginManager::$PLUGINS_PATH
+ PluginManager::$PLUGINS_PATH,
+ $linkTitle
);
$value['link_plugin'][] = $wallabag;
}
return $data;
}
+/**
+ * This function is never called, but contains translation calls for GNU gettext extraction.
+ */
+function wallabag_dummy_translation()
+{
+ // meta
+ t('For each link, add a QRCode icon.');
+ t('Wallabag API URL');
+ t('Wallabag API version (1 or 2)');
+}
+
<?php
-require_once 'application/Languages.php';
+namespace Shaarli;
+
+use Shaarli\Config\ConfigManager;
/**
* Class LanguagesTest.
*/
-class LanguagesTest extends PHPUnit_Framework_TestCase
+class LanguagesTest extends \PHPUnit_Framework_TestCase
{
+ /**
+ * @var string Config file path (without extension).
+ */
+ protected static $configFile = 'tests/utils/config/configJson';
+
+ /**
+ * @var ConfigManager
+ */
+ protected $conf;
+
+ /**
+ *
+ */
+ public function setUp()
+ {
+ $this->conf = new ConfigManager(self::$configFile);
+ }
+
+ /**
+ * Test t() with a simple non identified value.
+ */
+ public function testTranslateSingleNotIDGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ new Languages('en', $this->conf);
+ $text = 'abcdé 564 fgK';
+ $this->assertEquals($text, t($text));
+ }
+
+ /**
+ * Test t() with a simple identified value in gettext mode.
+ */
+ public function testTranslateSingleIDGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ new Languages('en', $this->conf);
+ $text = 'permalink';
+ $this->assertEquals($text, t($text));
+ }
+
+ /**
+ * Test t() with a non identified plural form in gettext mode.
+ */
+ public function testTranslatePluralNotIDGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ new Languages('en', $this->conf);
+ $text = 'sandwich';
+ $nText = 'sandwiches';
+ $this->assertEquals('sandwiches', t($text, $nText, 0));
+ $this->assertEquals('sandwich', t($text, $nText, 1));
+ $this->assertEquals('sandwiches', t($text, $nText, 2));
+ }
+
+ /**
+ * Test t() with an identified plural form in gettext mode.
+ */
+ public function testTranslatePluralIDGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ new Languages('en', $this->conf);
+ $text = 'shaare';
+ $nText = 'shaares';
+ // In english, zero is followed by plural form
+ $this->assertEquals('shaares', t($text, $nText, 0));
+ $this->assertEquals('shaare', t($text, $nText, 1));
+ $this->assertEquals('shaares', t($text, $nText, 2));
+ }
+
/**
* Test t() with a simple non identified value.
*/
- public function testTranslateSingleNotID()
+ public function testTranslateSingleNotIDPhp()
{
+ $this->conf->set('translation.mode', 'php');
+ new Languages('en', $this->conf);
$text = 'abcdé 564 fgK';
$this->assertEquals($text, t($text));
}
/**
- * Test t() with a non identified plural form.
+ * Test t() with a simple identified value in PHP mode.
*/
- public function testTranslatePluralNotID()
+ public function testTranslateSingleIDPhp()
{
- $text = '%s sandwich';
- $nText = '%s sandwiches';
- $this->assertEquals('0 sandwich', t($text, $nText));
- $this->assertEquals('1 sandwich', t($text, $nText, 1));
- $this->assertEquals('2 sandwiches', t($text, $nText, 2));
+ $this->conf->set('translation.mode', 'php');
+ new Languages('en', $this->conf);
+ $text = 'permalink';
+ $this->assertEquals($text, t($text));
}
/**
- * Test t() with a non identified invalid plural form.
+ * Test t() with a non identified plural form in PHP mode.
*/
- public function testTranslatePluralNotIDInvalid()
+ public function testTranslatePluralNotIDPhp()
{
+ $this->conf->set('translation.mode', 'php');
+ new Languages('en', $this->conf);
$text = 'sandwich';
$nText = 'sandwiches';
+ $this->assertEquals('sandwiches', t($text, $nText, 0));
$this->assertEquals('sandwich', t($text, $nText, 1));
$this->assertEquals('sandwiches', t($text, $nText, 2));
}
+
+ /**
+ * Test t() with an identified plural form in PHP mode.
+ */
+ public function testTranslatePluralIDPhp()
+ {
+ $this->conf->set('translation.mode', 'php');
+ new Languages('en', $this->conf);
+ $text = 'shaare';
+ $nText = 'shaares';
+ // In english, zero is followed by plural form
+ $this->assertEquals('shaares', t($text, $nText, 0));
+ $this->assertEquals('shaare', t($text, $nText, 1));
+ $this->assertEquals('shaares', t($text, $nText, 2));
+ }
+
+ /**
+ * Test t() with an invalid language set in the configuration in gettext mode.
+ */
+ public function testTranslateWithInvalidConfLanguageGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ $this->conf->set('translation.language', 'nope');
+ new Languages('fr', $this->conf);
+ $text = 'grumble';
+ $this->assertEquals($text, t($text));
+ }
+
+ /**
+ * Test t() with an invalid language set in the configuration in PHP mode.
+ */
+ public function testTranslateWithInvalidConfLanguagePhp()
+ {
+ $this->conf->set('translation.mode', 'php');
+ $this->conf->set('translation.language', 'nope');
+ new Languages('fr', $this->conf);
+ $text = 'grumble';
+ $this->assertEquals($text, t($text));
+ }
+
+ /**
+ * Test t() with an invalid language set with auto language in gettext mode.
+ */
+ public function testTranslateWithInvalidAutoLanguageGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ new Languages('nope', $this->conf);
+ $text = 'grumble';
+ $this->assertEquals($text, t($text));
+ }
+
+ /**
+ * Test t() with an invalid language set with auto language in PHP mode.
+ */
+ public function testTranslateWithInvalidAutoLanguagePhp()
+ {
+ $this->conf->set('translation.mode', 'php');
+ new Languages('nope', $this->conf);
+ $text = 'grumble';
+ $this->assertEquals($text, t($text));
+ }
+
+ /**
+ * Test t() with an extension language file in gettext mode
+ */
+ public function testTranslationExtensionGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
+ new Languages('en', $this->conf);
+ $txt = 'car'; // ignore me poedit
+ $this->assertEquals('car', t($txt, $txt, 1, 'test'));
+ $this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
+ }
+
+ /**
+ * Test t() with an extension language file in PHP mode
+ */
+ public function testTranslationExtensionPhp()
+ {
+ $this->conf->set('translation.mode', 'php');
+ $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
+ new Languages('en', $this->conf);
+ $txt = 'car'; // ignore me poedit
+ $this->assertEquals('car', t($txt, $txt, 1, 'test'));
+ $this->assertEquals('Search', t('Search', 'Search', 1, 'test'));
+ }
}
$expectedText = 'stuff <a href="http://hello.there/is=someone#here">http://hello.there/is=someone#here</a> otherstuff';
$processedText = text2clickable($text, '');
$this->assertEquals($expectedText, $processedText);
+
+ $text = 'stuff http://hello.there/is=someone#here(please) otherstuff';
+ $expectedText = 'stuff <a href="http://hello.there/is=someone#here(please)">http://hello.there/is=someone#here(please)</a> otherstuff';
+ $processedText = text2clickable($text, '');
+ $this->assertEquals($expectedText, $processedText);
+
+ $text = 'stuff http://hello.there/is=someone#here(please)&no otherstuff';
+ $expectedText = 'stuff <a href="http://hello.there/is=someone#here(please)&no">http://hello.there/is=someone#here(please)&no</a> otherstuff';
+ $processedText = text2clickable($text, '');
+ $this->assertEquals($expectedText, $processedText);
}
/**
public function testImportInternetExplorerEncoding()
{
$files = file2array('internet_explorer_encoding.htm');
- $this->assertEquals(
- 'File internet_explorer_encoding.htm (356 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File internet_explorer_encoding.htm (356 bytes) was successfully processed in %d seconds:'
.' 1 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
);
public function testImportNested()
{
$files = file2array('netscape_nested.htm');
- $this->assertEquals(
- 'File netscape_nested.htm (1337 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_nested.htm (1337 bytes) was successfully processed in %d seconds:'
.' 8 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
);
public function testImportDefaultPrivacyNoPost()
{
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import([], $files, $this->linkDb, $this->conf, $this->history)
);
{
$post = array('privacy' => 'default');
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
{
$post = array('privacy' => 'public');
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
{
$post = array('privacy' => 'private');
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
// import links as private
$post = array('privacy' => 'private');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
'privacy' => 'public',
'overwrite' => 'true'
);
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 2 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
// import links as public
$post = array('privacy' => 'public');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
'privacy' => 'private',
'overwrite' => 'true'
);
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 2 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
{
$post = array('privacy' => 'public');
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
// re-import as private, DO NOT enable overwriting
$post = array('privacy' => 'private');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 0 links imported, 0 links overwritten, 2 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
'default_tags' => 'tag1,tag2 tag3'
);
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
'default_tags' => 'tag1&,tag2 "tag3"'
);
$files = file2array('netscape_basic.htm');
- $this->assertEquals(
- 'File netscape_basic.htm (482 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File netscape_basic.htm (482 bytes) was successfully processed in %d seconds:'
.' 2 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history)
);
public function testImportSameDate()
{
$files = file2array('same_date.htm');
- $this->assertEquals(
- 'File same_date.htm (453 bytes) was successfully processed:'
+ $this->assertStringMatchesFormat(
+ 'File same_date.htm (453 bytes) was successfully processed in %d seconds:'
.' 3 links imported, 0 links overwritten, 0 links skipped.',
NetscapeBookmarkUtils::import(array(), $files, $this->linkDb, $this->conf, $this->history)
);
'overwrite' => 'true',
];
$files = file2array('netscape_basic.htm');
- $nbLinks = 2;
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
$history = $this->history->getHistory();
- $this->assertEquals($nbLinks, count($history));
- foreach ($history as $value) {
- $this->assertEquals(History::CREATED, $value['event']);
- $this->assertTrue(new DateTime('-5 seconds') < $value['datetime']);
- $this->assertTrue(is_int($value['id']));
- }
+ $this->assertEquals(1, count($history));
+ $this->assertEquals(History::IMPORT, $history[0]['event']);
+ $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
// re-import as private, enable overwriting
NetscapeBookmarkUtils::import($post, $files, $this->linkDb, $this->conf, $this->history);
$history = $this->history->getHistory();
- $this->assertEquals($nbLinks * 2, count($history));
- for ($i = 0 ; $i < $nbLinks ; $i++) {
- $this->assertEquals(History::UPDATED, $history[$i]['event']);
- $this->assertTrue(new DateTime('-5 seconds') < $history[$i]['datetime']);
- $this->assertTrue(is_int($history[$i]['id']));
- }
+ $this->assertEquals(2, count($history));
+ $this->assertEquals(History::IMPORT, $history[0]['event']);
+ $this->assertTrue(new DateTime('-5 seconds') < $history[0]['datetime']);
+ $this->assertEquals(History::IMPORT, $history[1]['event']);
+ $this->assertTrue(new DateTime('-5 seconds') < $history[1]['datetime']);
}
}
--- /dev/null
+<?php
+// Initialize reference data _before_ PHPUnit starts a session
+require_once 'tests/utils/ReferenceSessionIdHashes.php';
+ReferenceSessionIdHashes::genAllHashes();
+
+use \Shaarli\SessionManager;
+use \PHPUnit\Framework\TestCase;
+
+
+/**
+ * Fake ConfigManager
+ */
+class FakeConfigManager
+{
+ public static function get($key)
+ {
+ return $key;
+ }
+}
+
+
+/**
+ * Test coverage for SessionManager
+ */
+class SessionManagerTest extends TestCase
+{
+ // Session ID hashes
+ protected static $sidHashes = null;
+
+ /**
+ * Assign reference data
+ */
+ public static function setUpBeforeClass()
+ {
+ self::$sidHashes = ReferenceSessionIdHashes::getHashes();
+ }
+
+ /**
+ * Generate a session token
+ */
+ public function testGenerateToken()
+ {
+ $session = [];
+ $conf = new FakeConfigManager();
+ $sessionManager = new SessionManager($session, $conf);
+
+ $token = $sessionManager->generateToken();
+
+ $this->assertEquals(1, $session['tokens'][$token]);
+ $this->assertEquals(40, strlen($token));
+ }
+
+ /**
+ * Check a session token
+ */
+ public function testCheckToken()
+ {
+ $token = '4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b';
+ $session = [
+ 'tokens' => [
+ $token => 1,
+ ],
+ ];
+ $conf = new FakeConfigManager();
+ $sessionManager = new SessionManager($session, $conf);
+
+
+ // check and destroy the token
+ $this->assertTrue($sessionManager->checkToken($token));
+ $this->assertFalse(isset($session['tokens'][$token]));
+
+ // ensure the token has been destroyed
+ $this->assertFalse($sessionManager->checkToken($token));
+ }
+
+ /**
+ * Generate and check a session token
+ */
+ public function testGenerateAndCheckToken()
+ {
+ $session = [];
+ $conf = new FakeConfigManager();
+ $sessionManager = new SessionManager($session, $conf);
+
+ $token = $sessionManager->generateToken();
+
+ // ensure a token has been generated
+ $this->assertEquals(1, $session['tokens'][$token]);
+ $this->assertEquals(40, strlen($token));
+
+ // check and destroy the token
+ $this->assertTrue($sessionManager->checkToken($token));
+ $this->assertFalse(isset($session['tokens'][$token]));
+
+ // ensure the token has been destroyed
+ $this->assertFalse($sessionManager->checkToken($token));
+ }
+
+ /**
+ * Check an invalid session token
+ */
+ public function testCheckInvalidToken()
+ {
+ $session = [];
+ $conf = new FakeConfigManager();
+ $sessionManager = new SessionManager($session, $conf);
+
+ $this->assertFalse($sessionManager->checkToken('4dccc3a45ad9d03e5542b90c37d8db6d10f2b38b'));
+ }
+
+ /**
+ * Test SessionManager::checkId with a valid ID - TEST ALL THE HASHES!
+ *
+ * This tests extensively covers all hash algorithms / bit representations
+ */
+ public function testIsAnyHashSessionIdValid()
+ {
+ foreach (self::$sidHashes as $algo => $bpcs) {
+ foreach ($bpcs as $bpc => $hash) {
+ $this->assertTrue(SessionManager::checkId($hash));
+ }
+ }
+ }
+
+ /**
+ * Test checkId with a valid ID - SHA-1 hashes
+ */
+ public function testIsSha1SessionIdValid()
+ {
+ $this->assertTrue(SessionManager::checkId(sha1('shaarli')));
+ }
+
+ /**
+ * Test checkId with a valid ID - SHA-256 hashes
+ */
+ public function testIsSha256SessionIdValid()
+ {
+ $this->assertTrue(SessionManager::checkId(hash('sha256', 'shaarli')));
+ }
+
+ /**
+ * Test checkId with a valid ID - SHA-512 hashes
+ */
+ public function testIsSha512SessionIdValid()
+ {
+ $this->assertTrue(SessionManager::checkId(hash('sha512', 'shaarli')));
+ }
+
+ /**
+ * Test checkId with invalid IDs.
+ */
+ public function testIsSessionIdInvalid()
+ {
+ $this->assertFalse(SessionManager::checkId(''));
+ $this->assertFalse(SessionManager::checkId([]));
+ $this->assertFalse(
+ SessionManager::checkId('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
+ );
+ }
+}
require_once 'application/Utils.php';
require_once 'application/Languages.php';
-require_once 'tests/utils/ReferenceSessionIdHashes.php';
-
-// Initialize reference data before PHPUnit starts a session
-ReferenceSessionIdHashes::genAllHashes();
/**
*/
class UtilsTest extends PHPUnit_Framework_TestCase
{
- // Session ID hashes
- protected static $sidHashes = null;
-
// Log file
protected static $testLogFile = 'tests.log';
*/
protected static $defaultTimeZone;
-
/**
* Assign reference data
*/
public static function setUpBeforeClass()
{
- self::$sidHashes = ReferenceSessionIdHashes::getHashes();
self::$defaultTimeZone = date_default_timezone_get();
// Timezone without DST for test consistency
date_default_timezone_set('Africa/Nairobi');
$this->assertEquals('?', generateLocation($ref, 'localhost'));
}
- /**
- * Test is_session_id_valid with a valid ID - TEST ALL THE HASHES!
- *
- * This tests extensively covers all hash algorithms / bit representations
- */
- public function testIsAnyHashSessionIdValid()
- {
- foreach (self::$sidHashes as $algo => $bpcs) {
- foreach ($bpcs as $bpc => $hash) {
- $this->assertTrue(is_session_id_valid($hash));
- }
- }
- }
- /**
- * Test is_session_id_valid with a valid ID - SHA-1 hashes
- */
- public function testIsSha1SessionIdValid()
- {
- $this->assertTrue(is_session_id_valid(sha1('shaarli')));
- }
-
- /**
- * Test is_session_id_valid with a valid ID - SHA-256 hashes
- */
- public function testIsSha256SessionIdValid()
- {
- $this->assertTrue(is_session_id_valid(hash('sha256', 'shaarli')));
- }
-
- /**
- * Test is_session_id_valid with a valid ID - SHA-512 hashes
- */
- public function testIsSha512SessionIdValid()
- {
- $this->assertTrue(is_session_id_valid(hash('sha512', 'shaarli')));
- }
-
- /**
- * Test is_session_id_valid with invalid IDs.
- */
- public function testIsSessionIdInvalid()
- {
- $this->assertFalse(is_session_id_valid(''));
- $this->assertFalse(is_session_id_valid(array()));
- $this->assertFalse(
- is_session_id_valid('c0ZqcWF3VFE2NmJBdm1HMVQ0ZHJ3UmZPbTFsNGhkNHI=')
- );
- }
-
/**
* Test generateSecretApi.
*/
*/
public function testHumanBytes()
{
- $this->assertEquals('2kiB', human_bytes(2 * 1024));
- $this->assertEquals('2kiB', human_bytes(strval(2 * 1024)));
- $this->assertEquals('2MiB', human_bytes(2 * (pow(1024, 2))));
- $this->assertEquals('2MiB', human_bytes(strval(2 * (pow(1024, 2)))));
- $this->assertEquals('2GiB', human_bytes(2 * (pow(1024, 3))));
- $this->assertEquals('2GiB', human_bytes(strval(2 * (pow(1024, 3)))));
- $this->assertEquals('374B', human_bytes(374));
- $this->assertEquals('374B', human_bytes('374'));
- $this->assertEquals('232kiB', human_bytes(237481));
- $this->assertEquals('Unlimited', human_bytes('0'));
- $this->assertEquals('Unlimited', human_bytes(0));
- $this->assertEquals('Setting not set', human_bytes(''));
+ $this->assertEquals('2'. t('kiB'), human_bytes(2 * 1024));
+ $this->assertEquals('2'. t('kiB'), human_bytes(strval(2 * 1024)));
+ $this->assertEquals('2'. t('MiB'), human_bytes(2 * (pow(1024, 2))));
+ $this->assertEquals('2'. t('MiB'), human_bytes(strval(2 * (pow(1024, 2)))));
+ $this->assertEquals('2'. t('GiB'), human_bytes(2 * (pow(1024, 3))));
+ $this->assertEquals('2'. t('GiB'), human_bytes(strval(2 * (pow(1024, 3)))));
+ $this->assertEquals('374'. t('B'), human_bytes(374));
+ $this->assertEquals('374'. t('B'), human_bytes('374'));
+ $this->assertEquals('232'. t('kiB'), human_bytes(237481));
+ $this->assertEquals(t('Unlimited'), human_bytes('0'));
+ $this->assertEquals(t('Unlimited'), human_bytes(0));
+ $this->assertEquals(t('Setting not set'), human_bytes(''));
}
/**
*/
public function testGetMaxUploadSize()
{
- $this->assertEquals('1MiB', get_max_upload_size(2097152, '1024k'));
- $this->assertEquals('1MiB', get_max_upload_size('1m', '2m'));
- $this->assertEquals('100B', get_max_upload_size(100, 100));
+ $this->assertEquals('1'. t('MiB'), get_max_upload_size(2097152, '1024k'));
+ $this->assertEquals('1'. t('MiB'), get_max_upload_size('1m', '2m'));
+ $this->assertEquals('100'. t('B'), get_max_upload_size(100, 100));
}
/**
$this->assertEquals(1, count($data));
$this->assertEquals(41, $data[0]['id']);
$this->assertEquals(self::NB_FIELDS_LINK, count($data[0]));
+
+ // wildcard: placeholder at the start
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'GET',
+ 'QUERY_STRING' => 'searchtags=*Tuff',
+ ]);
+ $request = Request::createFromEnvironment($env);
+ $response = $this->controller->getLinks($request, new Response());
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(2, count($data));
+ $this->assertEquals(41, $data[0]['id']);
+
+ // wildcard: placeholder at the end
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'GET',
+ 'QUERY_STRING' => 'searchtags=c*',
+ ]);
+ $request = Request::createFromEnvironment($env);
+ $response = $this->controller->getLinks($request, new Response());
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(4, count($data));
+ $this->assertEquals(6, $data[0]['id']);
+
+ // wildcard: placeholder at the middle
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'GET',
+ 'QUERY_STRING' => 'searchtags=w*b',
+ ]);
+ $request = Request::createFromEnvironment($env);
+ $response = $this->controller->getLinks($request, new Response());
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(4, count($data));
+ $this->assertEquals(6, $data[0]['id']);
+
+ // wildcard: match all
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'GET',
+ 'QUERY_STRING' => 'searchtags=*',
+ ]);
+ $request = Request::createFromEnvironment($env);
+ $response = $this->controller->getLinks($request, new Response());
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(9, count($data));
+ $this->assertEquals(41, $data[0]['id']);
+
+ // wildcard: optional ('*' does not need to expand)
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'GET',
+ 'QUERY_STRING' => 'searchtags=*stuff*',
+ ]);
+ $request = Request::createFromEnvironment($env);
+ $response = $this->controller->getLinks($request, new Response());
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(2, count($data));
+ $this->assertEquals(41, $data[0]['id']);
+
+ // wildcard: exclusions
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'GET',
+ 'QUERY_STRING' => 'searchtags=*a*+-*e*',
+ ]);
+ $request = Request::createFromEnvironment($env);
+ $response = $this->controller->getLinks($request, new Response());
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(1, count($data));
+ $this->assertEquals(41, $data[0]['id']); // finds '#hashtag' in descr.
+
+ // wildcard: exclude all
+ $env = Environment::mock([
+ 'REQUEST_METHOD' => 'GET',
+ 'QUERY_STRING' => 'searchtags=-*',
+ ]);
+ $request = Request::createFromEnvironment($env);
+ $response = $this->controller->getLinks($request, new Response());
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode((string) $response->getBody(), true);
+ $this->assertEquals(0, count($data));
}
/**
--- /dev/null
+<?php
+
+require_once 'vendor/autoload.php';
+
+$conf = new \Shaarli\Config\ConfigManager('tests/utils/config/configJson');
+new \Shaarli\Languages('en', $conf);
<?php
-if (! empty('UT_LOCALE')) {
+require_once 'tests/bootstrap.php';
+
+if (! empty(getenv('UT_LOCALE'))) {
setlocale(LC_ALL, getenv('UT_LOCALE'));
}
-
-require_once 'vendor/autoload.php';
-
}
/**
- * Test autoLocale with multiples value, the second one is valid
+ * Test autoLocale with multiples value, the second one is available
*/
- public function testAutoLocaleMultipleSecondValid()
+ public function testAutoLocaleMultipleSecondAvailable()
{
$current = setlocale(LC_ALL, 0);
- $header = 'pt_BR,fr-fr';
+ $header = 'mag_IN,fr-fr';
autoLocale($header);
$this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0));
}
/**
- * Test autoLocale with an invalid value: defaults to en_US.
+ * Test autoLocale with an unavailable value: defaults to en_US.
*/
- public function testAutoLocaleInvalid()
+ public function testAutoLocaleUnavailable()
{
$current = setlocale(LC_ALL, 0);
- autoLocale('pt_BR');
+ autoLocale('mag_IN');
$this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
setlocale(LC_ALL, $current);
}
/**
- * Test autoLocale with multiples value, the second one is valid
+ * Test autoLocale with multiples value, the second one is available
*/
- public function testAutoLocaleMultipleSecondValid()
+ public function testAutoLocaleMultipleSecondAvailable()
{
$current = setlocale(LC_ALL, 0);
- $header = 'pt_BR,fr-fr';
+ $header = 'mag_IN,fr-fr';
autoLocale($header);
$this->assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0));
}
/**
- * Test autoLocale with an invalid value: defaults to en_US.
+ * Test autoLocale with an unavailable value: defaults to en_US.
*/
- public function testAutoLocaleInvalid()
+ public function testAutoLocaleUnavailable()
{
$current = setlocale(LC_ALL, 0);
- autoLocale('pt_BR');
+ autoLocale('mag_IN');
$this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
setlocale(LC_ALL, $current);
--- /dev/null
+<?php
+
+
+namespace Shaarli;
+
+
+use Shaarli\Config\ConfigManager;
+
+/**
+ * Class LanguagesFrTest
+ *
+ * Test the translation system in PHP and gettext mode with French language.
+ *
+ * @package Shaarli
+ */
+class LanguagesFrTest extends \PHPUnit_Framework_TestCase
+{
+ /**
+ * @var string Config file path (without extension).
+ */
+ protected static $configFile = 'tests/utils/config/configJson';
+
+ /**
+ * @var ConfigManager
+ */
+ protected $conf;
+
+ /**
+ * Init: force French
+ */
+ public function setUp()
+ {
+ $this->conf = new ConfigManager(self::$configFile);
+ $this->conf->set('translation.language', 'fr');
+ }
+
+ /**
+ * Reset the locale since gettext seems to mess with it, making it too long
+ */
+ public static function tearDownAfterClass()
+ {
+ if (! empty(getenv('UT_LOCALE'))) {
+ setlocale(LC_ALL, getenv('UT_LOCALE'));
+ }
+ }
+
+ /**
+ * Test t() with a simple non identified value.
+ */
+ public function testTranslateSingleNotIDGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ new Languages('en', $this->conf);
+ $text = 'abcdé 564 fgK';
+ $this->assertEquals($text, t($text));
+ }
+
+ /**
+ * Test t() with a simple identified value in gettext mode.
+ */
+ public function testTranslateSingleIDGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ new Languages('en', $this->conf);
+ $text = 'permalink';
+ $this->assertEquals('permalien', t($text));
+ }
+
+ /**
+ * Test t() with a non identified plural form in gettext mode.
+ */
+ public function testTranslatePluralNotIDGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ new Languages('en', $this->conf);
+ $text = 'sandwich';
+ $nText = 'sandwiches';
+ // Not ID, so English fallback, and in english, plural 0
+ $this->assertEquals('sandwiches', t($text, $nText, 0));
+ $this->assertEquals('sandwich', t($text, $nText, 1));
+ $this->assertEquals('sandwiches', t($text, $nText, 2));
+ }
+
+ /**
+ * Test t() with an identified plural form in gettext mode.
+ */
+ public function testTranslatePluralIDGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ new Languages('en', $this->conf);
+ $text = 'shaare';
+ $nText = 'shaares';
+ $this->assertEquals('shaare', t($text, $nText, 0));
+ $this->assertEquals('shaare', t($text, $nText, 1));
+ $this->assertEquals('shaares', t($text, $nText, 2));
+ }
+
+ /**
+ * Test t() with a simple non identified value.
+ */
+ public function testTranslateSingleNotIDPhp()
+ {
+ $this->conf->set('translation.mode', 'php');
+ new Languages('en', $this->conf);
+ $text = 'abcdé 564 fgK';
+ $this->assertEquals($text, t($text));
+ }
+
+ /**
+ * Test t() with a simple identified value in PHP mode.
+ */
+ public function testTranslateSingleIDPhp()
+ {
+ $this->conf->set('translation.mode', 'php');
+ new Languages('en', $this->conf);
+ $text = 'permalink';
+ $this->assertEquals('permalien', t($text));
+ }
+
+ /**
+ * Test t() with a non identified plural form in PHP mode.
+ */
+ public function testTranslatePluralNotIDPhp()
+ {
+ $this->conf->set('translation.mode', 'php');
+ new Languages('en', $this->conf);
+ $text = 'sandwich';
+ $nText = 'sandwiches';
+ // Not ID, so English fallback, and in english, plural 0
+ $this->assertEquals('sandwiches', t($text, $nText, 0));
+ $this->assertEquals('sandwich', t($text, $nText, 1));
+ $this->assertEquals('sandwiches', t($text, $nText, 2));
+ }
+
+ /**
+ * Test t() with an identified plural form in PHP mode.
+ */
+ public function testTranslatePluralIDPhp()
+ {
+ $this->conf->set('translation.mode', 'php');
+ new Languages('en', $this->conf);
+ $text = 'shaare';
+ $nText = 'shaares';
+ // In english, zero is followed by plural form
+ $this->assertEquals('shaare', t($text, $nText, 0));
+ $this->assertEquals('shaare', t($text, $nText, 1));
+ $this->assertEquals('shaares', t($text, $nText, 2));
+ }
+
+ /**
+ * Test t() with an extension language file in gettext mode
+ */
+ public function testTranslationExtensionGettext()
+ {
+ $this->conf->set('translation.mode', 'gettext');
+ $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
+ new Languages('en', $this->conf);
+ $txt = 'car'; // ignore me poedit
+ $this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
+ $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
+ }
+
+ /**
+ * Test t() with an extension language file in PHP mode
+ */
+ public function testTranslationExtensionPhp()
+ {
+ $this->conf->set('translation.mode', 'php');
+ $this->conf->set('translation.extensions.test', 'tests/utils/languages/');
+ new Languages('en', $this->conf);
+ $txt = 'car'; // ignore me poedit
+ $this->assertEquals('voiture', t($txt, $txt, 1, 'test'));
+ $this->assertEquals('Fouille', t('Search', 'Search', 1, 'test'));
+ }
+}
}
/**
- * Test autoLocale with multiples value, the second one is valid
+ * Test autoLocale with multiples value, the second one is available
*/
- public function testAutoLocaleMultipleSecondValid()
+ public function testAutoLocaleMultipleSecondAvailable()
{
$current = setlocale(LC_ALL, 0);
- $header = 'pt_BR,de-de';
+ $header = 'mag_IN,de-de';
autoLocale($header);
$this->assertEquals('de_DE.utf8', setlocale(LC_ALL, 0));
}
/**
- * Test autoLocale with an invalid value: defaults to en_US.
+ * Test autoLocale with an unavailable value: defaults to en_US.
*/
- public function testAutoLocaleInvalid()
+ public function testAutoLocaleUnavailable()
{
$current = setlocale(LC_ALL, 0);
- autoLocale('pt_BR');
+ autoLocale('mag_IN');
$this->assertEquals('en_US.utf8', setlocale(LC_ALL, 0));
setlocale(LC_ALL, $current);
--- /dev/null
+msgid ""
+msgstr ""
+"Project-Id-Version: Extension test\n"
+"POT-Creation-Date: 2017-05-20 13:54+0200\n"
+"PO-Revision-Date: 2017-05-20 14:16+0200\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"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+"X-Generator: Poedit 2.0.1\n"
+
+msgid "car"
+msgstr "voiture"
+
+msgid "Search"
+msgstr "Fouille"
</div>
</form>
- <p>You can also edit tags in the <a href="?do=taglist&sort=usage">tag list</a>.</p>
+ <p>{'You can also edit tags in the'|t} <a href="?do=taglist&sort=usage">{'tag list'|t}</a>.</p>
</div>
</div>
{include="page.footer"}
</div>
</div>
</div>
+ <div class="pure-g">
+ <div class="pure-u-lg-{$ratioLabel} pure-u-1">
+ <div class="form-label">
+ <label for="language">
+ <span class="label-name">{'Language'|t}</span>
+ </label>
+ </div>
+ </div>
+ <div class="pure-u-lg-{$ratioInput} pure-u-1">
+ <div class="form-input">
+ <select name="language" id="language" class="align">
+ {loop="$languages"}
+ <option value="{$key}"
+ {if="$key===$language"}
+ selected="selected"
+ {/if}
+ >
+ {$value}
+ </option>
+ {/loop}
+ </select>
+ </div>
+ </div>
+ </div>
<div class="pure-g">
<div class="pure-u-lg-{$ratioLabel} pure-u-1 ">
<div class="form-label">
}
.linklist-item-title a:visited .linklist-link {
- color: #555555;
+ color: #2a4c41;
}
.linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{
<div class="center" id="import-field">
<input type="hidden" name="MAX_FILE_SIZE" value="{$maxfilesize}">
<input type="file" name="filetoupload">
- <p><br>Maximum size allowed: <strong>{$maxfilesizeHuman}</strong></p>
+ <p><br>{'Maximum size allowed:'|t} <strong>{$maxfilesizeHuman}</strong></p>
</div>
<div class="pure-g">
<div class="radio-buttons">
<div>
<input type="radio" name="privacy" value="default" checked="checked">
- Use values from the imported file, default to public
+ {'Use values from the imported file, default to public'|t}
</div>
<div>
<input type="radio" name="privacy" value="private">
- Import all bookmarks as private
+ {'Import all bookmarks as private'|t}
</div>
<div>
<input type="radio" name="privacy" value="public">
- Import all bookmarks as public
+ {'Import all bookmarks as public'|t}
</div>
</div>
</div>
<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" />
<link href="img/favicon.png" rel="shortcut icon" type="image/png" />
-<link type="text/css" rel="stylesheet" href="css/pure.min.css" />
-<link type="text/css" rel="stylesheet" href="css/grids-responsive.min.css">
-<link type="text/css" rel="stylesheet" href="css/pure-extras.css">
-<link type="text/css" rel="stylesheet" href="css/font-awesome.min.css" />
-<link type="text/css" rel="stylesheet" href="inc/awesomplete.css#" />
-<link type="text/css" rel="stylesheet" href="css/shaarli.css" />
+<link type="text/css" rel="stylesheet" href="css/pure.min.css?v={$version_hash}" />
+<link type="text/css" rel="stylesheet" href="css/grids-responsive.min.css?v={$version_hash}">
+<link type="text/css" rel="stylesheet" href="css/pure-extras.css?v={$version_hash}">
+<link type="text/css" rel="stylesheet" href="css/font-awesome.min.css?v={$version_hash}" />
+<link type="text/css" rel="stylesheet" href="inc/awesomplete.css?v={$version_hash}#" />
+<link type="text/css" rel="stylesheet" href="css/shaarli.css?v={$version_hash}" />
{if="is_file('data/user.css')"}
<link type="text/css" rel="stylesheet" href="data/user.css#" />
{/if}
{loop="$plugins_includes.css_files"}
- <link type="text/css" rel="stylesheet" href="{$value}#"/>
+ <link type="text/css" rel="stylesheet" href="{$value}?v={$version_hash}#"/>
{/loop}
<link rel="search" type="application/opensearchdescription+xml" href="?do=opensearch#" title="Shaarli search - {$shaarlititle}"/>
\ No newline at end of file
</div>
</div>
+ <div class="pure-g">
+ <div class="pure-u-lg-{$ratioLabel} pure-u-1">
+ <div class="form-label">
+ <label for="language">
+ <span class="label-name">{'Language'|t}</span>
+ </label>
+ </div>
+ </div>
+ <div class="pure-u-lg-{$ratioInput} pure-u-1">
+ <div class="form-input">
+ <select name="language" id="language" class="align">
+ {loop="$languages"}
+ <option value="{$key}">
+ {$value}
+ </option>
+ {/loop}
+ </select>
+ </div>
+ </div>
+ </div>
+
<div class="pure-g">
<div class="pure-u-lg-{$ratioLabel} pure-u-1">
<div class="form-label">
});
foldAllButton.firstElementChild.classList.toggle('fa-chevron-down');
foldAllButton.firstElementChild.classList.toggle('fa-chevron-up');
+ foldAllButton.title = state === 'down'
+ ? document.getElementById('translation-fold-all').innerHTML
+ : document.getElementById('translation-expand-all').innerHTML
});
});
}
{
// Switch fold/expand - up = fold
if (button.classList.contains('fa-chevron-up')) {
- button.title = 'Expand';
+ button.title = document.getElementById('translation-expand').innerHTML;
if (description != null) {
description.style.display = 'none';
}
}
}
else {
- button.title = 'Fold';
+ button.title = document.getElementById('translation-fold').innerHTML;
if (description != null) {
description.style.display = 'block';
}
var deleteLinks = document.querySelectorAll('.confirm-delete');
[].forEach.call(deleteLinks, function(deleteLink) {
deleteLink.addEventListener('click', function(event) {
- if(! confirm('Are you sure you want to delete this link ?')) {
+ if(! confirm(document.getElementById('translation-delete-link').innerHTML)) {
event.preventDefault();
}
});
};
function init () {
function resize () {
+ /* Fix jumpy resizing: https://stackoverflow.com/a/18262927/1484919 */
+ var scrollTop = window.pageYOffset ||
+ (document.documentElement || document.body.parentNode || document.body).scrollTop;
+
description.style.height = 'auto';
description.style.height = description.scrollHeight+10+'px';
+
+ window.scrollTo(0, scrollTop);
}
/* 0-timeout to get the already changed text */
function delayedResize () {
// Keeping the data separated (ie. not in the DOM) so that it's maintainable and diffable.
var data = {
name: title,
- description: "The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community.",
+ description: document.getElementById('translation-delete-link').innerHTML,
author: "Shaarli",
version: "1.0.0",
<div class="pure-g pure-alert pure-alert-success search-result">
<div class="pure-u-2-24"></div>
<div class="pure-u-20-24">
- {function="t('%s result', '%s results', $result_count)"}
+ {function="sprintf(t('%s result', '%s results', $result_count), $result_count)"}
{if="!empty($search_term)"}
{'for'|t} <em><strong>{$search_term}</strong></em>
{/if}
<div class="pure-g">
<div class="pure-u-lg-2-24 pure-u-1-24"></div>
<div class="pure-u-lg-20-24 pure-u-22-24">
+ {ignore}Set translation here, for performances{/ignore}
+ {$strPrivate=t('Private')}
+ {$strEdit=t('Edit')}
+ {$strDelete=t('Delete')}
+ {$strFold=t('Fold')}
+ {$strEdited=t('Edited: ')}
+ {$strPermalink=t('Permalink')}
+ {$strPermalinkLc=t('permalink')}
+ {$strAddTag=t('Add tag')}
+ {ignore}End of translations{/ignore}
{loop="links"}
<div class="anchor" id="{$value.shorturl}"></div>
<div class="linklist-item linklist-item{if="$value.class"} {$value.class}{/if}" data-id="{$value.id}">
{if="isLoggedIn()"}
<div class="linklist-item-editbuttons">
{if="$value.private"}
- <span class="label label-private">{'Private'|t}</span>
+ <span class="label label-private">{$strPrivate}</span>
{/if}
<input type="checkbox" class="delete-checkbox" value="{$value.id}">
<!-- FIXME! JS translation -->
- <a href="?edit_link={$value.id}" title="{'Edit'|t}"><i class="fa fa-pencil-square-o edit-link"></i></a>
- <a href="#" title="{'Fold'|t}" class="fold-button"><i class="fa fa-chevron-up"></i></a>
+ <a href="?edit_link={$value.id}" title="{$strEdit}"><i class="fa fa-pencil-square-o edit-link"></i></a>
+ <a href="#" title="{$strFold}" class="fold-button"><i class="fa fa-chevron-up"></i></a>
</div>
{/if}
<i class="fa fa-tags"></i>
{$tag_counter=count($value.taglist)}
{loop="value.taglist"}
- <span class="label label-tag" title="Add tag">
+ <span class="label label-tag" title="{$strAddTag}">
<a href="?addtag={$value|urlencode}">{$value}</a>
</span>
{if="$tag_counter - 1 != $counter"}·{/if}
<div class="pure-g">
<div class="linklist-item-infos-dateblock pure-u-lg-3-8 pure-u-1">
- <a href="?{$value.shorturl}" title="{'Permalink'|t}">
+ <a href="?{$value.shorturl}" title="{$strPermalink}">
{if="!$hide_timestamps || isLoggedIn()"}
- {$updated=$value.updated_timestamp ? 'Edited: '. format_date($value.updated) : 'Permalink'}
+ {$updated=$value.updated_timestamp ? $strEdited. format_date($value.updated) : $strPermalink}
<span class="linkdate" title="{$updated}">
<i class="fa fa-clock-o"></i>
{$value.created|format_date}
·
</span>
{/if}
- {'permalink'|t}
+ {$strPermalinkLc}
</a>
<div class="pure-u-0 pure-u-lg-visible">
</a>
{if="isLoggedIn()"}
<a href="?delete_link&lf_linkdate={$value.id}&token={$token}"
- title="{'Delete'|t}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
+ title="{$strDelete}" class="delete-link pure-u-0 pure-u-lg-visible confirm-delete">
<i class="fa fa-trash"></i>
</a>
{/if}
{if="isLoggedIn()"}
·
<a href="?delete_link&lf_linkdate={$value.id}&token={$token}"
- title="{'Delete'|t}" class="delete-link confirm-delete">
+ title="{$strDelete}" class="delete-link confirm-delete">
<i class="fa fa-trash"></i>
</a>
{/if}
<a href="?untaggedonly" title="{'Filter untagged links'|t}"
class={if="$untaggedonly"}"filter-on"{else}"filter-off"{/if}
><i class="fa fa-tag"></i></a>
- <a href="#" class="filter-off fold-all pure-u-lg-0" title="Fold all">
+ <a href="#" class="filter-off fold-all pure-u-lg-0" title="{'Fold all'|t}">
<i class="fa fa-chevron-up"></i>
</a>
{loop="$action_plugin"}
<form method="GET" class="pure-u-0 pure-u-lg-visible">
<input type="text" name="linksperpage" placeholder="133">
</form>
- <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" title="Fold all">
+ <a href="#" class="filter-off fold-all pure-u-0 pure-u-lg-visible" title="{'Fold all'|t}">
<i class="fa fa-chevron-up"></i>
</a>
</div>
{$version}
{/if}
·
- The personal, minimalist, super-fast, database free, bookmarking service by the Shaarli community ·
- <a href="doc/html/index.html" rel="nofollow">Documentation</a>
+ {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t} ·
+ <a href="doc/html/index.html" rel="nofollow">{'Documentation'|t}</a>
{loop="$plugins_footer.text"}
{$value}
{/loop}
<script src="{$value}#"></script>
{/loop}
-<script src="js/shaarli.js"></script>
-<script src="inc/awesomplete.js#"></script>
-<script src="inc/awesomplete-multiple-tags.js#"></script>
+<div id="js-translations" class="hidden">
+ <span id="translation-fold">{'Fold'|t}</span>
+ <span id="translation-fold-all">{'Fold all'|t}</span>
+ <span id="translation-expand">{'Expand'|t}</span>
+ <span id="translation-expand-all">{'Expand all'|t}</span>
+ <span id="translation-delete-link">{'Are you sure you want to delete this link?'|t}</span>
+ <span id="translation-shaarli-desc">
+ {'The personal, minimalist, super-fast, database free, bookmarking service'|t} {'by the Shaarli community'|t}
+ </span>
+</div>
+
+<script src="js/shaarli.js?v={$version_hash}"></script>
+<script src="inc/awesomplete.js?v={$version_hash}#"></script>
+<script src="inc/awesomplete-multiple-tags.js?v={$version_hash}#"></script>
</section>
<div class="center more">
- More plugins available
- <a href="doc/Community-&-Related-software.html#third-party-plugins">in the documentation</a>.
+ {"More plugins available"|t}
+ <a href="doc/Community-&-Related-software.html#third-party-plugins">{"in the documentation"|t}</a>.
</div>
<div class="center">
<input type="submit" value="{'Save'|t}" name="save">
<input type="hidden" name="do" value="tagcloud">
<input type="text" name="searchtags" placeholder="{'Filter by tag'|t}"
{if="!empty($search_tags)"}
- value="{$search_tags}"
+ value="{$search_tags}"
{/if}
autocomplete="off" data-multiple data-autofirst data-minChars="1"
data-list="{loop="$tags"}{$key}, {/loop}"