aboutsummaryrefslogtreecommitdiffhomepage
path: root/application
diff options
context:
space:
mode:
Diffstat (limited to 'application')
-rw-r--r--application/ApplicationUtils.php39
-rw-r--r--application/Cache.php2
-rw-r--r--application/FeedBuilder.php6
-rw-r--r--application/FileUtils.php26
-rw-r--r--application/History.php20
-rw-r--r--application/HttpUtils.php9
-rw-r--r--application/Languages.php167
-rw-r--r--application/LinkDB.php37
-rw-r--r--application/LinkFilter.php8
-rw-r--r--application/LinkUtils.php17
-rw-r--r--application/NetscapeBookmarkUtils.php27
-rw-r--r--application/PageBuilder.php22
-rw-r--r--application/PluginManager.php7
-rw-r--r--application/SessionManager.php83
-rw-r--r--application/ThemeUtils.php1
-rw-r--r--application/Updater.php21
-rw-r--r--application/Utils.php47
-rw-r--r--application/config/ConfigJson.php15
-rw-r--r--application/config/ConfigManager.php7
-rw-r--r--application/config/ConfigPhp.php4
-rw-r--r--application/config/exception/MissingFieldConfigException.php2
-rw-r--r--application/config/exception/PluginConfigOrderException.php2
-rw-r--r--application/config/exception/UnauthorizedConfigException.php2
-rw-r--r--application/exceptions/IOException.php2
24 files changed, 438 insertions, 135 deletions
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
149 public static function checkPHPVersion($minVersion, $curVersion) 149 public static function checkPHPVersion($minVersion, $curVersion)
150 { 150 {
151 if (version_compare($curVersion, $minVersion) < 0) { 151 if (version_compare($curVersion, $minVersion) < 0) {
152 throw new Exception( 152 $msg = t(
153 'Your PHP version is obsolete!' 153 'Your PHP version is obsolete!'
154 .' Shaarli requires at least PHP '.$minVersion.', and thus cannot run.' 154 . ' Shaarli requires at least PHP %s, and thus cannot run.'
155 .' Your PHP version has known security vulnerabilities and should be' 155 . ' Your PHP version has known security vulnerabilities and should be'
156 .' updated as soon as possible.' 156 . ' updated as soon as possible.'
157 ); 157 );
158 throw new Exception(sprintf($msg, $minVersion));
158 } 159 }
159 } 160 }
160 161
@@ -168,17 +169,18 @@ class ApplicationUtils
168 public static function checkResourcePermissions($conf) 169 public static function checkResourcePermissions($conf)
169 { 170 {
170 $errors = array(); 171 $errors = array();
172 $rainTplDir = rtrim($conf->get('resource.raintpl_tpl'), '/');
171 173
172 // Check script and template directories are readable 174 // Check script and template directories are readable
173 foreach (array( 175 foreach (array(
174 'application', 176 'application',
175 'inc', 177 'inc',
176 'plugins', 178 'plugins',
177 $conf->get('resource.raintpl_tpl'), 179 $rainTplDir,
178 $conf->get('resource.raintpl_tpl').'/'.$conf->get('resource.theme'), 180 $rainTplDir.'/'.$conf->get('resource.theme'),
179 ) as $path) { 181 ) as $path) {
180 if (! is_readable(realpath($path))) { 182 if (! is_readable(realpath($path))) {
181 $errors[] = '"'.$path.'" directory is not readable'; 183 $errors[] = '"'.$path.'" '. t('directory is not readable');
182 } 184 }
183 } 185 }
184 186
@@ -190,10 +192,10 @@ class ApplicationUtils
190 $conf->get('resource.raintpl_tmp'), 192 $conf->get('resource.raintpl_tmp'),
191 ) as $path) { 193 ) as $path) {
192 if (! is_readable(realpath($path))) { 194 if (! is_readable(realpath($path))) {
193 $errors[] = '"'.$path.'" directory is not readable'; 195 $errors[] = '"'.$path.'" '. t('directory is not readable');
194 } 196 }
195 if (! is_writable(realpath($path))) { 197 if (! is_writable(realpath($path))) {
196 $errors[] = '"'.$path.'" directory is not writable'; 198 $errors[] = '"'.$path.'" '. t('directory is not writable');
197 } 199 }
198 } 200 }
199 201
@@ -211,13 +213,28 @@ class ApplicationUtils
211 } 213 }
212 214
213 if (! is_readable(realpath($path))) { 215 if (! is_readable(realpath($path))) {
214 $errors[] = '"'.$path.'" file is not readable'; 216 $errors[] = '"'.$path.'" '. t('file is not readable');
215 } 217 }
216 if (! is_writable(realpath($path))) { 218 if (! is_writable(realpath($path))) {
217 $errors[] = '"'.$path.'" file is not writable'; 219 $errors[] = '"'.$path.'" '. t('file is not writable');
218 } 220 }
219 } 221 }
220 222
221 return $errors; 223 return $errors;
222 } 224 }
225
226 /**
227 * Returns a salted hash representing the current Shaarli version.
228 *
229 * Useful for assets browser cache.
230 *
231 * @param string $currentVersion of Shaarli
232 * @param string $salt User personal salt, also used for the authentication
233 *
234 * @return string version hash
235 */
236 public static function getVersionHash($currentVersion, $salt)
237 {
238 return hash_hmac('sha256', $currentVersion, $salt);
239 }
223} 240}
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 @@
13function purgeCachedPages($pageCacheDir) 13function purgeCachedPages($pageCacheDir)
14{ 14{
15 if (! is_dir($pageCacheDir)) { 15 if (! is_dir($pageCacheDir)) {
16 $error = 'Cannot purge '.$pageCacheDir.': no directory'; 16 $error = sprintf(t('Cannot purge %s: no directory'), $pageCacheDir);
17 error_log($error); 17 error_log($error);
18 return $error; 18 return $error;
19 } 19 }
diff --git a/application/FeedBuilder.php b/application/FeedBuilder.php
index 7377bcec..ebae18b4 100644
--- a/application/FeedBuilder.php
+++ b/application/FeedBuilder.php
@@ -148,11 +148,11 @@ class FeedBuilder
148 $link['url'] = $pageaddr . $link['url']; 148 $link['url'] = $pageaddr . $link['url'];
149 } 149 }
150 if ($this->usePermalinks === true) { 150 if ($this->usePermalinks === true) {
151 $permalink = '<a href="'. $link['url'] .'" title="Direct link">Direct link</a>'; 151 $permalink = '<a href="'. $link['url'] .'" title="'. t('Direct link') .'">'. t('Direct link') .'</a>';
152 } else { 152 } else {
153 $permalink = '<a href="'. $link['guid'] .'" title="Permalink">Permalink</a>'; 153 $permalink = '<a href="'. $link['guid'] .'" title="'. t('Permalink') .'">'. t('Permalink') .'</a>';
154 } 154 }
155 $link['description'] = format_description($link['description'], '', $pageaddr); 155 $link['description'] = format_description($link['description'], '', false, $pageaddr);
156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink; 156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;
157 157
158 $pubDate = $link['created']; 158 $pubDate = $link['created'];
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
50 50
51 /** 51 /**
52 * Read data from a file containing Shaarli database format content. 52 * Read data from a file containing Shaarli database format content.
53 * If the file isn't readable or doesn't exists, default data will be returned. 53 *
54 * If the file isn't readable or doesn't exist, default data will be returned.
54 * 55 *
55 * @param string $file File path. 56 * @param string $file File path.
56 * @param mixed $default The default value to return if the file isn't readable. 57 * @param mixed $default The default value to return if the file isn't readable.
@@ -61,16 +62,21 @@ class FileUtils
61 { 62 {
62 // Note that gzinflate is faster than gzuncompress. 63 // Note that gzinflate is faster than gzuncompress.
63 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439 64 // See: http://www.php.net/manual/en/function.gzdeflate.php#96439
64 if (is_readable($file)) { 65 if (! is_readable($file)) {
65 return unserialize( 66 return $default;
66 gzinflate( 67 }
67 base64_decode( 68
68 substr(file_get_contents($file), strlen(self::$phpPrefix), -strlen(self::$phpSuffix)) 69 $data = file_get_contents($file);
69 ) 70 if ($data == '') {
70 ) 71 return $default;
71 );
72 } 72 }
73 73
74 return $default; 74 return unserialize(
75 gzinflate(
76 base64_decode(
77 substr($data, strlen(self::$phpPrefix), -strlen(self::$phpSuffix))
78 )
79 )
80 );
75 } 81 }
76} 82}
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 @@
16 * - UPDATED: link updated 16 * - UPDATED: link updated
17 * - DELETED: link deleted 17 * - DELETED: link deleted
18 * - SETTINGS: the settings have been updated through the UI. 18 * - SETTINGS: the settings have been updated through the UI.
19 * - IMPORT: bulk links import
19 * 20 *
20 * Note: new events are put at the beginning of the file and history array. 21 * Note: new events are put at the beginning of the file and history array.
21 */ 22 */
@@ -42,6 +43,11 @@ class History
42 const SETTINGS = 'SETTINGS'; 43 const SETTINGS = 'SETTINGS';
43 44
44 /** 45 /**
46 * @var string Action key: a bulk import has been processed.
47 */
48 const IMPORT = 'IMPORT';
49
50 /**
45 * @var string History file path. 51 * @var string History file path.
46 */ 52 */
47 protected $historyFilePath; 53 protected $historyFilePath;
@@ -122,6 +128,16 @@ class History
122 } 128 }
123 129
124 /** 130 /**
131 * Add Event: bulk import.
132 *
133 * Note: we don't store links add/update one by one since it can have a huge impact on performances.
134 */
135 public function importLinks()
136 {
137 $this->addEvent(self::IMPORT);
138 }
139
140 /**
125 * Save a new event and write it in the history file. 141 * Save a new event and write it in the history file.
126 * 142 *
127 * @param string $status Event key, should be defined as constant. 143 * @param string $status Event key, should be defined as constant.
@@ -155,7 +171,7 @@ class History
155 } 171 }
156 172
157 if (! is_writable($this->historyFilePath)) { 173 if (! is_writable($this->historyFilePath)) {
158 throw new Exception('History file isn\'t readable or writable'); 174 throw new Exception(t('History file isn\'t readable or writable'));
159 } 175 }
160 } 176 }
161 177
@@ -166,7 +182,7 @@ class History
166 { 182 {
167 $this->history = FileUtils::readFlatDB($this->historyFilePath, []); 183 $this->history = FileUtils::readFlatDB($this->historyFilePath, []);
168 if ($this->history === false) { 184 if ($this->history === false) {
169 throw new Exception('Could not parse history file'); 185 throw new Exception(t('Could not parse history file'));
170 } 186 }
171 } 187 }
172 188
diff --git a/application/HttpUtils.php b/application/HttpUtils.php
index 2edf5ce2..83a4c5e2 100644
--- a/application/HttpUtils.php
+++ b/application/HttpUtils.php
@@ -82,7 +82,7 @@ function get_http_response($url, $timeout = 30, $maxBytes = 4194304, $curlWriteF
82 } 82 }
83 83
84 // Max download size management 84 // Max download size management
85 curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024); 85 curl_setopt($ch, CURLOPT_BUFFERSIZE, 1024*16);
86 curl_setopt($ch, CURLOPT_NOPROGRESS, false); 86 curl_setopt($ch, CURLOPT_NOPROGRESS, false);
87 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, 87 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION,
88 function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes) 88 function($arg0, $arg1, $arg2, $arg3, $arg4 = 0) use ($maxBytes)
@@ -308,6 +308,13 @@ function server_url($server)
308 $port = $server['HTTP_X_FORWARDED_PORT']; 308 $port = $server['HTTP_X_FORWARDED_PORT'];
309 } 309 }
310 310
311 // This is a workaround for proxies that don't forward the scheme properly.
312 // Connecting over port 443 has to be in HTTPS.
313 // See https://github.com/shaarli/Shaarli/issues/1022
314 if ($port == '443') {
315 $scheme = 'https';
316 }
317
311 if (($scheme == 'http' && $port != '80') 318 if (($scheme == 'http' && $port != '80')
312 || ($scheme == 'https' && $port != '443') 319 || ($scheme == 'https' && $port != '443')
313 ) { 320 ) {
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 @@
1<?php 1<?php
2 2
3namespace Shaarli;
4
5use Gettext\GettextTranslator;
6use Gettext\Merge;
7use Gettext\Translations;
8use Gettext\Translator;
9use Gettext\TranslatorInterface;
10use Shaarli\Config\ConfigManager;
11
3/** 12/**
4 * Wrapper function for translation which match the API 13 * Class Languages
5 * of gettext()/_() and ngettext(). 14 *
15 * Load Shaarli translations using 'gettext/gettext'.
16 * This class allows to either use PHP gettext extension, or a PHP implementation of gettext,
17 * with a fixed language, or dynamically using autoLocale().
6 * 18 *
7 * Not doing translation for now. 19 * Translation files PO/MO files follow gettext standard and must be placed under:
20 * <translation path>/<language>/LC_MESSAGES/<domain>.[po|mo]
8 * 21 *
9 * @param string $text Text to translate. 22 * Pros/cons:
10 * @param string $nText The plural message ID. 23 * - gettext extension is faster
11 * @param int $nb The number of items for plural forms. 24 * - gettext is very system dependent (PHP extension, the locale must be installed, and web server reloaded)
12 * 25 *
13 * @return String Text translated. 26 * Settings:
27 * - translation.mode:
28 * - auto: use default setting (PHP implementation)
29 * - php: use PHP implementation
30 * - gettext: use gettext wrapper
31 * - translation.language:
32 * - auto: use autoLocale() and the language change according to user HTTP headers
33 * - fixed language: e.g. 'fr'
34 * - translation.extensions:
35 * - domain => translation_path: allow plugins and themes to extend the defaut extension
36 * The domain must be unique, and translation path must be relative, and contains the tree mentioned above.
37 *
38 * @package Shaarli
14 */ 39 */
15function t($text, $nText = '', $nb = 0) { 40class Languages
16 if (empty($nText)) { 41{
17 return $text; 42 /**
43 * Core translations domain
44 */
45 const DEFAULT_DOMAIN = 'shaarli';
46
47 /**
48 * @var TranslatorInterface
49 */
50 protected $translator;
51
52 /**
53 * @var string
54 */
55 protected $language;
56
57 /**
58 * @var ConfigManager
59 */
60 protected $conf;
61
62 /**
63 * Languages constructor.
64 *
65 * @param string $language lang determined by autoLocale(), can be overridden.
66 * @param ConfigManager $conf instance.
67 */
68 public function __construct($language, $conf)
69 {
70 $this->conf = $conf;
71 $confLanguage = $this->conf->get('translation.language', 'auto');
72 if ($confLanguage === 'auto' || ! $this->isValidLanguage($confLanguage)) {
73 $this->language = substr($language, 0, 5);
74 } else {
75 $this->language = $confLanguage;
76 }
77
78 if (! extension_loaded('gettext')
79 || in_array($this->conf->get('translation.mode', 'auto'), ['auto', 'php'])
80 ) {
81 $this->initPhpTranslator();
82 } else {
83 $this->initGettextTranslator();
84 }
85
86 // Register default functions (e.g. '__()') to use our Translator
87 $this->translator->register();
88 }
89
90 /**
91 * Initialize the translator using php gettext extension (gettext dependency act as a wrapper).
92 */
93 protected function initGettextTranslator ()
94 {
95 $this->translator = new GettextTranslator();
96 $this->translator->setLanguage($this->language);
97 $this->translator->loadDomain(self::DEFAULT_DOMAIN, 'inc/languages');
98
99 foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
100 if ($domain !== self::DEFAULT_DOMAIN) {
101 $this->translator->loadDomain($domain, $translationPath, false);
102 }
103 }
104 }
105
106 /**
107 * Initialize the translator using a PHP implementation of gettext.
108 *
109 * Note that if language po file doesn't exist, errors are ignored (e.g. not installed language).
110 */
111 protected function initPhpTranslator()
112 {
113 $this->translator = new Translator();
114 $translations = new Translations();
115 // Core translations
116 try {
117 /** @var Translations $translations */
118 $translations = $translations->addFromPoFile('inc/languages/'. $this->language .'/LC_MESSAGES/shaarli.po');
119 $translations->setDomain('shaarli');
120 $this->translator->loadTranslations($translations);
121 } catch (\InvalidArgumentException $e) {}
122
123
124 // Extension translations (plugins, themes, etc.).
125 foreach ($this->conf->get('translation.extensions', []) as $domain => $translationPath) {
126 if ($domain === self::DEFAULT_DOMAIN) {
127 continue;
128 }
129
130 try {
131 /** @var Translations $extension */
132 $extension = Translations::fromPoFile($translationPath . $this->language .'/LC_MESSAGES/'. $domain .'.po');
133 $extension->setDomain($domain);
134 $this->translator->loadTranslations($extension);
135 } catch (\InvalidArgumentException $e) {}
136 }
137 }
138
139 /**
140 * Checks if a language string is valid.
141 *
142 * @param string $language e.g. 'fr' or 'en_US'
143 *
144 * @return bool true if valid, false otherwise
145 */
146 protected function isValidLanguage($language)
147 {
148 return preg_match('/^[a-z]{2}(_[A-Z]{2})?/', $language) === 1;
149 }
150
151 /**
152 * Get the list of available languages for Shaarli.
153 *
154 * @return array List of available languages, with their label.
155 */
156 public static function getAvailableLanguages()
157 {
158 return [
159 'auto' => t('Automatic'),
160 'en' => t('English'),
161 'fr' => t('French'),
162 ];
18 } 163 }
19 $actualForm = $nb > 1 ? $nText : $text;
20 return sprintf($actualForm, $nb);
21} 164}
diff --git a/application/LinkDB.php b/application/LinkDB.php
index 22c1f0ab..c1661d52 100644
--- a/application/LinkDB.php
+++ b/application/LinkDB.php
@@ -133,16 +133,16 @@ class LinkDB implements Iterator, Countable, ArrayAccess
133 { 133 {
134 // TODO: use exceptions instead of "die" 134 // TODO: use exceptions instead of "die"
135 if (!$this->loggedIn) { 135 if (!$this->loggedIn) {
136 die('You are not authorized to add a link.'); 136 die(t('You are not authorized to add a link.'));
137 } 137 }
138 if (!isset($value['id']) || empty($value['url'])) { 138 if (!isset($value['id']) || empty($value['url'])) {
139 die('Internal Error: A link should always have an id and URL.'); 139 die(t('Internal Error: A link should always have an id and URL.'));
140 } 140 }
141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) { 141 if (($offset !== null && ! is_int($offset)) || ! is_int($value['id'])) {
142 die('You must specify an integer as a key.'); 142 die(t('You must specify an integer as a key.'));
143 } 143 }
144 if ($offset !== null && $offset !== $value['id']) { 144 if ($offset !== null && $offset !== $value['id']) {
145 die('Array offset and link ID must be equal.'); 145 die(t('Array offset and link ID must be equal.'));
146 } 146 }
147 147
148 // If the link exists, we reuse the real offset, otherwise new entry 148 // If the link exists, we reuse the real offset, otherwise new entry
@@ -248,13 +248,13 @@ class LinkDB implements Iterator, Countable, ArrayAccess
248 $this->links = array(); 248 $this->links = array();
249 $link = array( 249 $link = array(
250 'id' => 1, 250 'id' => 1,
251 'title'=>' Shaarli: the personal, minimalist, super-fast, no-database delicious clone', 251 'title'=> t('The personal, minimalist, super-fast, database free, bookmarking service'),
252 'url'=>'https://shaarli.readthedocs.io', 252 'url'=>'https://shaarli.readthedocs.io',
253 'description'=>'Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login. 253 'description'=>t('Welcome to Shaarli! This is your first public bookmark. To edit or delete me, you must first login.
254 254
255To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. 255To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page.
256 256
257You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', 257You use the community supported version of the original Shaarli project, by Sebastien Sauvage.'),
258 'private'=>0, 258 'private'=>0,
259 'created'=> new DateTime(), 259 'created'=> new DateTime(),
260 'tags'=>'opensource software' 260 'tags'=>'opensource software'
@@ -264,9 +264,9 @@ You use the community supported version of the original Shaarli project, by Seba
264 264
265 $link = array( 265 $link = array(
266 'id' => 0, 266 'id' => 0,
267 'title'=>'My secret stuff... - Pastebin.com', 267 'title'=> t('My secret stuff... - Pastebin.com'),
268 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=', 268 'url'=>'http://sebsauvage.net/paste/?8434b27936c09649#bR7XsXhoTiLcqCpQbmOpBi3rq2zzQUC5hBI7ZT1O3x8=',
269 'description'=>'Shhhh! I\'m a private link only YOU can see. You can delete me too.', 269 'description'=> t('Shhhh! I\'m a private link only YOU can see. You can delete me too.'),
270 'private'=>1, 270 'private'=>1,
271 'created'=> new DateTime('1 minute ago'), 271 'created'=> new DateTime('1 minute ago'),
272 'tags'=>'secretstuff', 272 'tags'=>'secretstuff',
@@ -289,13 +289,15 @@ You use the community supported version of the original Shaarli project, by Seba
289 return; 289 return;
290 } 290 }
291 291
292 $this->urls = [];
293 $this->ids = [];
292 $this->links = FileUtils::readFlatDB($this->datastore, []); 294 $this->links = FileUtils::readFlatDB($this->datastore, []);
293 295
294 $toremove = array(); 296 $toremove = array();
295 foreach ($this->links as $key => &$link) { 297 foreach ($this->links as $key => &$link) {
296 if (! $this->loggedIn && $link['private'] != 0) { 298 if (! $this->loggedIn && $link['private'] != 0) {
297 // Transition for not upgraded databases. 299 // Transition for not upgraded databases.
298 $toremove[] = $key; 300 unset($this->links[$key]);
299 continue; 301 continue;
300 } 302 }
301 303
@@ -329,14 +331,10 @@ You use the community supported version of the original Shaarli project, by Seba
329 } 331 }
330 $link['shorturl'] = smallHash($link['linkdate']); 332 $link['shorturl'] = smallHash($link['linkdate']);
331 } 333 }
332 }
333 334
334 // If user is not logged in, filter private links. 335 $this->urls[$link['url']] = $key;
335 foreach ($toremove as $offset) { 336 $this->ids[$link['id']] = $key;
336 unset($this->links[$offset]);
337 } 337 }
338
339 $this->reorder();
340 } 338 }
341 339
342 /** 340 /**
@@ -346,6 +344,7 @@ You use the community supported version of the original Shaarli project, by Seba
346 */ 344 */
347 private function write() 345 private function write()
348 { 346 {
347 $this->reorder();
349 FileUtils::writeFlatDB($this->datastore, $this->links); 348 FileUtils::writeFlatDB($this->datastore, $this->links);
350 } 349 }
351 350
@@ -528,8 +527,8 @@ You use the community supported version of the original Shaarli project, by Seba
528 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order; 527 return $a['created'] < $b['created'] ? 1 * $order : -1 * $order;
529 }); 528 });
530 529
531 $this->urls = array(); 530 $this->urls = [];
532 $this->ids = array(); 531 $this->ids = [];
533 foreach ($this->links as $key => $link) { 532 foreach ($this->links as $key => $link) {
534 $this->urls[$link['url']] = $key; 533 $this->urls[$link['url']] = $key;
535 $this->ids[$link['id']] = $key; 534 $this->ids[$link['id']] = $key;
diff --git a/application/LinkFilter.php b/application/LinkFilter.php
index 99ecd1e2..12376e27 100644
--- a/application/LinkFilter.php
+++ b/application/LinkFilter.php
@@ -444,5 +444,11 @@ class LinkFilter
444 444
445class LinkNotFoundException extends Exception 445class LinkNotFoundException extends Exception
446{ 446{
447 protected $message = 'The link you are trying to reach does not exist or has been deleted.'; 447 /**
448 * LinkNotFoundException constructor.
449 */
450 public function __construct()
451 {
452 $this->message = t('The link you are trying to reach does not exist or has been deleted.');
453 }
448} 454}
diff --git a/application/LinkUtils.php b/application/LinkUtils.php
index c0dd32a6..3705f7e9 100644
--- a/application/LinkUtils.php
+++ b/application/LinkUtils.php
@@ -123,14 +123,15 @@ function count_private($links)
123 * 123 *
124 * @param string $text input string. 124 * @param string $text input string.
125 * @param string $redirector if a redirector is set, use it to gerenate links. 125 * @param string $redirector if a redirector is set, use it to gerenate links.
126 * @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not.
126 * 127 *
127 * @return string returns $text with all links converted to HTML links. 128 * @return string returns $text with all links converted to HTML links.
128 * 129 *
129 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722 130 * @see Function inspired from http://www.php.net/manual/en/function.preg-replace.php#85722
130 */ 131 */
131function text2clickable($text, $redirector = '') 132function text2clickable($text, $redirector = '', $urlEncode = true)
132{ 133{
133 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si'; 134 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
134 135
135 if (empty($redirector)) { 136 if (empty($redirector)) {
136 return preg_replace($regex, '<a href="$1">$1</a>', $text); 137 return preg_replace($regex, '<a href="$1">$1</a>', $text);
@@ -138,8 +139,9 @@ function text2clickable($text, $redirector = '')
138 // Redirector is set, urlencode the final URL. 139 // Redirector is set, urlencode the final URL.
139 return preg_replace_callback( 140 return preg_replace_callback(
140 $regex, 141 $regex,
141 function ($matches) use ($redirector) { 142 function ($matches) use ($redirector, $urlEncode) {
142 return '<a href="' . $redirector . urlencode($matches[1]) .'">'. $matches[1] .'</a>'; 143 $url = $urlEncode ? urlencode($matches[1]) : $matches[1];
144 return '<a href="' . $redirector . $url .'">'. $matches[1] .'</a>';
143 }, 145 },
144 $text 146 $text
145 ); 147 );
@@ -185,12 +187,13 @@ function space2nbsp($text)
185 * 187 *
186 * @param string $description shaare's description. 188 * @param string $description shaare's description.
187 * @param string $redirector if a redirector is set, use it to gerenate links. 189 * @param string $redirector if a redirector is set, use it to gerenate links.
190 * @param bool $urlEncode Use `urlencode()` on the URL after the redirector or not.
188 * @param string $indexUrl URL to Shaarli's index. 191 * @param string $indexUrl URL to Shaarli's index.
189 * 192
190 * @return string formatted description. 193 * @return string formatted description.
191 */ 194 */
192function format_description($description, $redirector = '', $indexUrl = '') { 195function format_description($description, $redirector = '', $urlEncode = true, $indexUrl = '') {
193 return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector), $indexUrl))); 196 return nl2br(space2nbsp(hashtag_autolink(text2clickable($description, $redirector, $urlEncode), $indexUrl)));
194} 197}
195 198
196/** 199/**
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
32 { 32 {
33 // see tpl/export.html for possible values 33 // see tpl/export.html for possible values
34 if (! in_array($selection, array('all', 'public', 'private'))) { 34 if (! in_array($selection, array('all', 'public', 'private'))) {
35 throw new Exception('Invalid export selection: "'.$selection.'"'); 35 throw new Exception(t('Invalid export selection:') .' "'.$selection.'"');
36 } 36 }
37 37
38 $bookmarkLinks = array(); 38 $bookmarkLinks = array();
39
40 foreach ($linkDb as $link) { 39 foreach ($linkDb as $link) {
41 if ($link['private'] != 0 && $selection == 'public') { 40 if ($link['private'] != 0 && $selection == 'public') {
42 continue; 41 continue;
@@ -66,6 +65,7 @@ class NetscapeBookmarkUtils
66 * @param int $importCount how many links were imported 65 * @param int $importCount how many links were imported
67 * @param int $overwriteCount how many links were overwritten 66 * @param int $overwriteCount how many links were overwritten
68 * @param int $skipCount how many links were skipped 67 * @param int $skipCount how many links were skipped
68 * @param int $duration how many seconds did the import take
69 * 69 *
70 * @return string Summary of the bookmark import status 70 * @return string Summary of the bookmark import status
71 */ 71 */
@@ -74,16 +74,18 @@ class NetscapeBookmarkUtils
74 $filesize, 74 $filesize,
75 $importCount=0, 75 $importCount=0,
76 $overwriteCount=0, 76 $overwriteCount=0,
77 $skipCount=0 77 $skipCount=0,
78 $duration=0
78 ) 79 )
79 { 80 {
80 $status = 'File '.$filename.' ('.$filesize.' bytes) '; 81 $status = sprintf(t('File %s (%d bytes) '), $filename, $filesize);
81 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) { 82 if ($importCount == 0 && $overwriteCount == 0 && $skipCount == 0) {
82 $status .= 'has an unknown file format. Nothing was imported.'; 83 $status .= t('has an unknown file format. Nothing was imported.');
83 } else { 84 } else {
84 $status .= 'was successfully processed: '.$importCount.' links imported, '; 85 $status .= vsprintf(
85 $status .= $overwriteCount.' links overwritten, '; 86 t('was successfully processed in %d seconds: %d links imported, %d links overwritten, %d links skipped.'),
86 $status .= $skipCount.' links skipped.'; 87 [$duration, $importCount, $overwriteCount, $skipCount]
88 );
87 } 89 }
88 return $status; 90 return $status;
89 } 91 }
@@ -101,6 +103,7 @@ class NetscapeBookmarkUtils
101 */ 103 */
102 public static function import($post, $files, $linkDb, $conf, $history) 104 public static function import($post, $files, $linkDb, $conf, $history)
103 { 105 {
106 $start = time();
104 $filename = $files['filetoupload']['name']; 107 $filename = $files['filetoupload']['name'];
105 $filesize = $files['filetoupload']['size']; 108 $filesize = $files['filetoupload']['size'];
106 $data = file_get_contents($files['filetoupload']['tmp_name']); 109 $data = file_get_contents($files['filetoupload']['tmp_name']);
@@ -184,7 +187,6 @@ class NetscapeBookmarkUtils
184 $linkDb[$existingLink['id']] = $newLink; 187 $linkDb[$existingLink['id']] = $newLink;
185 $importCount++; 188 $importCount++;
186 $overwriteCount++; 189 $overwriteCount++;
187 $history->updateLink($newLink);
188 continue; 190 continue;
189 } 191 }
190 192
@@ -196,16 +198,19 @@ class NetscapeBookmarkUtils
196 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']); 198 $newLink['shorturl'] = link_small_hash($newLink['created'], $newLink['id']);
197 $linkDb[$newLink['id']] = $newLink; 199 $linkDb[$newLink['id']] = $newLink;
198 $importCount++; 200 $importCount++;
199 $history->addLink($newLink);
200 } 201 }
201 202
202 $linkDb->save($conf->get('resource.page_cache')); 203 $linkDb->save($conf->get('resource.page_cache'));
204 $history->importLinks();
205
206 $duration = time() - $start;
203 return self::importStatus( 207 return self::importStatus(
204 $filename, 208 $filename,
205 $filesize, 209 $filesize,
206 $importCount, 210 $importCount,
207 $overwriteCount, 211 $overwriteCount,
208 $skipCount 212 $skipCount,
213 $duration
209 ); 214 );
210 } 215 }
211} 216}
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
32 * 32 *
33 * @param ConfigManager $conf Configuration Manager instance (reference). 33 * @param ConfigManager $conf Configuration Manager instance (reference).
34 * @param LinkDB $linkDB instance. 34 * @param LinkDB $linkDB instance.
35 * @param string $token Session token
35 */ 36 */
36 public function __construct(&$conf, $linkDB = null) 37 public function __construct(&$conf, $linkDB = null, $token = null)
37 { 38 {
38 $this->tpl = false; 39 $this->tpl = false;
39 $this->conf = $conf; 40 $this->conf = $conf;
40 $this->linkDB = $linkDB; 41 $this->linkDB = $linkDB;
42 $this->token = $token;
41 } 43 }
42 44
43 /** 45 /**
@@ -49,7 +51,7 @@ class PageBuilder
49 51
50 try { 52 try {
51 $version = ApplicationUtils::checkUpdate( 53 $version = ApplicationUtils::checkUpdate(
52 shaarli_version, 54 SHAARLI_VERSION,
53 $this->conf->get('resource.update_check'), 55 $this->conf->get('resource.update_check'),
54 $this->conf->get('updates.check_updates_interval'), 56 $this->conf->get('updates.check_updates_interval'),
55 $this->conf->get('updates.check_updates'), 57 $this->conf->get('updates.check_updates'),
@@ -75,7 +77,11 @@ class PageBuilder
75 } 77 }
76 $this->tpl->assign('searchcrits', $searchcrits); 78 $this->tpl->assign('searchcrits', $searchcrits);
77 $this->tpl->assign('source', index_url($_SERVER)); 79 $this->tpl->assign('source', index_url($_SERVER));
78 $this->tpl->assign('version', shaarli_version); 80 $this->tpl->assign('version', SHAARLI_VERSION);
81 $this->tpl->assign(
82 'version_hash',
83 ApplicationUtils::getVersionHash(SHAARLI_VERSION, $this->conf->get('credentials.salt'))
84 );
79 $this->tpl->assign('scripturl', index_url($_SERVER)); 85 $this->tpl->assign('scripturl', index_url($_SERVER));
80 $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links? 86 $this->tpl->assign('privateonly', !empty($_SESSION['privateonly'])); // Show only private links?
81 $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly'])); 87 $this->tpl->assign('untaggedonly', !empty($_SESSION['untaggedonly']));
@@ -88,7 +94,8 @@ class PageBuilder
88 $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true)); 94 $this->tpl->assign('showatom', $this->conf->get('feed.show_atom', true));
89 $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss'); 95 $this->tpl->assign('feed_type', $this->conf->get('feed.show_atom', true) !== false ? 'atom' : 'rss');
90 $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false)); 96 $this->tpl->assign('hide_timestamps', $this->conf->get('privacy.hide_timestamps', false));
91 $this->tpl->assign('token', getToken($this->conf)); 97 $this->tpl->assign('token', $this->token);
98
92 if ($this->linkDB !== null) { 99 if ($this->linkDB !== null) {
93 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag()); 100 $this->tpl->assign('tags', $this->linkDB->linksCountPerTag());
94 } 101 }
@@ -154,9 +161,12 @@ class PageBuilder
154 * 161 *
155 * @param string $message A messate to display what is not found 162 * @param string $message A messate to display what is not found
156 */ 163 */
157 public function render404($message = 'The page you are trying to reach does not exist or has been deleted.') 164 public function render404($message = '')
158 { 165 {
159 header($_SERVER['SERVER_PROTOCOL'] . ' 404 Not Found'); 166 if (empty($message)) {
167 $message = t('The page you are trying to reach does not exist or has been deleted.');
168 }
169 header($_SERVER['SERVER_PROTOCOL'] .' '. t('404 Not Found'));
160 $this->tpl->assign('error_message', $message); 170 $this->tpl->assign('error_message', $message);
161 $this->renderPage('404'); 171 $this->renderPage('404');
162 } 172 }
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
188 $metaData[$plugin] = parse_ini_file($metaFile); 188 $metaData[$plugin] = parse_ini_file($metaFile);
189 $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins); 189 $metaData[$plugin]['order'] = array_search($plugin, $this->authorizedPlugins);
190 190
191 if (isset($metaData[$plugin]['description'])) {
192 $metaData[$plugin]['description'] = t($metaData[$plugin]['description']);
193 }
191 // Read parameters and format them into an array. 194 // Read parameters and format them into an array.
192 if (isset($metaData[$plugin]['parameters'])) { 195 if (isset($metaData[$plugin]['parameters'])) {
193 $params = explode(';', $metaData[$plugin]['parameters']); 196 $params = explode(';', $metaData[$plugin]['parameters']);
@@ -203,7 +206,7 @@ class PluginManager
203 $metaData[$plugin]['parameters'][$param]['value'] = ''; 206 $metaData[$plugin]['parameters'][$param]['value'] = '';
204 // Optional parameter description in parameter.PARAM_NAME= 207 // Optional parameter description in parameter.PARAM_NAME=
205 if (isset($metaData[$plugin]['parameter.'. $param])) { 208 if (isset($metaData[$plugin]['parameter.'. $param])) {
206 $metaData[$plugin]['parameters'][$param]['desc'] = $metaData[$plugin]['parameter.'. $param]; 209 $metaData[$plugin]['parameters'][$param]['desc'] = t($metaData[$plugin]['parameter.'. $param]);
207 } 210 }
208 } 211 }
209 } 212 }
@@ -237,6 +240,6 @@ class PluginFileNotFoundException extends Exception
237 */ 240 */
238 public function __construct($pluginName) 241 public function __construct($pluginName)
239 { 242 {
240 $this->message = 'Plugin "'. $pluginName .'" files not found.'; 243 $this->message = sprintf(t('Plugin "%s" files not found.'), $pluginName);
241 } 244 }
242} 245}
diff --git a/application/SessionManager.php b/application/SessionManager.php
new file mode 100644
index 00000000..71f0b38d
--- /dev/null
+++ b/application/SessionManager.php
@@ -0,0 +1,83 @@
1<?php
2namespace Shaarli;
3
4/**
5 * Manages the server-side session
6 */
7class SessionManager
8{
9 protected $session = [];
10
11 /**
12 * Constructor
13 *
14 * @param array $session The $_SESSION array (reference)
15 * @param ConfigManager $conf ConfigManager instance
16 */
17 public function __construct(& $session, $conf)
18 {
19 $this->session = &$session;
20 $this->conf = $conf;
21 }
22
23 /**
24 * Generates a session token
25 *
26 * @return string token
27 */
28 public function generateToken()
29 {
30 $token = sha1(uniqid('', true) .'_'. mt_rand() . $this->conf->get('credentials.salt'));
31 $this->session['tokens'][$token] = 1;
32 return $token;
33 }
34
35 /**
36 * Checks the validity of a session token, and destroys it afterwards
37 *
38 * @param string $token The token to check
39 *
40 * @return bool true if the token is valid, else false
41 */
42 public function checkToken($token)
43 {
44 if (! isset($this->session['tokens'][$token])) {
45 // the token is wrong, or has already been used
46 return false;
47 }
48
49 // destroy the token to prevent future use
50 unset($this->session['tokens'][$token]);
51 return true;
52 }
53
54 /**
55 * Validate session ID to prevent Full Path Disclosure.
56 *
57 * See #298.
58 * The session ID's format depends on the hash algorithm set in PHP settings
59 *
60 * @param string $sessionId Session ID
61 *
62 * @return true if valid, false otherwise.
63 *
64 * @see http://php.net/manual/en/function.hash-algos.php
65 * @see http://php.net/manual/en/session.configuration.php
66 */
67 public static function checkId($sessionId)
68 {
69 if (empty($sessionId)) {
70 return false;
71 }
72
73 if (!$sessionId) {
74 return false;
75 }
76
77 if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
78 return false;
79 }
80
81 return true;
82 }
83}
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
22 */ 22 */
23 public static function getThemes($tplDir) 23 public static function getThemes($tplDir)
24 { 24 {
25 $tplDir = rtrim($tplDir, '/');
25 $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR); 26 $allTheme = glob($tplDir.'/*', GLOB_ONLYDIR);
26 $themes = []; 27 $themes = [];
27 foreach ($allTheme as $value) { 28 foreach ($allTheme as $value) {
diff --git a/application/Updater.php b/application/Updater.php
index 40a15906..8d2bd577 100644
--- a/application/Updater.php
+++ b/application/Updater.php
@@ -73,7 +73,7 @@ class Updater
73 } 73 }
74 74
75 if ($this->methods === null) { 75 if ($this->methods === null) {
76 throw new UpdaterException('Couldn\'t retrieve Updater class methods.'); 76 throw new UpdaterException(t('Couldn\'t retrieve Updater class methods.'));
77 } 77 }
78 78
79 foreach ($this->methods as $method) { 79 foreach ($this->methods as $method) {
@@ -398,7 +398,7 @@ class Updater
398 */ 398 */
399 public function updateMethodCheckUpdateRemoteBranch() 399 public function updateMethodCheckUpdateRemoteBranch()
400 { 400 {
401 if (shaarli_version === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') { 401 if (SHAARLI_VERSION === 'dev' || $this->conf->get('updates.check_updates_branch') === 'latest') {
402 return true; 402 return true;
403 } 403 }
404 404
@@ -413,7 +413,7 @@ class Updater
413 $latestMajor = $matches[1]; 413 $latestMajor = $matches[1];
414 414
415 // Get current major version digit 415 // Get current major version digit
416 preg_match('/(\d+)\.\d+$/', shaarli_version, $matches); 416 preg_match('/(\d+)\.\d+$/', SHAARLI_VERSION, $matches);
417 $currentMajor = $matches[1]; 417 $currentMajor = $matches[1];
418 418
419 if ($currentMajor === $latestMajor) { 419 if ($currentMajor === $latestMajor) {
@@ -436,6 +436,15 @@ class Updater
436 } 436 }
437 return true; 437 return true;
438 } 438 }
439
440 /**
441 * Save the datastore -> the link order is now applied when links are saved.
442 */
443 public function updateMethodReorderDatastore()
444 {
445 $this->linkDB->save($this->conf->get('resource.page_cache'));
446 return true;
447 }
439} 448}
440 449
441/** 450/**
@@ -482,7 +491,7 @@ class UpdaterException extends Exception
482 } 491 }
483 492
484 if (! empty($this->method)) { 493 if (! empty($this->method)) {
485 $out .= 'An error occurred while running the update '. $this->method . PHP_EOL; 494 $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
486 } 495 }
487 496
488 if (! empty($this->previous)) { 497 if (! empty($this->previous)) {
@@ -522,11 +531,11 @@ function read_updates_file($updatesFilepath)
522function write_updates_file($updatesFilepath, $updates) 531function write_updates_file($updatesFilepath, $updates)
523{ 532{
524 if (empty($updatesFilepath)) { 533 if (empty($updatesFilepath)) {
525 throw new Exception('Updates file path is not set, can\'t write updates.'); 534 throw new Exception(t('Updates file path is not set, can\'t write updates.'));
526 } 535 }
527 536
528 $res = file_put_contents($updatesFilepath, implode(';', $updates)); 537 $res = file_put_contents($updatesFilepath, implode(';', $updates));
529 if ($res === false) { 538 if ($res === false) {
530 throw new Exception('Unable to write updates in '. $updatesFilepath . '.'); 539 throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
531 } 540 }
532} 541}
diff --git a/application/Utils.php b/application/Utils.php
index 4a2f5561..97b12fcf 100644
--- a/application/Utils.php
+++ b/application/Utils.php
@@ -182,36 +182,6 @@ function generateLocation($referer, $host, $loopTerms = array())
182} 182}
183 183
184/** 184/**
185 * Validate session ID to prevent Full Path Disclosure.
186 *
187 * See #298.
188 * The session ID's format depends on the hash algorithm set in PHP settings
189 *
190 * @param string $sessionId Session ID
191 *
192 * @return true if valid, false otherwise.
193 *
194 * @see http://php.net/manual/en/function.hash-algos.php
195 * @see http://php.net/manual/en/session.configuration.php
196 */
197function is_session_id_valid($sessionId)
198{
199 if (empty($sessionId)) {
200 return false;
201 }
202
203 if (!$sessionId) {
204 return false;
205 }
206
207 if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
208 return false;
209 }
210
211 return true;
212}
213
214/**
215 * Sniff browser language to set the locale automatically. 185 * Sniff browser language to set the locale automatically.
216 * Note that is may not work on your server if the corresponding locale is not installed. 186 * Note that is may not work on your server if the corresponding locale is not installed.
217 * 187 *
@@ -452,7 +422,7 @@ function get_max_upload_size($limitPost, $limitUpload, $format = true)
452 */ 422 */
453function alphabetical_sort(&$data, $reverse = false, $byKeys = false) 423function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
454{ 424{
455 $callback = function($a, $b) use ($reverse) { 425 $callback = function ($a, $b) use ($reverse) {
456 // Collator is part of PHP intl. 426 // Collator is part of PHP intl.
457 if (class_exists('Collator')) { 427 if (class_exists('Collator')) {
458 $collator = new Collator(setlocale(LC_COLLATE, 0)); 428 $collator = new Collator(setlocale(LC_COLLATE, 0));
@@ -470,3 +440,18 @@ function alphabetical_sort(&$data, $reverse = false, $byKeys = false)
470 usort($data, $callback); 440 usort($data, $callback);
471 } 441 }
472} 442}
443
444/**
445 * Wrapper function for translation which match the API
446 * of gettext()/_() and ngettext().
447 *
448 * @param string $text Text to translate.
449 * @param string $nText The plural message ID.
450 * @param int $nb The number of items for plural forms.
451 * @param string $domain The domain where the translation is stored (default: shaarli).
452 *
453 * @return string Text translated.
454 */
455function t($text, $nText = '', $nb = 1, $domain = 'shaarli') {
456 return dn__($domain, $text, $nText, $nb);
457}
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
22 $data = json_decode($data, true); 22 $data = json_decode($data, true);
23 if ($data === null) { 23 if ($data === null) {
24 $errorCode = json_last_error(); 24 $errorCode = json_last_error();
25 $error = 'An error occurred while parsing JSON configuration file ('. $filepath .'): error code #'; 25 $error = sprintf(
26 $error .= $errorCode. '<br>➜ <code>' . json_last_error_msg() .'</code>'; 26 'An error occurred while parsing JSON configuration file (%s): error code #%d',
27 $filepath,
28 $errorCode
29 );
30 $error .= '<br>➜ <code>' . json_last_error_msg() .'</code>';
27 if ($errorCode === JSON_ERROR_SYNTAX) { 31 if ($errorCode === JSON_ERROR_SYNTAX) {
28 $error .= '<br>Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as '; 32 $error .= '<br>';
33 $error .= 'Please check your JSON syntax (without PHP comment tags) using a JSON lint tool such as ';
29 $error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.'; 34 $error .= '<a href="http://jsonlint.com/">jsonlint.com</a>.';
30 } 35 }
31 throw new \Exception($error); 36 throw new \Exception($error);
@@ -44,8 +49,8 @@ class ConfigJson implements ConfigIO
44 if (!file_put_contents($filepath, $data)) { 49 if (!file_put_contents($filepath, $data)) {
45 throw new \IOException( 50 throw new \IOException(
46 $filepath, 51 $filepath,
47 'Shaarli could not create the config file. 52 t('Shaarli could not create the config file. '.
48 Please make sure Shaarli has the right to write in the folder is it installed in.' 53 'Please make sure Shaarli has the right to write in the folder is it installed in.')
49 ); 54 );
50 } 55 }
51 } 56 }
diff --git a/application/config/ConfigManager.php b/application/config/ConfigManager.php
index 32f6ef6d..9e4c9f63 100644
--- a/application/config/ConfigManager.php
+++ b/application/config/ConfigManager.php
@@ -132,7 +132,7 @@ class ConfigManager
132 public function set($setting, $value, $write = false, $isLoggedIn = false) 132 public function set($setting, $value, $write = false, $isLoggedIn = false)
133 { 133 {
134 if (empty($setting) || ! is_string($setting)) { 134 if (empty($setting) || ! is_string($setting)) {
135 throw new \Exception('Invalid setting key parameter. String expected, got: '. gettype($setting)); 135 throw new \Exception(t('Invalid setting key parameter. String expected, got: '). gettype($setting));
136 } 136 }
137 137
138 // During the ConfigIO transition, map legacy settings to the new ones. 138 // During the ConfigIO transition, map legacy settings to the new ones.
@@ -317,6 +317,7 @@ class ConfigManager
317 $this->setEmpty('general.header_link', '?'); 317 $this->setEmpty('general.header_link', '?');
318 $this->setEmpty('general.links_per_page', 20); 318 $this->setEmpty('general.links_per_page', 20);
319 $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS); 319 $this->setEmpty('general.enabled_plugins', self::$DEFAULT_PLUGINS);
320 $this->setEmpty('general.default_note_title', 'Note: ');
320 321
321 $this->setEmpty('updates.check_updates', false); 322 $this->setEmpty('updates.check_updates', false);
322 $this->setEmpty('updates.check_updates_branch', 'stable'); 323 $this->setEmpty('updates.check_updates_branch', 'stable');
@@ -338,6 +339,10 @@ class ConfigManager
338 $this->setEmpty('redirector.url', ''); 339 $this->setEmpty('redirector.url', '');
339 $this->setEmpty('redirector.encode_url', true); 340 $this->setEmpty('redirector.encode_url', true);
340 341
342 $this->setEmpty('translation.language', 'auto');
343 $this->setEmpty('translation.mode', 'php');
344 $this->setEmpty('translation.extensions', []);
345
341 $this->setEmpty('plugins', array()); 346 $this->setEmpty('plugins', array());
342 } 347 }
343 348
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
118 ) { 118 ) {
119 throw new \IOException( 119 throw new \IOException(
120 $filepath, 120 $filepath,
121 'Shaarli could not create the config file. 121 t('Shaarli could not create the config file. '.
122 Please make sure Shaarli has the right to write in the folder is it installed in.' 122 'Please make sure Shaarli has the right to write in the folder is it installed in.')
123 ); 123 );
124 } 124 }
125 } 125 }
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
18 public function __construct($field) 18 public function __construct($field)
19 { 19 {
20 $this->field = $field; 20 $this->field = $field;
21 $this->message = 'Configuration value is required for '. $this->field; 21 $this->message = sprintf(t('Configuration value is required for %s'), $this->field);
22 } 22 }
23} 23}
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
12 */ 12 */
13 public function __construct() 13 public function __construct()
14 { 14 {
15 $this->message = 'An error occurred while trying to save plugins loading order.'; 15 $this->message = t('An error occurred while trying to save plugins loading order.');
16 } 16 }
17} 17}
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
13 */ 13 */
14 public function __construct() 14 public function __construct()
15 { 15 {
16 $this->message = 'You are not authorized to alter config.'; 16 $this->message = t('You are not authorized to alter config.');
17 } 17 }
18} 18}
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
16 public function __construct($path, $message = '') 16 public function __construct($path, $message = '')
17 { 17 {
18 $this->path = $path; 18 $this->path = $path;
19 $this->message = empty($message) ? 'Error accessing' : $message; 19 $this->message = empty($message) ? t('Error accessing') : $message;
20 $this->message .= ' "' . $this->path .'"'; 20 $this->message .= ' "' . $this->path .'"';
21 } 21 }
22} 22}