diff options
Diffstat (limited to 'application')
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 @@ | |||
13 | function purgeCachedPages($pageCacheDir) | 13 | function 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>— '. $permalink; | 156 | $link['description'] .= PHP_EOL .'<br>— '. $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 | ||
3 | namespace Shaarli; | ||
4 | |||
5 | use Gettext\GettextTranslator; | ||
6 | use Gettext\Merge; | ||
7 | use Gettext\Translations; | ||
8 | use Gettext\Translator; | ||
9 | use Gettext\TranslatorInterface; | ||
10 | use 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 | */ |
15 | function t($text, $nText = '', $nb = 0) { | 40 | class 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 | ||
255 | To learn how to use Shaarli, consult the link "Help/documentation" at the bottom of this page. | 255 | To learn how to use Shaarli, consult the link "Documentation" at the bottom of this page. |
256 | 256 | ||
257 | You use the community supported version of the original Shaarli project, by Sebastien Sauvage.', | 257 | You 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 | ||
408 | class LinkNotFoundException extends Exception | 445 | class 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 | */ |
110 | function text2clickable($text, $redirector = '') | 110 | function 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 | ||
2 | namespace Shaarli; | ||
3 | |||
4 | /** | ||
5 | * Manages the server-side session | ||
6 | */ | ||
7 | class 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) | |||
530 | function write_updates_file($updatesFilepath, $updates) | 530 | function 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 | */ | ||
197 | function 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 | */ |
453 | function alphabetical_sort(&$data, $reverse = false, $byKeys = false) | 423 | function 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 | */ | ||
455 | function 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 | } |