From: ArthurHoaro Date: Sat, 28 Oct 2017 10:44:44 +0000 (+0200) Subject: Merge pull request #962 from ArthurHoaro/feature/perfs2 X-Git-Tag: v0.9.4~36 X-Git-Url: https://git.immae.eu/?a=commitdiff_plain;h=0926d263902c184bd4f4c2036cb8ee90f81c5060;hp=9ec0a61156192484ca90a8dc88b7c23b26129755;p=github%2Fshaarli%2FShaarli.git Merge pull request #962 from ArthurHoaro/feature/perfs2 Performances: reorder links when they're written instead of read --- diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..4a6589a2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# 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 diff --git a/.gitattributes b/.gitattributes index dd0e573c..b191e227 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,8 +22,10 @@ Dockerfile text *.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 diff --git a/.github/mailmap b/.github/mailmap index 41d91e47..bbdb7908 100644 --- a/.github/mailmap +++ b/.github/mailmap @@ -11,3 +11,5 @@ Timo Van Neerden lehollandaisvolant VirtualTam VirtualTam +Willi Eggeling +Willi Eggeling diff --git a/.gitignore b/.gitignore index d546f248..3f6939a4 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ vendor/ # Release archives *.tar.gz *.zip +inc/languages/*/LC_MESSAGES/shaarli.mo # Development and test resources coverage diff --git a/.travis.yml b/.travis.yml index 26535ad3..322e4337 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,6 @@ sudo: false -dist: precise +dist: trusty language: php -addons: - apt: - packages: - - locales - - language-pack-de - - language-pack-fr cache: directories: - $HOME/.composer/cache @@ -18,6 +12,9 @@ php: install: - composer self-update - composer install --prefer-dist + - locale -a +before_script: + - PATH=${PATH//:\.\/node_modules\/\.bin/} script: - make clean - make check_permissions diff --git a/AUTHORS b/AUTHORS index 2181ec9d..105561c1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,11 +1,13 @@ - 518 ArthurHoaro - 231 VirtualTam - 147 nodiscc + 537 ArthurHoaro + 252 VirtualTam + 148 nodiscc 56 Sébastien Sauvage 15 Florian Eula 13 Emilien Klein 12 Nicolas Danelon + 9 Willi Eggeling 8 Christophe HENRY + 6 B. van Berkum 5 Lucas Cimon 4 Alexandre Alapetite 4 David Sferruzza @@ -37,6 +39,7 @@ 1 Kevin Canévet 1 Knah Tsaeb 1 Lionel Martin + 1 Mark Gerarts 1 Marsup 1 Sbgodin 1 TsT diff --git a/CHANGELOG.md b/CHANGELOG.md index 60262d56..33feac20 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [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: diff --git a/Makefile b/Makefile index 40badb1d..c2d55946 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,16 @@ PHP_COMMA_SOURCE = index.php,application,tests,plugins 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 @@ -105,7 +115,7 @@ check_permissions: @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"; \ @@ -120,12 +130,12 @@ check_permissions: # 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 \ @@ -158,15 +168,15 @@ composer_dependencies: clean 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/ @@ -203,3 +213,8 @@ htmldoc: 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 diff --git a/README.md b/README.md index 100ff46b..c1050027 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ _It is designed to be personal (single-user), fast and handy._ [![](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) diff --git a/application/ApplicationUtils.php b/application/ApplicationUtils.php index 85dcbeeb..911873a0 100644 --- a/application/ApplicationUtils.php +++ b/application/ApplicationUtils.php @@ -149,12 +149,13 @@ class ApplicationUtils 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)); } } @@ -168,17 +169,18 @@ class ApplicationUtils 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'); } } @@ -190,10 +192,10 @@ class ApplicationUtils $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'); } } @@ -211,13 +213,28 @@ class ApplicationUtils } 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); + } } diff --git a/application/Cache.php b/application/Cache.php index 5d050165..e5d43e61 100644 --- a/application/Cache.php +++ b/application/Cache.php @@ -13,7 +13,7 @@ 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; } diff --git a/application/FeedBuilder.php b/application/FeedBuilder.php index 7377bcec..3cfaafb4 100644 --- a/application/FeedBuilder.php +++ b/application/FeedBuilder.php @@ -148,9 +148,9 @@ class FeedBuilder $link['url'] = $pageaddr . $link['url']; } if ($this->usePermalinks === true) { - $permalink = 'Direct link'; + $permalink = ''. t('Direct link') .''; } else { - $permalink = 'Permalink'; + $permalink = ''. t('Permalink') .''; } $link['description'] = format_description($link['description'], '', $pageaddr); $link['description'] .= PHP_EOL .'
— '. $permalink; diff --git a/application/FileUtils.php b/application/FileUtils.php index a167f642..918cb83b 100644 --- a/application/FileUtils.php +++ b/application/FileUtils.php @@ -50,7 +50,8 @@ class FileUtils /** * 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. @@ -61,16 +62,21 @@ class FileUtils { // 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)) + ) + ) + ); } } diff --git a/application/History.php b/application/History.php index 116b9264..35ec016a 100644 --- a/application/History.php +++ b/application/History.php @@ -16,6 +16,7 @@ * - 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. */ @@ -41,6 +42,11 @@ class History */ const SETTINGS = 'SETTINGS'; + /** + * @var string Action key: a bulk import has been processed. + */ + const IMPORT = 'IMPORT'; + /** * @var string History file path. */ @@ -121,6 +127,16 @@ class History $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. * @@ -155,7 +171,7 @@ class History } 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')); } } @@ -166,7 +182,7 @@ class History { $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')); } } diff --git a/application/Languages.php b/application/Languages.php index c8b0a25a..357c7524 100644 --- a/application/Languages.php +++ b/application/Languages.php @@ -1,21 +1,164 @@ //LC_MESSAGES/.[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); } diff --git a/application/LinkDB.php b/application/LinkDB.php index eace625e..c1661d52 100644 --- a/application/LinkDB.php +++ b/application/LinkDB.php @@ -133,16 +133,16 @@ class LinkDB implements Iterator, Countable, ArrayAccess { // 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 @@ -248,13 +248,13 @@ class LinkDB implements Iterator, Countable, ArrayAccess $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' @@ -264,9 +264,9 @@ You use the community supported version of the original Shaarli project, by Seba $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', diff --git a/application/LinkFilter.php b/application/LinkFilter.php index 95519528..12376e27 100644 --- a/application/LinkFilter.php +++ b/application/LinkFilter.php @@ -249,6 +249,51 @@ class LinkFilter 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 * @@ -263,20 +308,32 @@ class LinkFilter */ 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; @@ -284,25 +341,27 @@ class LinkFilter 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( + '/(? 0) { - return true; - } - - return false; - } - /** * Convert a list of tags (str) to an array. Also * - handle case sensitivity. @@ -407,5 +444,11 @@ class LinkFilter 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.'); + } } diff --git a/application/LinkUtils.php b/application/LinkUtils.php index 976474de..267e62cd 100644 --- a/application/LinkUtils.php +++ b/application/LinkUtils.php @@ -109,7 +109,7 @@ function count_private($links) */ 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, '$1', $text); diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php index 2a10ff22..dd7057f8 100644 --- a/application/NetscapeBookmarkUtils.php +++ b/application/NetscapeBookmarkUtils.php @@ -32,11 +32,10 @@ class NetscapeBookmarkUtils { // 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; @@ -66,6 +65,7 @@ class NetscapeBookmarkUtils * @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 */ @@ -74,16 +74,18 @@ class NetscapeBookmarkUtils $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; } @@ -101,6 +103,7 @@ class NetscapeBookmarkUtils */ 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']); @@ -184,7 +187,6 @@ class NetscapeBookmarkUtils $linkDb[$existingLink['id']] = $newLink; $importCount++; $overwriteCount++; - $history->updateLink($newLink); continue; } @@ -196,16 +198,19 @@ class NetscapeBookmarkUtils $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 ); } } diff --git a/application/PageBuilder.php b/application/PageBuilder.php index 7a42400d..468f144b 100644 --- a/application/PageBuilder.php +++ b/application/PageBuilder.php @@ -32,12 +32,14 @@ class PageBuilder * * @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; } /** @@ -49,7 +51,7 @@ class PageBuilder 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'), @@ -75,7 +77,11 @@ class PageBuilder } $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'])); @@ -88,7 +94,8 @@ class PageBuilder $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()); } @@ -154,9 +161,12 @@ class PageBuilder * * @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'); } diff --git a/application/PluginManager.php b/application/PluginManager.php index 59ece4fa..cf603845 100644 --- a/application/PluginManager.php +++ b/application/PluginManager.php @@ -188,6 +188,9 @@ class PluginManager $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']); @@ -203,7 +206,7 @@ class PluginManager $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]); } } } @@ -237,6 +240,6 @@ class PluginFileNotFoundException extends Exception */ public function __construct($pluginName) { - $this->message = 'Plugin "'. $pluginName .'" files not found.'; + $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName); } } diff --git a/application/SessionManager.php b/application/SessionManager.php new file mode 100644 index 00000000..3aa4ddfc --- /dev/null +++ b/application/SessionManager.php @@ -0,0 +1,83 @@ +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; + } +} diff --git a/application/ThemeUtils.php b/application/ThemeUtils.php index 2718ed13..16f2f6a2 100644 --- a/application/ThemeUtils.php +++ b/application/ThemeUtils.php @@ -22,6 +22,7 @@ class ThemeUtils */ public static function getThemes($tplDir) { + $tplDir = rtrim($tplDir, '/'); $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR); $themes = []; foreach ($allTheme as $value) { diff --git a/application/Updater.php b/application/Updater.php index 0702158a..bc859536 100644 --- a/application/Updater.php +++ b/application/Updater.php @@ -73,7 +73,7 @@ class Updater } 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) { @@ -398,7 +398,7 @@ class Updater */ 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; } @@ -413,7 +413,7 @@ class Updater $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) { @@ -490,7 +490,7 @@ class UpdaterException extends Exception } 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)) { @@ -530,11 +530,11 @@ function read_updates_file($updatesFilepath) 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 . '.')); } } diff --git a/application/Utils.php b/application/Utils.php index 4a2f5561..97b12fcf 100644 --- a/application/Utils.php +++ b/application/Utils.php @@ -181,36 +181,6 @@ function generateLocation($referer, $host, $loopTerms = array()) 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. @@ -452,7 +422,7 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true) */ 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)); @@ -470,3 +440,18 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false) 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); +} diff --git a/application/config/ConfigJson.php b/application/config/ConfigJson.php index 9ef2ef56..8c8d5610 100644 --- a/application/config/ConfigJson.php +++ b/application/config/ConfigJson.php @@ -22,10 +22,15 @@ class ConfigJson implements ConfigIO $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. '
➜ ' . json_last_error_msg() .''; + $error = sprintf( + 'An error occurred while parsing JSON configuration file (%s): error code #%d', + $filepath, + $errorCode + ); + $error .= '
➜ ' . json_last_error_msg() .''; if ($errorCode === JSON_ERROR_SYNTAX) { - $error .= '
Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as '; + $error .= '
'; + $error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as '; $error .= 'jsonlint.com.'; } throw new \Exception($error); @@ -44,8 +49,8 @@ class ConfigJson implements ConfigIO 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.') ); } } diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php index fdd5b3d7..9e4c9f63 100644 --- a/application/config/ConfigManager.php +++ b/application/config/ConfigManager.php @@ -132,7 +132,7 @@ class ConfigManager 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. @@ -317,6 +317,7 @@ class ConfigManager $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'); @@ -327,6 +328,7 @@ class ConfigManager $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); @@ -337,6 +339,10 @@ class ConfigManager $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()); } diff --git a/application/config/ConfigPhp.php b/application/config/ConfigPhp.php index 2633824d..2f66e8e0 100644 --- a/application/config/ConfigPhp.php +++ b/application/config/ConfigPhp.php @@ -118,8 +118,8 @@ class ConfigPhp implements ConfigIO ) { 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.') ); } } diff --git a/application/config/exception/MissingFieldConfigException.php b/application/config/exception/MissingFieldConfigException.php index 6346c6a9..9e0a9359 100644 --- a/application/config/exception/MissingFieldConfigException.php +++ b/application/config/exception/MissingFieldConfigException.php @@ -18,6 +18,6 @@ class MissingFieldConfigException extends \Exception 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); } } diff --git a/application/config/exception/PluginConfigOrderException.php b/application/config/exception/PluginConfigOrderException.php index f9d68750..f82ec26e 100644 --- a/application/config/exception/PluginConfigOrderException.php +++ b/application/config/exception/PluginConfigOrderException.php @@ -12,6 +12,6 @@ class PluginConfigOrderException extends \Exception */ 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.'); } } diff --git a/application/config/exception/UnauthorizedConfigException.php b/application/config/exception/UnauthorizedConfigException.php index 79672c1b..72311fae 100644 --- a/application/config/exception/UnauthorizedConfigException.php +++ b/application/config/exception/UnauthorizedConfigException.php @@ -13,6 +13,6 @@ class UnauthorizedConfigException extends \Exception */ public function __construct() { - $this->message = 'You are not authorized to alter config.'; + $this->message = t('You are not authorized to alter config.'); } } diff --git a/application/exceptions/IOException.php b/application/exceptions/IOException.php index b563b23d..18e46b77 100644 --- a/application/exceptions/IOException.php +++ b/application/exceptions/IOException.php @@ -16,7 +16,7 @@ class IOException extends Exception 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 .'"'; } } diff --git a/composer.json b/composer.json index afb8aca4..f331d6ca 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,8 @@ "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", diff --git a/composer.lock b/composer.lock index 435d6a88..39909b8f 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "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", @@ -76,6 +76,129 @@ ], "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", @@ -371,12 +494,12 @@ "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": { @@ -406,7 +529,7 @@ "publishers", "pubsubhubbub" ], - "time": "2016-11-15T06:24:01+00:00" + "time": "2017-10-08T10:59:41+00:00" }, { "name": "shaarli/netscape-bookmark-parser", @@ -632,16 +755,16 @@ }, { "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": { @@ -682,20 +805,20 @@ "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": { @@ -727,7 +850,7 @@ } ], "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", @@ -844,22 +967,22 @@ }, { "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" }, @@ -870,7 +993,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.7.x-dev" } }, "autoload": { @@ -903,7 +1026,7 @@ "spy", "stub" ], - "time": "2017-03-02T20:05:34+00:00" + "time": "2017-09-04T11:05:03+00:00" }, { "name": "phpunit/php-code-coverage", @@ -1875,20 +1998,20 @@ }, { "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": { @@ -1933,20 +2056,20 @@ ], "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": { @@ -1994,7 +2117,7 @@ ], "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", @@ -2055,20 +2178,20 @@ }, { "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": { @@ -2121,24 +2244,24 @@ ], "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": { @@ -2170,24 +2293,24 @@ ], "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": { @@ -2219,20 +2342,20 @@ ], "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": { @@ -2244,7 +2367,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -2278,24 +2401,24 @@ "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" @@ -2333,7 +2456,7 @@ ], "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", diff --git a/data/.htaccess b/data/.htaccess index f601c1ee..1d49da37 100644 --- a/data/.htaccess +++ b/data/.htaccess @@ -1,10 +1,16 @@ = 2.4> - Require all denied + Require all denied + + Require all granted + - Allow from none - Deny from all + Allow from none + Deny from all + + Allow from all + diff --git a/doc/md/Download-and-Installation.md b/doc/md/Download-and-Installation.md index e5e929ef..be848c97 100644 --- a/doc/md/Download-and-Installation.md +++ b/doc/md/Download-and-Installation.md @@ -4,11 +4,18 @@ Document Root (or directly at the document root). 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) --- @@ -28,14 +35,16 @@ $ unzip shaarli-v0.9.1-full.zip $ 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 @@ -83,13 +92,14 @@ $ git clone https://github.com/shaarli/Shaarli.git -b master /path/to/shaarli/ # 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! diff --git a/doc/md/Server-requirements.md b/doc/md/Server-requirements.md index 707af762..400b85a9 100644 --- a/doc/md/Server-requirements.md +++ b/doc/md/Server-requirements.md @@ -39,3 +39,4 @@ Extension | Required? | Usage [`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) diff --git a/doc/md/Shaarli-configuration.md b/doc/md/Shaarli-configuration.md index d90e95eb..920c7e27 100644 --- a/doc/md/Shaarli-configuration.md +++ b/doc/md/Shaarli-configuration.md @@ -55,6 +55,7 @@ _These settings should not be edited_ - **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 @@ -80,6 +81,20 @@ _These settings should not be edited_ - **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. @@ -90,6 +105,7 @@ _These settings should not be edited_ - **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 @@ -194,6 +210,7 @@ _These settings should not be edited_ "privacy": { "default_private_links": true, "hide_public_links": false, + "force_login": false, "hide_timestamps": false, "remember_user_default": true }, @@ -208,6 +225,13 @@ _These settings should not be edited_ "plugins": { "WALLABAG_URL": "http://demo.wallabag.org", "WALLABAG_VERSION": "1" + }, + "translation": { + "language": "fr", + "mode": "php", + "extensions": { + "demo": "plugins/demo_plugin/languages/" + } } } ?> ``` diff --git a/doc/md/Translations.md b/doc/md/Translations.md new file mode 100644 index 00000000..54a36655 --- /dev/null +++ b/doc/md/Translations.md @@ -0,0 +1,152 @@ +## 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:/// +http:///?nonope +http:///?do=addlink +http:///?do=changepasswd +http:///?do=changetag +http:///?do=configure +http:///?do=tools +http:///?do=daily +http:///?post +http:///?do=export +http:///?do=import +http:///?do=login +http:///?do=picwall +http:///?do=pluginadmin +http:///?do=tagcloud +http:///?do=taglist +``` + +#### Improve existing translation + +In Poedit, click on "Edit a Translation", and from Shaarli's directory open +`inc/languages//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//LC_MESSAGES/shaarli.po`. + +Then select the language you want to create. + +Click on `File > Save as...`, and save your file in `/inc/language//LC_MESSAGES/shaarli.po`. +`` 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: + +``` +/languages//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.: `. + +Example: + +```php +if (! $conf->exists('translation.extensions.my_theme')) { + $conf->set('translation.extensions.my_theme', '/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 `/languages//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! diff --git a/doc/md/Unit-tests-Docker.md b/doc/md/Unit-tests-Docker.md new file mode 100644 index 00000000..c2de7cc7 --- /dev/null +++ b/doc/md/Unit-tests-Docker.md @@ -0,0 +1,56 @@ +## 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//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 +``` diff --git a/doc/md/Upgrade-and-migration.md b/doc/md/Upgrade-and-migration.md index b3a08764..1dc07339 100644 --- a/doc/md/Upgrade-and-migration.md +++ b/doc/md/Upgrade-and-migration.md @@ -14,7 +14,7 @@ Shaarli stores all user data under the `data` directory: - `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: @@ -27,7 +27,7 @@ As all user data is kept under `data`, this is the only directory you need to wo - 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 @@ -35,10 +35,13 @@ As all user data is kept under `data`, this is the only directory you need to wo 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 @@ -72,6 +75,14 @@ Updating dependencies 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. @@ -151,6 +162,14 @@ Updating dependencies 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 @@ -173,7 +192,7 @@ Total 3317 (delta 2050), reused 3301 (delta 2034)to #### 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 diff --git a/doc/md/docker/docker-101.md b/doc/md/docker/docker-101.md index b02dd149..a9c00b85 100644 --- a/doc/md/docker/docker-101.md +++ b/doc/md/docker/docker-101.md @@ -60,3 +60,81 @@ wheezy: Pulling from debian 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 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 +``` + +### 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 +``` diff --git a/doc/md/docker/reverse-proxy-configuration.md b/doc/md/docker/reverse-proxy-configuration.md index 91ffecff..6066140e 100644 --- a/doc/md/docker/reverse-proxy-configuration.md +++ b/doc/md/docker/reverse-proxy-configuration.md @@ -1,6 +1,120 @@ +## 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 + + ServerName shaarli.domain.tld + Redirect permanent / https://shaarli.domain.tld + + + + 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/ + +``` -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; + } + } +} +``` diff --git a/doc/md/docker/shaarli-images.md b/doc/md/docker/shaarli-images.md index 6d108d21..1d19510a 100644 --- a/doc/md/docker/shaarli-images.md +++ b/doc/md/docker/shaarli-images.md @@ -5,14 +5,23 @@ The images can be found in the [`shaarli/shaarli`](https://hub.docker.com/r/shaa 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 diff --git a/doc/md/images/install-shaarli.png b/doc/md/images/install-shaarli.png new file mode 100644 index 00000000..7ae33816 Binary files /dev/null and b/doc/md/images/install-shaarli.png differ diff --git a/doc/md/images/poedit-1.jpg b/doc/md/images/poedit-1.jpg new file mode 100644 index 00000000..673ae6d6 Binary files /dev/null and b/doc/md/images/poedit-1.jpg differ diff --git a/doc/md/index.md b/doc/md/index.md index 24ada6c7..2b7d0f00 100644 --- a/doc/md/index.md +++ b/doc/md/index.md @@ -22,6 +22,17 @@ It runs the latest development version of Shaarli and is updated/reset daily. 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 diff --git a/docker/alpine/Dockerfile.latest b/docker/alpine/Dockerfile.latest new file mode 100644 index 00000000..dd4a173c --- /dev/null +++ b/docker/alpine/Dockerfile.latest @@ -0,0 +1,47 @@ +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 [] diff --git a/docker/alpine/Dockerfile.master b/docker/alpine/Dockerfile.master new file mode 100644 index 00000000..58f7c6e7 --- /dev/null +++ b/docker/alpine/Dockerfile.master @@ -0,0 +1,47 @@ +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 [] diff --git a/docker/alpine/IMAGE.md b/docker/alpine/IMAGE.md new file mode 100644 index 00000000..a8952257 --- /dev/null +++ b/docker/alpine/IMAGE.md @@ -0,0 +1,10 @@ +## 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 diff --git a/docker/production/stable/nginx.conf b/docker/alpine/nginx.conf similarity index 94% rename from docker/production/stable/nginx.conf rename to docker/alpine/nginx.conf index e8754d9b..07fba33f 100644 --- a/docker/production/stable/nginx.conf +++ b/docker/alpine/nginx.conf @@ -1,6 +1,7 @@ -user www-data www-data; +user nginx nginx; daemon off; worker_processes 4; +pid /var/run/nginx.pid; events { worker_connections 768; @@ -59,7 +60,7 @@ http { 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; } diff --git a/docker/alpine/php-fpm.conf b/docker/alpine/php-fpm.conf new file mode 100644 index 00000000..0843c164 --- /dev/null +++ b/docker/alpine/php-fpm.conf @@ -0,0 +1,16 @@ +[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 diff --git a/docker/alpine/services.d/.s6-svscan/finish b/docker/alpine/services.d/.s6-svscan/finish new file mode 100755 index 00000000..1dadeeaf --- /dev/null +++ b/docker/alpine/services.d/.s6-svscan/finish @@ -0,0 +1,2 @@ +#!/bin/sh +/bin/true diff --git a/docker/alpine/services.d/nginx/run b/docker/alpine/services.d/nginx/run new file mode 100755 index 00000000..21e7b0d6 --- /dev/null +++ b/docker/alpine/services.d/nginx/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +nginx diff --git a/docker/alpine/services.d/php-fpm/run b/docker/alpine/services.d/php-fpm/run new file mode 100755 index 00000000..21dd0107 --- /dev/null +++ b/docker/alpine/services.d/php-fpm/run @@ -0,0 +1,2 @@ +#!/bin/execlineb -P +php-fpm7 -F diff --git a/docker/production/stable/Dockerfile b/docker/debian/Dockerfile.stable similarity index 100% rename from docker/production/stable/Dockerfile rename to docker/debian/Dockerfile.stable diff --git a/docker/production/stable/IMAGE.md b/docker/debian/IMAGE.md similarity index 100% rename from docker/production/stable/IMAGE.md rename to docker/debian/IMAGE.md diff --git a/docker/production/nginx.conf b/docker/debian/nginx.conf similarity index 100% rename from docker/production/nginx.conf rename to docker/debian/nginx.conf diff --git a/docker/production/stable/supervised.conf b/docker/debian/supervised.conf similarity index 100% rename from docker/production/stable/supervised.conf rename to docker/debian/supervised.conf diff --git a/docker/production/Dockerfile b/docker/production/Dockerfile deleted file mode 100644 index d0509115..00000000 --- a/docker/production/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -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"] diff --git a/docker/production/IMAGE.md b/docker/production/IMAGE.md deleted file mode 100644 index 6f827b35..00000000 --- a/docker/production/IMAGE.md +++ /dev/null @@ -1,5 +0,0 @@ -## 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) diff --git a/docker/production/supervised.conf b/docker/production/supervised.conf deleted file mode 100644 index 5acd9795..00000000 --- a/docker/production/supervised.conf +++ /dev/null @@ -1,13 +0,0 @@ -[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 diff --git a/docker/test/alpine36/Dockerfile b/docker/test/alpine36/Dockerfile new file mode 100644 index 00000000..fa84f6e2 --- /dev/null +++ b/docker/test/alpine36/Dockerfile @@ -0,0 +1,34 @@ +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 [] diff --git a/docker/test/debian8/Dockerfile b/docker/test/debian8/Dockerfile new file mode 100644 index 00000000..eaa34e9b --- /dev/null +++ b/docker/test/debian8/Dockerfile @@ -0,0 +1,35 @@ +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 [] diff --git a/docker/test/debian9/Dockerfile b/docker/test/debian9/Dockerfile new file mode 100644 index 00000000..3ab4b93d --- /dev/null +++ b/docker/test/debian9/Dockerfile @@ -0,0 +1,36 @@ +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 [] diff --git a/docker/test/ubuntu16/Dockerfile b/docker/test/ubuntu16/Dockerfile new file mode 100644 index 00000000..e53ed9e3 --- /dev/null +++ b/docker/test/ubuntu16/Dockerfile @@ -0,0 +1,36 @@ +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 [] diff --git a/inc/languages/fr/LC_MESSAGES/shaarli.po b/inc/languages/fr/LC_MESSAGES/shaarli.po new file mode 100644 index 00000000..6b2de950 --- /dev/null +++ b/inc/languages/fr/LC_MESSAGES/shaarli.po @@ -0,0 +1,1366 @@ +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 "" +"
Sessions do not seem to work correctly on your server.
Make sure the " +"variable \"session.save_path\" is set correctly in your PHP config, and that " +"you have write access to it.
It currently points to %s.
On some " +"browsers, accessing your server via a hostname like 'localhost' or any " +"custom hostname without a dot causes cookie storage to fail. We recommend " +"accessing your server via it's IP address or Fully Qualified Domain Name.
" +msgstr "" +"
Les sesssions ne semble pas fonctionner sur ce serveur.
Assurez vous " +"que la variable « session.save_path » est correctement définie dans votre " +"fichier de configuration PHP, et que vous y avez les droits d'écriture." +"
Ce paramètre pointe actuellement sur %s.
Sur certains navigateurs, " +"accéder à votre serveur depuis un nom d'hôte comme « localhost » ou autre " +"nom personnalisé sans point '.' entraine l'échec de la sauvegarde des " +"cookies. Nous vous recommandons d'accéder à votre serveur depuis son adresse " +"IP ou un Fully Qualified Domain Name.
" + +#: index.php: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.
Warning:\n" +"If your shaared descriptions contained HTML tags before enabling the " +"markdown plugin,\n" +"enabling it might break your page.\n" +"See the README." +msgstr "" +"Utilise la syntaxe Markdown pour la description des liens." +"
Attention :\n" +"Si vous aviez des descriptions contenant du HTML avant d'activer cette " +"extension,\n" +"l'activer pourrait déformer vos pages.\n" +"Voir le README." + +#: plugins/piwik/piwik.php: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 HTTPS to use this " +"functionality." +msgstr "" +"Vous devez utiliser Shaarli en HTTPS 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" diff --git a/index.php b/index.php index 218d317d..e1516d37 100644 --- a/index.php +++ b/index.php @@ -64,7 +64,6 @@ require_once 'application/FeedBuilder.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'; @@ -76,8 +75,10 @@ require_once 'application/Utils.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 { @@ -88,7 +89,7 @@ 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(); @@ -115,14 +116,23 @@ if (session_id() == '') { } // 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 @@ -144,7 +154,7 @@ if (! is_file($conf->getConfigFileExt())) { $errors = ApplicationUtils::checkResourcePermissions($conf); if ($errors != array()) { - $message = '

Insufficient permissions:

    '; + $message = '

    '. t('Insufficient permissions:') .'

      '; foreach ($errors as $error) { $message .= '
    • '.$error.'
    • '; @@ -157,17 +167,12 @@ if (! is_file($conf->getConfigFileExt())) { } // 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) * @@ -376,9 +381,9 @@ function ban_canLogin($conf) // 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); @@ -440,7 +445,8 @@ if (isset($_POST['login'])) } } } - echo ''; // Redirect to login screen. + // Redirect to login screen. + echo ''; exit; } } @@ -450,32 +456,6 @@ if (isset($_POST['login'])) // 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). @@ -683,12 +663,13 @@ function showLinkList($PAGE, $LINKSDB, $conf, $pluginManager) { /** * 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')), @@ -709,7 +690,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) 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()); @@ -718,6 +699,23 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) $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( @@ -823,7 +821,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) } $data = array( - 'search_tags' => implode(' ', $filteringTags), + 'search_tags' => implode(' ', escape($filteringTags)), 'tags' => $tagList, ); $pluginManager->executeHooks('render_tagcloud', $data, array('loggedin' => isLoggedIn())); @@ -853,7 +851,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) } $data = [ - 'search_tags' => implode(' ', $filteringTags), + 'search_tags' => implode(' ', escape($filteringTags)), 'tags' => $tags, ]; $pluginManager->executeHooks('render_taglist', $data, ['loggedin' => isLoggedIn()]); @@ -1083,16 +1081,19 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) 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 ''; exit; } + if ($oldhash!= $conf->get('credentials.hash')) { + echo ''; + exit; + } // Save new password // Salt renders rainbow-tables attacks useless. $conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand())); @@ -1110,7 +1111,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) echo ''; exit; } - echo ''; + echo ''; exit; } else // show the change password form. @@ -1125,8 +1126,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) { 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']) @@ -1146,6 +1147,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) $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(); @@ -1161,7 +1164,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) echo ''; exit; } - echo ''; + echo ''; exit; } else // Show the configuration form. @@ -1183,6 +1186,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) $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; } @@ -1197,8 +1202,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) 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'])); @@ -1208,9 +1213,10 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) } $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 ''; exit; } @@ -1226,8 +1232,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) 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. @@ -1326,8 +1332,8 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) // -------- 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']); @@ -1426,7 +1432,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) if ($url == '') { $url = '?' . smallHash($linkdate . $LINKSDB->getNextId()); - $title = 'Note: '; + $title = $conf->get('general.default_note_title', t('Note: ')); } $url = escape($url); $title = escape($title); @@ -1533,14 +1539,17 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) // 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 ''; + $msg = sprintf( + t( + 'The file you are trying to upload is probably bigger than what this webserver can accept' + .' (%s). Please upload in smaller chunks.' + ), + get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')) + ); + echo ''; exit; } - if (! tokenOk($_POST['token'])) { + if (! $sessionManager->checkToken($_POST['token'])) { die('Wrong token.'); } $status = NetscapeBookmarkUtils::import( @@ -1607,7 +1616,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history) // Get a fresh token if ($targetPage == Router::$GET_TOKEN) { header('Content-Type:text/plain'); - echo getToken($conf); + echo $sessionManager->generateToken($conf); exit; } @@ -1933,10 +1942,10 @@ function lazyThumbnail($conf, $url,$href=false) * 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); @@ -1945,12 +1954,20 @@ function install($conf) // (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 '
      Sessions do not seem to work correctly on your server.
      '; - echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.
      '; - echo 'It currently points to '.session_save_path().'
      '; - 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.
      '; - echo '
      Click to try again.
      '; + { + // Step 2: Check if data in session is correct. + $msg = t( + '
      Sessions do not seem to work correctly on your server.
      '. + 'Make sure the variable "session.save_path" is set correctly in your PHP config, '. + 'and that you have write access to it.
      '. + 'It currently points to %s.
      '. + 'On some browsers, accessing your server via a hostname like \'localhost\' '. + 'or any custom hostname without a dot causes cookie storage to fail. '. + 'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.
      ' + ); + $msg = sprintf($msg, session_save_path()); + echo $msg; + echo '
      '. t('Click to try again.') .'
      '; die; } if (!isset($_SESSION['session_tested'])) @@ -1983,6 +2000,7 @@ function install($conf) } 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( @@ -2010,10 +2028,11 @@ function install($conf) 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; } @@ -2286,7 +2305,7 @@ $response = $app->run(true); 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); } diff --git a/mkdocs.yml b/mkdocs.yml index 648d8f67..8617ea45 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -43,8 +43,10 @@ pages: - 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 diff --git a/plugins/TODO.md b/plugins/TODO.md deleted file mode 100644 index e3313d67..00000000 --- a/plugins/TODO.md +++ /dev/null @@ -1,28 +0,0 @@ -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 `` to includes.html template; then add `
      ` 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. - -`
      ` -`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 diff --git a/plugins/addlink_toolbar/addlink_toolbar.php b/plugins/addlink_toolbar/addlink_toolbar.php index ddf50aaf..8c05a231 100644 --- a/plugins/addlink_toolbar/addlink_toolbar.php +++ b/plugins/addlink_toolbar/addlink_toolbar.php @@ -26,11 +26,11 @@ function hook_addlink_toolbar_render_header($data) array( 'type' => 'text', 'name' => 'post', - 'placeholder' => 'URI', + 'placeholder' => t('URI'), ), array( 'type' => 'submit', - 'value' => 'Add link', + 'value' => t('Add link'), 'class' => 'bigbutton', ), ), @@ -40,3 +40,12 @@ function hook_addlink_toolbar_render_header($data) 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.'); +} diff --git a/plugins/archiveorg/archiveorg.html b/plugins/archiveorg/archiveorg.html index 0781fe35..ad501f47 100644 --- a/plugins/archiveorg/archiveorg.html +++ b/plugins/archiveorg/archiveorg.html @@ -1 +1,5 @@ -archive.org + + + archive.org + + diff --git a/plugins/archiveorg/archiveorg.php b/plugins/archiveorg/archiveorg.php index 03d13d0e..cda35751 100644 --- a/plugins/archiveorg/archiveorg.php +++ b/plugins/archiveorg/archiveorg.php @@ -20,9 +20,18 @@ function hook_archiveorg_render_linklist($data) 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.'); +} diff --git a/plugins/demo_plugin/demo_plugin.php b/plugins/demo_plugin/demo_plugin.php index 8fdbf663..b80a2b6d 100644 --- a/plugins/demo_plugin/demo_plugin.php +++ b/plugins/demo_plugin/demo_plugin.php @@ -14,6 +14,26 @@ * 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. @@ -27,6 +47,12 @@ function demo_plugin_init($conf) { $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; } @@ -160,7 +186,7 @@ function hook_demo_plugin_render_includes($data) function hook_demo_plugin_render_footer($data) { // footer text - $data['text'][] = 'Shaarli is now enhanced by the awesome demo_plugin.'; + $data['text'][] = '
      '. demo_plugin_t('Shaarli is now enhanced by the awesome demo_plugin.'); // Free elements at the end of the page. $data['endofpage'][] = '' . @@ -433,3 +459,12 @@ function hook_demo_plugin_render_feed($data) } 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.'); +} diff --git a/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo new file mode 100644 index 00000000..0f80f6ed Binary files /dev/null and b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.mo differ diff --git a/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po new file mode 100644 index 00000000..921379c0 --- /dev/null +++ b/plugins/demo_plugin/languages/fr/LC_MESSAGES/demo.po @@ -0,0 +1,21 @@ +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." diff --git a/plugins/isso/isso.php b/plugins/isso/isso.php index ce16645f..5bc1cce2 100644 --- a/plugins/isso/isso.php +++ b/plugins/isso/isso.php @@ -4,10 +4,11 @@ * 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. @@ -16,8 +17,8 @@ function isso_init($conf) { $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); } } @@ -52,3 +53,13 @@ function hook_isso_render_linklist($data, $conf) 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://\')'); +} diff --git a/plugins/markdown/help.html b/plugins/markdown/help.html index 9c4e5ae0..ded3d347 100644 --- a/plugins/markdown/help.html +++ b/plugins/markdown/help.html @@ -1,5 +1,5 @@
      - Description will be rendered with - - Markdown syntax. + %s + + %s.
      diff --git a/plugins/markdown/markdown.php b/plugins/markdown/markdown.php index 772c56e8..1531549d 100644 --- a/plugins/markdown/markdown.php +++ b/plugins/markdown/markdown.php @@ -154,8 +154,13 @@ function hook_markdown_render_includes($data) 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; @@ -325,3 +330,15 @@ function process_markdown($description, $escape = true, $allowedProtocols = []) 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.
      Warning: +If your shaared descriptions contained HTML tags before enabling the markdown plugin, +enabling it might break your page. +See the README.'); +} diff --git a/plugins/piwik/piwik.php b/plugins/piwik/piwik.php index 4a2b48a1..ca00c2be 100644 --- a/plugins/piwik/piwik.php +++ b/plugins/piwik/piwik.php @@ -18,8 +18,8 @@ function piwik_init($conf) $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); } } @@ -60,3 +60,14 @@ function hook_piwik_render_footer($data, $conf) 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'); +} diff --git a/plugins/playvideos/playvideos.php b/plugins/playvideos/playvideos.php index 64484504..c6d6b0cc 100644 --- a/plugins/playvideos/playvideos.php +++ b/plugins/playvideos/playvideos.php @@ -19,10 +19,10 @@ function hook_playvideos_render_header($data) $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; } @@ -46,3 +46,12 @@ function hook_playvideos_render_footer($data) 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.'); +} diff --git a/plugins/pubsubhubbub/pubsubhubbub.php b/plugins/pubsubhubbub/pubsubhubbub.php index 03b6757b..184b588b 100644 --- a/plugins/pubsubhubbub/pubsubhubbub.php +++ b/plugins/pubsubhubbub/pubsubhubbub.php @@ -10,6 +10,7 @@ */ use pubsubhubbub\publisher\Publisher; +use Shaarli\Config\ConfigManager; /** * Plugin init function - set the hub to the default appspot one. @@ -65,7 +66,7 @@ function hook_pubsubhubbub_save_link($data, $conf) $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; @@ -91,11 +92,20 @@ function nocurl_http_post($url, $postString) { $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.'); +} diff --git a/plugins/qrcode/qrcode.meta b/plugins/qrcode/qrcode.meta index cbf371ea..1812cd21 100644 --- a/plugins/qrcode/qrcode.meta +++ b/plugins/qrcode/qrcode.meta @@ -1 +1 @@ -description="For each link, add a QRCode icon ." +description="For each link, add a QRCode icon." diff --git a/plugins/qrcode/qrcode.php b/plugins/qrcode/qrcode.php index 8bc610d1..0f96a106 100644 --- a/plugins/qrcode/qrcode.php +++ b/plugins/qrcode/qrcode.php @@ -59,3 +59,12 @@ function hook_qrcode_render_includes($data) 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.'); +} diff --git a/plugins/wallabag/wallabag.html b/plugins/wallabag/wallabag.html index e861536d..4c57691d 100644 --- a/plugins/wallabag/wallabag.html +++ b/plugins/wallabag/wallabag.html @@ -1 +1,5 @@ -wallabag + + + wallabag + + diff --git a/plugins/wallabag/wallabag.php b/plugins/wallabag/wallabag.php index 641e4cc2..9dfd079e 100644 --- a/plugins/wallabag/wallabag.php +++ b/plugins/wallabag/wallabag.php @@ -5,6 +5,7 @@ */ require_once 'WallabagInstance.php'; +use Shaarli\Config\ConfigManager; /** * Init function, return an error if the server is not set. @@ -17,8 +18,8 @@ function wallabag_init($conf) { $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); } } @@ -43,12 +44,14 @@ function hook_wallabag_render_linklist($data, $conf) $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; } @@ -56,3 +59,14 @@ function hook_wallabag_render_linklist($data, $conf) 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)'); +} + diff --git a/tests/LanguagesTest.php b/tests/LanguagesTest.php index 79c136c8..864ce630 100644 --- a/tests/LanguagesTest.php +++ b/tests/LanguagesTest.php @@ -1,41 +1,203 @@ 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')); + } } diff --git a/tests/LinkUtilsTest.php b/tests/LinkUtilsTest.php index 7c0d4b0b..c77922ec 100644 --- a/tests/LinkUtilsTest.php +++ b/tests/LinkUtilsTest.php @@ -103,6 +103,16 @@ class LinkUtilsTest extends PHPUnit_Framework_TestCase $expectedText = 'stuff http://hello.there/is=someone#here otherstuff'; $processedText = text2clickable($text, ''); $this->assertEquals($expectedText, $processedText); + + $text = 'stuff http://hello.there/is=someone#here(please) otherstuff'; + $expectedText = 'stuff http://hello.there/is=someone#here(please) otherstuff'; + $processedText = text2clickable($text, ''); + $this->assertEquals($expectedText, $processedText); + + $text = 'stuff http://hello.there/is=someone#here(please)&no otherstuff'; + $expectedText = 'stuff http://hello.there/is=someone#here(please)&no otherstuff'; + $processedText = text2clickable($text, ''); + $this->assertEquals($expectedText, $processedText); } /** diff --git a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php index 5fc1d1e8..4961aa2c 100644 --- a/tests/NetscapeBookmarkUtils/BookmarkImportTest.php +++ b/tests/NetscapeBookmarkUtils/BookmarkImportTest.php @@ -132,8 +132,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase 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) ); @@ -161,8 +161,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase 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) ); @@ -283,8 +283,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase 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) ); @@ -328,8 +328,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase { $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) ); @@ -372,8 +372,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase { $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) ); @@ -396,8 +396,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase { $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) ); @@ -422,8 +422,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase // 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) ); @@ -442,8 +442,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase '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) ); @@ -468,8 +468,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase // 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) ); @@ -489,8 +489,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase '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) ); @@ -513,8 +513,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase { $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) ); @@ -523,8 +523,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase // 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) ); @@ -542,8 +542,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase '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) ); @@ -569,8 +569,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase '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) ); @@ -594,8 +594,8 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase 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) ); @@ -622,24 +622,19 @@ class BookmarkImportTest extends PHPUnit_Framework_TestCase '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']); } } diff --git a/tests/SessionManagerTest.php b/tests/SessionManagerTest.php new file mode 100644 index 00000000..a92c3ccc --- /dev/null +++ b/tests/SessionManagerTest.php @@ -0,0 +1,160 @@ +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=') + ); + } +} diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index 3d1aa653..6cd37a7a 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -5,10 +5,6 @@ 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(); /** @@ -16,9 +12,6 @@ ReferenceSessionIdHashes::genAllHashes(); */ class UtilsTest extends PHPUnit_Framework_TestCase { - // Session ID hashes - protected static $sidHashes = null; - // Log file protected static $testLogFile = 'tests.log'; @@ -30,13 +23,11 @@ class UtilsTest extends PHPUnit_Framework_TestCase */ 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'); @@ -221,56 +212,7 @@ class UtilsTest extends PHPUnit_Framework_TestCase $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. */ @@ -384,18 +326,18 @@ class UtilsTest extends PHPUnit_Framework_TestCase */ 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('')); } /** @@ -403,9 +345,9 @@ class UtilsTest extends PHPUnit_Framework_TestCase */ 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)); } /** diff --git a/tests/api/controllers/GetLinksTest.php b/tests/api/controllers/GetLinksTest.php index 4cb70224..d22ed3bf 100644 --- a/tests/api/controllers/GetLinksTest.php +++ b/tests/api/controllers/GetLinksTest.php @@ -367,6 +367,89 @@ class GetLinksTest extends \PHPUnit_Framework_TestCase $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)); } /** diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 00000000..d36d73cd --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,6 @@ +assertEquals('fr_FR.utf8', setlocale(LC_ALL, 0)); @@ -106,12 +106,12 @@ class UtilsDeTest extends UtilsTest } /** - * 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); diff --git a/tests/languages/en/UtilsEnTest.php b/tests/languages/en/UtilsEnTest.php index d8680b2b..a74063ae 100644 --- a/tests/languages/en/UtilsEnTest.php +++ b/tests/languages/en/UtilsEnTest.php @@ -81,12 +81,12 @@ class UtilsEnTest extends UtilsTest } /** - * 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)); @@ -106,12 +106,12 @@ class UtilsEnTest extends UtilsTest } /** - * 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); diff --git a/tests/languages/fr/LanguagesFrTest.php b/tests/languages/fr/LanguagesFrTest.php new file mode 100644 index 00000000..79d05172 --- /dev/null +++ b/tests/languages/fr/LanguagesFrTest.php @@ -0,0 +1,175 @@ +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')); + } +} diff --git a/tests/languages/fr/UtilsFrTest.php b/tests/languages/fr/UtilsFrTest.php index 0d50a878..3dbb126f 100644 --- a/tests/languages/fr/UtilsFrTest.php +++ b/tests/languages/fr/UtilsFrTest.php @@ -81,12 +81,12 @@ class UtilsFrTest extends UtilsTest } /** - * 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)); @@ -106,12 +106,12 @@ class UtilsFrTest extends UtilsTest } /** - * 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); diff --git a/tests/utils/languages/fr/LC_MESSAGES/test.mo b/tests/utils/languages/fr/LC_MESSAGES/test.mo new file mode 100644 index 00000000..416c7831 Binary files /dev/null and b/tests/utils/languages/fr/LC_MESSAGES/test.mo differ diff --git a/tests/utils/languages/fr/LC_MESSAGES/test.po b/tests/utils/languages/fr/LC_MESSAGES/test.po new file mode 100644 index 00000000..89a4fd9b --- /dev/null +++ b/tests/utils/languages/fr/LC_MESSAGES/test.po @@ -0,0 +1,19 @@ +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" diff --git a/tpl/default/changetag.html b/tpl/default/changetag.html index 49dd20d9..6606c4fa 100644 --- a/tpl/default/changetag.html +++ b/tpl/default/changetag.html @@ -32,7 +32,7 @@ -

      You can also edit tags in the tag list.

      +

      {'You can also edit tags in the'|t} {'tag list'|t}.

      {include="page.footer"} diff --git a/tpl/default/configure.html b/tpl/default/configure.html index 76a1b9fd..cc3b299b 100644 --- a/tpl/default/configure.html +++ b/tpl/default/configure.html @@ -69,6 +69,30 @@ +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      diff --git a/tpl/default/css/shaarli.css b/tpl/default/css/shaarli.css index e1868c59..ba589723 100644 --- a/tpl/default/css/shaarli.css +++ b/tpl/default/css/shaarli.css @@ -539,7 +539,7 @@ body, .pure-g [class*="pure-u"] { } .linklist-item-title a:visited .linklist-link { - color: #555555; + color: #2a4c41; } .linklist-item-title a:hover, .linklist-item-title .linklist-link:hover{ diff --git a/tpl/default/import.html b/tpl/default/import.html index 1f040685..000a50ac 100644 --- a/tpl/default/import.html +++ b/tpl/default/import.html @@ -18,7 +18,7 @@
      -


      Maximum size allowed: {$maxfilesizeHuman}

      +


      {'Maximum size allowed:'|t} {$maxfilesizeHuman}

      @@ -31,15 +31,15 @@
      - Use values from the imported file, default to public + {'Use values from the imported file, default to public'|t}
      - Import all bookmarks as private + {'Import all bookmarks as private'|t}
      - Import all bookmarks as public + {'Import all bookmarks as public'|t}
      diff --git a/tpl/default/includes.html b/tpl/default/includes.html index 0350ef66..80c08333 100644 --- a/tpl/default/includes.html +++ b/tpl/default/includes.html @@ -5,16 +5,16 @@ - - - - - - + + + + + + {if="is_file('data/user.css')"} {/if} {loop="$plugins_includes.css_files"} - + {/loop} \ No newline at end of file diff --git a/tpl/default/install.html b/tpl/default/install.html index 164d453b..6199b33d 100644 --- a/tpl/default/install.html +++ b/tpl/default/install.html @@ -65,6 +65,27 @@
      +
      +
      +
      + +
      +
      +
      +
      + +
      +
      +
      +
      diff --git a/tpl/default/js/shaarli.js b/tpl/default/js/shaarli.js index 1c66ebbd..09b07eed 100644 --- a/tpl/default/js/shaarli.js +++ b/tpl/default/js/shaarli.js @@ -138,6 +138,9 @@ window.onload = function () { }); 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 }); }); } @@ -146,7 +149,7 @@ window.onload = function () { { // 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'; } @@ -155,7 +158,7 @@ window.onload = function () { } } else { - button.title = 'Fold'; + button.title = document.getElementById('translation-fold').innerHTML; if (description != null) { description.style.display = 'block'; } @@ -173,7 +176,7 @@ window.onload = function () { 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(); } }); @@ -275,8 +278,14 @@ window.onload = function () { }; 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 () { @@ -612,7 +621,7 @@ function activateFirefoxSocial(node) { // 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", diff --git a/tpl/default/linklist.html b/tpl/default/linklist.html index 685821e3..5dab8e9a 100644 --- a/tpl/default/linklist.html +++ b/tpl/default/linklist.html @@ -86,7 +86,7 @@
      - {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} {$search_term} {/if} @@ -117,6 +117,16 @@
      + {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"}