Shaarli's translation

* translation system and unit tests
 * Translations everywhere

Dont use translation merge

It is not available with PHP builtin gettext, so it would have lead to inconsistency.
This commit is contained in:
ArthurHoaro 2017-05-09 18:12:15 +02:00
parent 72cfe44436
commit 12266213d0
51 changed files with 2252 additions and 246 deletions

View file

@ -135,7 +135,7 @@ test:
@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 \

View file

@ -149,12 +149,13 @@ public static function checkUpdate($currentVersion,
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));
}
}
@ -179,7 +180,7 @@ public static function checkResourcePermissions($conf)
$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');
}
}
@ -191,10 +192,10 @@ public static function checkResourcePermissions($conf)
$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');
}
}
@ -212,10 +213,10 @@ public static function checkResourcePermissions($conf)
}
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');
}
}

View file

@ -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;
}

View file

@ -148,9 +148,9 @@ protected function buildItem($link, $pageaddr)
$link['url'] = $pageaddr . $link['url'];
}
if ($this->usePermalinks === true) {
$permalink = '<a href="'. $link['url'] .'" title="Direct link">Direct link</a>';
$permalink = '<a href="'. $link['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
} else {
$permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>';
$permalink = '<a href="'. $link['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
}
$link['description'] = format_description($link['description'], '', $pageaddr);
$link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;

View file

@ -171,7 +171,7 @@ protected function check()
}
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'));
}
}
@ -182,7 +182,7 @@ protected function read()
{
$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'));
}
}

View file

@ -1,21 +1,150 @@
<?php
namespace Shaarli;
use Gettext\GettextTranslator;
use Gettext\Merge;
use Gettext\Translations;
use Gettext\Translator;
use Gettext\TranslatorInterface;
use Shaarli\Config\ConfigManager;
/**
* Wrapper function for translation which match the API
* of gettext()/_() and ngettext().
* Class Languages
*
* Not doing translation for now.
* Load Shaarli translations using 'gettext/gettext'.
* This class allows to either use PHP gettext extension, or a PHP implementation of gettext,
* with a fixed language, or dynamically using autoLocale().
*
* @param string $text Text to translate.
* @param string $nText The plural message ID.
* @param int $nb The number of items for plural forms.
* Translation files PO/MO files follow gettext standard and must be placed under:
* <translation path>/<language>/LC_MESSAGES/<domain>.[po|mo]
*
* @return String Text translated.
* Pros/cons:
* - gettext extension is faster
* - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded)
*
* 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 override.
* @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;
}
$actualForm = $nb > 1 ? $nText : $text;
return sprintf($actualForm, $nb);
}

View file

@ -133,16 +133,16 @@ public function offsetSet($offset, $value)
{
// 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 @@ private function check()
$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 @@ private function check()
$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',

View file

@ -444,5 +444,11 @@ public static function tagsStrToArray($tags, $casesensitive)
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.');
}
}

View file

@ -32,11 +32,11 @@ public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $in
{
// 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();
7
foreach ($linkDb as $link) {
if ($link['private'] != 0 && $selection == 'public') {
continue;
@ -79,14 +79,14 @@ private static function importStatus(
$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 in '. $duration .' seconds: ';
$status .= $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;
}

View file

@ -159,9 +159,12 @@ public function renderPage($page)
*
* @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');
}

View file

@ -188,6 +188,9 @@ public function getPluginsMeta()
$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 @@ public function getPluginsMeta()
$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);
}
}

View file

@ -73,7 +73,7 @@ public function update()
}
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) {
@ -482,7 +482,7 @@ private function buildMessage($message)
}
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)) {
@ -522,11 +522,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 . '.'));
}
}

View file

@ -452,7 +452,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 +470,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);
}

View file

@ -22,10 +22,15 @@ public function read($filepath)
$data = json_decode($data, true);
if ($data === null) {
$errorCode = json_last_error();
$error = 'An error occurred while parsing JSON configuration file ('. $filepath .'): error code #';
$error .= $errorCode. '<br>➜ <code>' . json_last_error_msg() .'</code>';
$error = sprintf(
'An error occurred while parsing JSON configuration file (%s): error code #%d',
$filepath,
$errorCode
);
$error .= '<br>➜ <code>' . json_last_error_msg() .'</code>';
if ($errorCode === JSON_ERROR_SYNTAX) {
$error .= '<br>Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
$error .= '<br>';
$error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
$error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.';
}
throw new \Exception($error);
@ -44,8 +49,8 @@ public function write($filepath, $conf)
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.')
);
}
}

View file

@ -132,7 +132,7 @@ public function get($setting, $default = '')
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.
@ -339,6 +339,10 @@ protected function setDefaultValues()
$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());
}

View file

@ -118,8 +118,8 @@ public function write($filepath, $conf)
) {
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.')
);
}
}

View file

@ -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);
}
}

View file

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

View file

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

View file

@ -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 .'"';
}
}

View file

@ -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",

217
composer.lock generated
View file

@ -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",
@ -686,16 +809,16 @@
},
{
"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",
@ -1875,20 +1998,20 @@
},
{
"name": "symfony/config",
"version": "v3.3.6",
"version": "v3.3.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297"
"reference": "6ac0cc1f047c1dbc058fc25b7a4d91b068ed4488"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297",
"reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297",
"url": "https://api.github.com/repos/symfony/config/zipball/6ac0cc1f047c1dbc058fc25b7a4d91b068ed4488",
"reference": "6ac0cc1f047c1dbc058fc25b7a4d91b068ed4488",
"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-08-03T08:59:45+00:00"
},
{
"name": "symfony/console",
"version": "v2.8.26",
"version": "v2.8.27",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd"
"reference": "c0807a2ca978e64d8945d373a9221a5c35d1a253"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/32a3c6b3398de5db8ed381f4ef92970c59c2fcdd",
"reference": "32a3c6b3398de5db8ed381f4ef92970c59c2fcdd",
"url": "https://api.github.com/repos/symfony/console/zipball/c0807a2ca978e64d8945d373a9221a5c35d1a253",
"reference": "c0807a2ca978e64d8945d373a9221a5c35d1a253",
"shasum": ""
},
"require": {
@ -1994,7 +2117,7 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2017-07-29T21:26:04+00:00"
"time": "2017-08-27T14:29:03+00:00"
},
{
"name": "symfony/debug",
@ -2055,20 +2178,20 @@
},
{
"name": "symfony/dependency-injection",
"version": "v3.3.6",
"version": "v3.3.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/dependency-injection.git",
"reference": "8d70987f991481e809c63681ffe8ce3f3fde68a0"
"reference": "2ac658972626c75cbde7b0067c84b988170a6907"
},
"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/2ac658972626c75cbde7b0067c84b988170a6907",
"reference": "2ac658972626c75cbde7b0067c84b988170a6907",
"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-08-28T22:20:37+00:00"
},
{
"name": "symfony/filesystem",
"version": "v3.3.6",
"version": "v3.3.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
"reference": "427987eb4eed764c3b6e38d52a0f87989e010676"
"reference": "b32a0e5f928d0fa3d1dd03c78d020777e50c10cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676",
"reference": "427987eb4eed764c3b6e38d52a0f87989e010676",
"url": "https://api.github.com/repos/symfony/filesystem/zipball/b32a0e5f928d0fa3d1dd03c78d020777e50c10cb",
"reference": "b32a0e5f928d0fa3d1dd03c78d020777e50c10cb",
"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-07-29T21:54:42+00:00"
},
{
"name": "symfony/finder",
"version": "v3.3.6",
"version": "v3.3.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4"
"reference": "b2260dbc80f3c4198f903215f91a1ac7fe9fe09e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4",
"reference": "baea7f66d30854ad32988c11a09d7ffd485810c4",
"url": "https://api.github.com/repos/symfony/finder/zipball/b2260dbc80f3c4198f903215f91a1ac7fe9fe09e",
"reference": "b2260dbc80f3c4198f903215f91a1ac7fe9fe09e",
"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-07-29T21:54:42+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.4.0",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "f29dca382a6485c3cbe6379f0c61230167681937"
"reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803"
},
"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/7c8fae0ac1d216eb54349e6a8baa57d515fe8803",
"reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803",
"shasum": ""
},
"require": {
@ -2244,7 +2367,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
"dev-master": "1.5-dev"
}
},
"autoload": {
@ -2278,24 +2401,24 @@
"portable",
"shim"
],
"time": "2017-06-09T14:24:12+00:00"
"time": "2017-06-14T15:44:48+00:00"
},
{
"name": "symfony/yaml",
"version": "v3.3.6",
"version": "v3.3.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed"
"reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
"reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
"url": "https://api.github.com/repos/symfony/yaml/zipball/1d8c2a99c80862bdc3af94c1781bf70f86bccac0",
"reference": "1d8c2a99c80862bdc3af94c1781bf70f86bccac0",
"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-07-29T21:54:42+00:00"
},
{
"name": "theseer/fdomdocument",

Binary file not shown.

File diff suppressed because it is too large Load diff

View file

@ -64,7 +64,6 @@
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,6 +75,7 @@
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;
@ -121,8 +121,16 @@
}
$conf = new ConfigManager();
// 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 +152,7 @@
$errors = ApplicationUtils::checkResourcePermissions($conf);
if ($errors != array()) {
$message = '<p>Insufficient permissions:</p><ul>';
$message = '<p>'. t('Insufficient permissions:') .'</p><ul>';
foreach ($errors as $error) {
$message .= '<li>'.$error.'</li>';
@ -163,11 +171,6 @@
// 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,7 +379,7 @@ 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'])
&& (check_auth($_POST['login'], $_POST['password'], $conf))
@ -440,7 +443,8 @@ function ban_canLogin($conf)
}
}
}
echo '<script>alert("Wrong login/password.");document.location=\'?do=login'.$redir.'\';</script>'; // Redirect to login screen.
// Redirect to login screen.
echo '<script>alert("'. t("Wrong login/password.") .'");document.location=\'?do=login'.$redir.'\';</script>';
exit;
}
}
@ -1100,16 +1104,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 (!tokenOk($_POST['token'])) die(t('Wrong token.')); // Go away!
// Make sure old password is correct.
$oldhash = sha1($_POST['oldpassword'].$conf->get('credentials.login').$conf->get('credentials.salt'));
if ($oldhash!= $conf->get('credentials.hash')) { echo '<script>alert("The old password is not correct.");document.location=\'?do=changepasswd\';</script>'; exit; }
if ($oldhash!= $conf->get('credentials.hash')) {
echo '<script>alert("'. t('The old password is not correct.') .'");document.location=\'?do=changepasswd\';</script>';
exit;
}
// Save new password
// Salt renders rainbow-tables attacks useless.
$conf->set('credentials.salt', sha1(uniqid('', true) .'_'. mt_rand()));
@ -1127,7 +1134,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=tools\';</script>';
exit;
}
echo '<script>alert("Your password has been changed.");document.location=\'?do=tools\';</script>';
echo '<script>alert("'. t('Your password has been changed') .'");document.location=\'?do=tools\';</script>';
exit;
}
else // show the change password form.
@ -1143,7 +1150,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
if (!empty($_POST['title']) )
{
if (!tokenOk($_POST['token'])) {
die('Wrong token.'); // Go away!
die(t('Wrong token.')); // Go away!
}
$tz = 'UTC';
if (!empty($_POST['continent']) && !empty($_POST['city'])
@ -1178,7 +1185,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
echo '<script>alert("'. $e->getMessage() .'");document.location=\'?do=configure\';</script>';
exit;
}
echo '<script>alert("Configuration was saved.");document.location=\'?do=configure\';</script>';
echo '<script>alert("'. t('Configuration was saved.') .'");document.location=\'?do=configure\';</script>';
exit;
}
else // Show the configuration form.
@ -1215,7 +1222,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
}
if (!tokenOk($_POST['token'])) {
die('Wrong token.');
die(t('Wrong token.'));
}
$alteredLinks = $LINKSDB->renameTag(escape($_POST['fromtag']), escape($_POST['totag']));
@ -1244,7 +1251,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
{
// Go away!
if (! tokenOk($_POST['token'])) {
die('Wrong token.');
die(t('Wrong token.'));
}
// lf_id should only be present if the link exists.
@ -1344,7 +1351,7 @@ function renderPage($conf, $pluginManager, $LINKSDB, $history)
if ($targetPage == Router::$PAGE_DELETELINK)
{
if (! tokenOk($_GET['token'])) {
die('Wrong token.');
die(t('Wrong token.'));
}
$ids = trim($_GET['lf_linkdate']);
@ -1550,11 +1557,14 @@ 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 '<script>alert("The file you are trying to upload is probably'
.' bigger than what this webserver can accept ('
.get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize')).').'
.' Please upload in smaller chunks.");document.location=\'?do='
.Router::$PAGE_IMPORT .'\';</script>';
$msg = sprintf(
t(
'The file you are trying to upload is probably bigger than what this webserver can accept'
.' (%s). Please upload in smaller chunks.'
),
get_max_upload_size(ini_get('post_max_size'), ini_get('upload_max_filesize'))
);
echo '<script>alert("'. $msg .'");document.location=\'?do='.Router::$PAGE_IMPORT .'\';</script>';
exit;
}
if (! tokenOk($_POST['token'])) {
@ -1962,12 +1972,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 '<pre>Sessions do not seem to work correctly on your server.<br>';
echo 'Make sure the variable session.save_path is set correctly in your php config, and that you have write access to it.<br>';
echo 'It currently points to '.session_save_path().'<br>';
echo 'Check that the hostname used to access Shaarli contains a dot. On some browsers, accessing your server via a hostname like \'localhost\' or any custom hostname without a dot causes cookie storage to fail. We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>';
echo '<br><a href="?">Click to try again.</a></pre>';
{
// Step 2: Check if data in session is correct.
$msg = t(
'<pre>Sessions do not seem to work correctly on your server.<br>'.
'Make sure the variable "session.save_path" is set correctly in your PHP config, '.
'and that you have write access to it.<br>'.
'It currently points to %s.<br>'.
'On some browsers, accessing your server via a hostname like \'localhost\' '.
'or any custom hostname without a dot causes cookie storage to fail. '.
'We recommend accessing your server via it\'s IP address or Fully Qualified Domain Name.<br>'
);
$msg = sprintf($msg, session_save_path());
echo $msg;
echo '<br><a href="?">'. t('Click to try again.') .'</a></pre>';
die;
}
if (!isset($_SESSION['session_tested']))

View file

@ -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 `<script data-isso="//comments.example.tld/" src="//comments.example.tld/js/embed.min.js"></script>` to includes.html template; then add `<section id="isso-thread"></section>` in the linklist template where you want the comments (in the linklist_plugins loop for example)
Problem: by default, Isso thread ID is guessed from the current url (only one thread per page).
if we want multiple threads on a single page (shaarli linklist), we must use : the `data-isso-id` client config,
with data-isso-id being the permalink of an item.
`<section data-isso-id="aH7klxW" id="isso-thread"></section>`
`data-isso-id: Set a custom thread id, defaults to current URI.`
Problem: feature is currently broken https://github.com/posativ/isso/issues/27
Another option, only display isso threads when current URL is a permalink (`\?(A-Z|a-z|0-9|-){7}`) (only show thread
when displaying only this link), and just display a "comments" button on each linklist item. Optionally show the comment
count on each item using the API (http://posativ.org/isso/docs/extras/api/#get-comment-count). API requests can be done
by raintpl `{function` or client-side with js. The former should be faster if isso and shaarli are on ther same server.
Showing all full isso threads in the linklist would destroy layout
-----------------------------------------------------------
http://www.git-attitude.fr/2014/11/04/git-rerere/ for the merge

View file

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

View file

@ -1 +1,5 @@
<span><a href="https://web.archive.org/web/%s"><img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="View on archive.org" alt="archive.org" /></a></span>
<span>
<a href="https://web.archive.org/web/%s">
<img class="linklist-plugin-icon" src="plugins/archiveorg/internetarchive.png" title="%s" alt="archive.org" />
</a>
</span>

11