Merge branch 'master' of https://github.com/shaarli/Shaarli into myShaarli_commu

This commit is contained in:
Knah Tsaeb 2017-12-15 10:04:41 +01:00
commit 22ee4c71a3
108 changed files with 3695 additions and 686 deletions

23
.editorconfig Normal file
View file

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

2
.gitattributes vendored
View file

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

1
.gitignore vendored
View file

@ -18,6 +18,7 @@ vendor/
# Release archives
*.tar.gz
*.zip
inc/languages/*/LC_MESSAGES/shaarli.mo
# Development and test resources
coverage

View file

@ -13,6 +13,8 @@ install:
- composer self-update
- composer install --prefer-dist
- locale -a
before_script:
- PATH=${PATH//:\.\/node_modules\/\.bin/}
script:
- make clean
- make check_permissions

View file

@ -40,7 +40,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Security
- Vulnerability introduced in v0.9.1 fixed.
- 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

View file

@ -1,17 +1,6 @@
# The personal, minimalist, super-fast, database free, bookmarking service.
# Makefile for PHP code analysis & testing, documentation and release generation
# Prerequisites:
# - install Composer, either:
# - from your distro's package manager;
# - from the official website (https://getcomposer.org/download/);
# - install/update test dependencies:
# $ composer install # 1st setup
# $ composer update
# - install Xdebug for PHPUnit code coverage reports:
# - see http://xdebug.org/docs/install
# - enable in php.ini
BIN = vendor/bin
PHP_SOURCE = index.php application tests plugins
PHP_COMMA_SOURCE = index.php,application,tests,plugins
@ -115,7 +104,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"; \
@ -130,12 +119,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 \
@ -168,15 +157,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/
@ -213,3 +202,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 \;

View file

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

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,11 +148,11 @@ 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'] = format_description($link['description'], '', false, $pageaddr);
$link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;
$pubDate = $link['created'];

View file

@ -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 @@ public function updateSettings()
$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 @@ 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'));
}
}
@ -166,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

@ -76,7 +76,7 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304)
curl_setopt($ch, CURLOPT_USERAGENT, $userAgent);
// Max download size management
curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024);
curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16);
curl_setopt($ch, CURLOPT_NOPROGRESS, false);
curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes)
@ -302,6 +302,13 @@ function server_url($server)
$port = $server['HTTP_X_FORWARDED_PORT'];
}
// This is a workaround for proxies that don't forward the scheme properly.
// Connecting over port 443 has to be in HTTPS.
// See https://github.com/shaarli/Shaarli/issues/1022
if ($port == '443') {
$scheme = 'https';
}
if (($scheme == 'http' && $port != '80')
|| ($scheme == 'https' && $port != '443')
) {

View file

@ -1,21 +1,164 @@
<?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 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);
}

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',
@ -289,13 +289,15 @@ private function read()
return;
}
$this->urls = [];
$this->ids = [];
$this->links = FileUtils::readFlatDB($this->datastore, []);
$toremove = array();
foreach ($this->links as $key => &$link) {
if (! $this->loggedIn && $link['private'] != 0) {
// Transition for not upgraded databases.
$toremove[] = $key;
unset($this->links[$key]);
continue;
}
@ -329,14 +331,10 @@ private function read()
}
$link['shorturl'] = smallHash($link['linkdate']);
}
}
// If user is not logged in, filter private links.
foreach ($toremove as $offset) {
unset($this->links[$offset]);
$this->urls[$link['url']] = $key;
$this->ids[$link['id']] = $key;
}
$this->reorder();
}
/**
@ -346,6 +344,7 @@ private function read()
*/
private function write()
{
$this->reorder();
FileUtils::writeFlatDB($this->datastore, $this->links);
}
@ -528,8 +527,8 @@ public function reorder($order = 'DESC')
return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
});
$this->urls = array();
$this->ids = array();
$this->urls = [];
$this->ids = [];
foreach ($this->links as $key => $link) {
$this->urls[$link['url']] = $key;
$this->ids[$link['id']] = $key;

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

@ -102,12 +102,13 @@ function count_private($links)
*
* @param string $text input string.
* @param string $redirector if a redirector is set, use it to gerenate links.
* @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not.
*
* @return string returns $text with all links converted to HTML links.
*
* @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
*/
function text2clickable($text, $redirector = '')
function text2clickable($text, $redirector = '', $urlEncode = true)
{
$regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
@ -117,8 +118,9 @@ function text2clickable($text, $redirector = '')
// Redirector is set, urlencode the final URL.
return preg_replace_callback(
$regex,
function ($matches) use ($redirector) {
return '<a href="' . $redirector . urlencode($matches[1]) .'">'. $matches[1] .'</a>';
function ($matches) use ($redirector, $urlEncode) {
$url = $urlEncode ? urlencode($matches[1]) : $matches[1];
return '<a href="' . $redirector . $url .'">'. $matches[1] .'</a>';
},
$text
);
@ -164,12 +166,13 @@ function space2nbsp($text)
*
* @param string $description shaare's description.
* @param string $redirector if a redirector is set, use it to gerenate links.
* @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not.
* @param string $indexUrl URL to Shaarli's index.
*
* @return string formatted description.
*/
function format_description($description, $redirector = '', $indexUrl = '') {
return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl)));
function format_description($description, $redirector = '', $urlEncode = true, $indexUrl = '') {
return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector, $urlEncode), $indexUrl)));
}
/**

View file

@ -32,11 +32,10 @@ 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();
foreach ($linkDb as $link) {
if ($link['private'] != 0 && $selection == 'public') {
continue;
@ -66,6 +65,7 @@ public static function filterAndFormat($linkDb, $selection, $prependNoteUrl, $in
* @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 @@ private static function importStatus(
$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 @@ private static function importStatus(
*/
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 @@ public static function import($post, $files, $linkDb, $conf, $history)
$linkDb[$existingLink['id']] = $newLink;
$importCount++;
$overwriteCount++;
$history->updateLink($newLink);
continue;
}
@ -196,16 +198,19 @@ public static function import($post, $files, $linkDb, $conf, $history)
$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
);
}
}

View file

@ -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;
}
/**
@ -92,7 +94,7 @@ private function initialize()
$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());
@ -159,9 +161,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

@ -0,0 +1,83 @@
<?php
namespace Shaarli;
/**
* Manages the server-side session
*/
class SessionManager
{
protected $session = [];
/**
* Constructor
*
* @param array $session The $_SESSION array (reference)
* @param ConfigManager $conf ConfigManager instance
*/
public function __construct(& $session, $conf)
{
$this->session = &$session;
$this->conf = $conf;
}
/**
* Generates a session token
*
* @return string token
*/
public function generateToken()
{
$token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
$this->session['tokens'][$token] = 1;
return $token;
}
/**
* Checks the validity of a session token, and destroys it afterwards
*
* @param string $token The token to check
*
* @return bool true if the token is valid, else false
*/
public function checkToken($token)
{
if (! isset($this->session['tokens'][$token])) {
// the token is wrong, or has already been used
return false;
}
// destroy the token to prevent future use
unset($this->session['tokens'][$token]);
return true;
}
/**
* Validate session ID to prevent Full Path Disclosure.
*
* See #298.
* The session ID's format depends on the hash algorithm set in PHP settings
*
* @param string $sessionId Session ID
*
* @return true if valid, false otherwise.
*
* @see http://php.net/manual/en/function.hash-algos.php
* @see http://php.net/manual/en/session.configuration.php
*/
public static function checkId($sessionId)
{
if (empty($sessionId)) {
return false;
}
if (!$sessionId) {
return false;
}
if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
return false;
}
return true;
}
}

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) {
@ -436,6 +436,15 @@ public function updateMethodResetHistoryFile()
}
return true;
}
/**
* Save the datastore -> the link order is now applied when links are saved.
*/
public function updateMethodReorderDatastore()
{
$this->linkDB->save($this->conf->get('resource.page_cache'));
return true;
}
}
/**
@ -482,7 +491,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 +531,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

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

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

249
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/",