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.php4
-rw-r--r--application/FileUtils.php26
-rw-r--r--application/History.php20
-rw-r--r--application/Languages.php167
-rw-r--r--application/LinkDB.php20
-rw-r--r--application/LinkFilter.php139
-rw-r--r--application/LinkUtils.php2
-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.php12
-rw-r--r--application/Utils.php47
-rw-r--r--application/config/ConfigJson.php15
-rw-r--r--application/config/ConfigManager.php8
-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
23 files changed, 488 insertions, 165 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..3cfaafb4 100644
--- a/application/FeedBuilder.php
+++ b/application/FeedBuilder.php
@@ -148,9 +148,9 @@ 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'], '', $pageaddr);
156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink; 156 $link['description'] .= PHP_EOL .'<br>&#8212; '. $permalink;
diff --git a/application/FileUtils.php b/application/FileUtils.php
index a167f642..918cb83b 100644
--- a/application/FileUtils.php
+++ b/application/FileUtils.php
@@ -50,7 +50,8 @@ class FileUtils
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/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 eace625e..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',
diff --git a/application/LinkFilter.php b/application/LinkFilter.php
index 95519528..12376e27 100644
--- a/application/LinkFilter.php
+++ b/application/LinkFilter.php
@@ -250,6 +250,51 @@ class LinkFilter
250 } 250 }
251 251
252 /** 252 /**
253 * generate a regex fragment out of a tag
254 * @param string $tag to to generate regexs from. may start with '-' to negate, contain '*' as wildcard
255 * @return string generated regex fragment
256 */
257 private static function tag2regex($tag)
258 {
259 $len = strlen($tag);
260 if(!$len || $tag === "-" || $tag === "*"){
261 // nothing to search, return empty regex
262 return '';
263 }
264 if($tag[0] === "-") {
265 // query is negated
266 $i = 1; // use offset to start after '-' character
267 $regex = '(?!'; // create negative lookahead
268 } else {
269 $i = 0; // start at first character
270 $regex = '(?='; // use positive lookahead
271 }
272 $regex .= '.*(?:^| )'; // before tag may only be a space or the beginning
273 // iterate over string, separating it into placeholder and content
274 for(; $i < $len; $i++){
275 if($tag[$i] === '*'){
276 // placeholder found
277 $regex .= '[^ ]*?';
278 } else {
279 // regular characters
280 $offset = strpos($tag, '*', $i);
281 if($offset === false){
282 // no placeholder found, set offset to end of string
283 $offset = $len;
284 }
285 // subtract one, as we want to get before the placeholder or end of string
286 $offset -= 1;
287 // we got a tag name that we want to search for. escape any regex characters to prevent conflicts.
288 $regex .= preg_quote(substr($tag, $i, $offset - $i + 1), '/');
289 // move $i on
290 $i = $offset;
291 }
292 }
293 $regex .= '(?:$| ))'; // after the tag may only be a space or the end
294 return $regex;
295 }
296
297 /**
253 * Returns the list of links associated with a given list of tags 298 * Returns the list of links associated with a given list of tags
254 * 299 *
255 * You can specify one or more tags, separated by space or a comma, e.g. 300 * You can specify one or more tags, separated by space or a comma, e.g.
@@ -263,20 +308,32 @@ class LinkFilter
263 */ 308 */
264 public function filterTags($tags, $casesensitive = false, $visibility = 'all') 309 public function filterTags($tags, $casesensitive = false, $visibility = 'all')
265 { 310 {
266 // Implode if array for clean up. 311 // get single tags (we may get passed an array, even though the docs say different)
267 $tags = is_array($tags) ? trim(implode(' ', $tags)) : $tags; 312 $inputTags = $tags;
268 if (empty($tags)) { 313 if(!is_array($tags)) {
314 // we got an input string, split tags
315 $inputTags = preg_split('/(?:\s+)|,/', $inputTags, -1, PREG_SPLIT_NO_EMPTY);
316 }
317
318 if(!count($inputTags)){
319 // no input tags
269 return $this->noFilter($visibility); 320 return $this->noFilter($visibility);
270 } 321 }
271 322
272 $searchtags = self::tagsStrToArray($tags, $casesensitive); 323 // build regex from all tags
273 $filtered = array(); 324 $re = '/^' . implode(array_map("self::tag2regex", $inputTags)) . '.*$/';
274 if (empty($searchtags)) { 325 if(!$casesensitive) {
275 return $filtered; 326 // make regex case insensitive
327 $re .= 'i';
276 } 328 }
277 329
330 // create resulting array
331 $filtered = array();
332
333 // iterate over each link
278 foreach ($this->links as $key => $link) { 334 foreach ($this->links as $key => $link) {
279 // ignore non private links when 'privatonly' is on. 335 // check level of visibility
336 // ignore non private links when 'privateonly' is on.
280 if ($visibility !== 'all') { 337 if ($visibility !== 'all') {
281 if (! $link['private'] && $visibility === 'private') { 338 if (! $link['private'] && $visibility === 'private') {
282 continue; 339 continue;
@@ -284,25 +341,27 @@ class LinkFilter
284 continue; 341 continue;
285 } 342 }
286 } 343 }
287 344 $search = $link['tags']; // build search string, start with tags of current link
288 $linktags = self::tagsStrToArray($link['tags'], $casesensitive); 345 if(strlen(trim($link['description'])) && strpos($link['description'], '#') !== false){
289 346 // description given and at least one possible tag found
290 $found = true; 347 $descTags = array();
291 for ($i = 0 ; $i < count($searchtags) && $found; $i++) { 348 // find all tags in the form of #tag in the description
292 // Exclusive search, quit if tag found. 349 preg_match_all(
293 // Or, tag not found in the link, quit. 350 '/(?<![' . self::$HASHTAG_CHARS . '])#([' . self::$HASHTAG_CHARS . ']+?)\b/sm',
294 if (($searchtags[$i][0] == '-' 351 $link['description'],
295 && $this->searchTagAndHashTag(substr($searchtags[$i], 1), $linktags, $link['description'])) 352 $descTags
296 || ($searchtags[$i][0] != '-') 353 );
297 && ! $this->searchTagAndHashTag($searchtags[$i], $linktags, $link['description']) 354 if(count($descTags[1])){
298 ) { 355 // there were some tags in the description, add them to the search string
299 $found = false; 356 $search .= ' ' . implode(' ', $descTags[1]);
300 } 357 }
358 };
359 // match regular expression with search string
360 if(!preg_match($re, $search)){
361 // this entry does _not_ match our regex
362 continue;
301 } 363 }
302 364 $filtered[$key] = $link;
303 if ($found) {
304 $filtered[$key] = $link;
305 }
306 } 365 }
307 return $filtered; 366 return $filtered;
308 } 367 }
@@ -364,28 +423,6 @@ class LinkFilter
364 } 423 }
365 424
366 /** 425 /**
367 * Check if a tag is found in the taglist, or as an hashtag in the link description.
368 *
369 * @param string $tag Tag to search.
370 * @param array $taglist List of tags for the current link.
371 * @param string $description Link description.
372 *
373 * @return bool True if found, false otherwise.
374 */
375 protected function searchTagAndHashTag($tag, $taglist, $description)
376 {
377 if (in_array($tag, $taglist)) {
378 return true;
379 }
380
381 if (preg_match('/(^| )#'. $tag .'([^'. self::$HASHTAG_CHARS .']|$)/mui', $description) > 0) {
382 return true;
383 }
384
385 return false;
386 }
387
388 /**
389 * Convert a list of tags (str) to an array. Also 426 * Convert a list of tags (str) to an array. Also
390 * - handle case sensitivity. 427 * - handle case sensitivity.
391 * - accepts spaces commas as separator. 428 * - accepts spaces commas as separator.
@@ -407,5 +444,11 @@ class LinkFilter
407 444
408class LinkNotFoundException extends Exception 445class LinkNotFoundException extends Exception
409{ 446{
410 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 }
411} 454}
diff --git a/application/LinkUtils.php b/application/LinkUtils.php
index 976474de..267e62cd 100644
--- a/application/LinkUtils.php
+++ b/application/LinkUtils.php
@@ -109,7 +109,7 @@ function count_private($links)
109 */ 109 */
110function text2clickable($text, $redirector = '') 110function text2clickable($text, $redirector = '')
111{ 111{
112 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[[:alnum:]]/?)!si'; 112 $regex = '!(((?:https?|ftp|file)://|apt:|magnet:)\S+[a-z0-9\(\)]/?)!si';
113 113
114 if (empty($redirector)) { 114 if (empty($redirector)) {
115 return preg_replace($regex, '<a href="$1">$1</a>', $text); 115 return preg_replace($regex, '<a href="$1">$1</a>', $text);
diff --git a/application/NetscapeBookmarkUtils.php b/application/NetscapeBookmarkUtils.php
index 2a10ff22..dd7057f8 100644
--- a/application/NetscapeBookmarkUtils.php
+++ b/application/NetscapeBookmarkUtils.php
@@ -32,11 +32,10 @@ class NetscapeBookmarkUtils
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..3aa4ddfc
--- /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 (reference)
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 0702158a..bc859536 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) {
@@ -490,7 +490,7 @@ class UpdaterException extends Exception
490 } 490 }
491 491
492 if (! empty($this->method)) { 492 if (! empty($this->method)) {
493 $out .= 'An error occurred while running the update '. $this->method . PHP_EOL; 493 $out .= t('An error occurred while running the update ') . $this->method . PHP_EOL;
494 } 494 }
495 495
496 if (! empty($this->previous)) { 496 if (! empty($this->previous)) {
@@ -530,11 +530,11 @@ function read_updates_file($updatesFilepath)
530function write_updates_file($updatesFilepath, $updates) 530function write_updates_file($updatesFilepath, $updates)
531{ 531{
532 if (empty($updatesFilepath)) { 532 if (empty($updatesFilepath)) {
533 throw new Exception('Updates file path is not set, can\'t write updates.'); 533 throw new Exception(t('Updates file path is not set, can\'t write updates.'));
534 } 534 }
535 535
536 $res = file_put_contents($updatesFilepath, implode(';', $updates)); 536 $res = file_put_contents($updatesFilepath, implode(';', $updates));
537 if ($res === false) { 537 if ($res === false) {
538 throw new Exception('Unable to write updates in '. $updatesFilepath . '.'); 538 throw new Exception(t('Unable to write updates in '. $updatesFilepath . '.'));
539 } 539 }
540} 540}
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 fdd5b3d7..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');
@@ -327,6 +328,7 @@ class ConfigManager
327 328
328 $this->setEmpty('privacy.default_private_links', false); 329 $this->setEmpty('privacy.default_private_links', false);
329 $this->setEmpty('privacy.hide_public_links', false); 330 $this->setEmpty('privacy.hide_public_links', false);
331 $this->setEmpty('privacy.force_login', false);
330 $this->setEmpty('privacy.hide_timestamps', false); 332 $this->setEmpty('privacy.hide_timestamps', false);
331 // default state of the 'remember me' checkbox of the login form 333 // default state of the 'remember me' checkbox of the login form
332 $this->setEmpty('privacy.remember_user_default', true); 334 $this->setEmpty('privacy.remember_user_default', true);
@@ -337,6 +339,10 @@ class ConfigManager
337 $this->setEmpty('redirector.url', ''); 339 $this->setEmpty('redirector.url', '');
338 $this->setEmpty('redirector.encode_url', true); 340 $this->setEmpty('redirector.encode_url', true);
339 341
342 $this->setEmpty('translation.language', 'auto');
343 $this->setEmpty('translation.mode', 'php');
344 $this->setEmpty('translation.extensions', []);
345
340 $this->setEmpty('plugins', array()); 346 $this->setEmpty('plugins', array());
341 } 347 }
342 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}