diff options
author | ArthurHoaro <arthur@hoa.ro> | 2018-07-28 11:07:55 +0200 |
---|---|---|
committer | ArthurHoaro <arthur@hoa.ro> | 2018-07-28 11:07:55 +0200 |
commit | 83faedadff76c5bdca036f39f13943f63b27e164 (patch) | |
tree | 6f44cede16ec6a60f10b9699e211e0818f06d2c8 /application/Utils.php | |
parent | 1d9eb22a3df85b67fe6652c0876cd7382c2fb525 (diff) | |
parent | 658988f3aeba7a5a938783249ccf2765251e5597 (diff) | |
download | Shaarli-83faedadff76c5bdca036f39f13943f63b27e164.tar.gz Shaarli-83faedadff76c5bdca036f39f13943f63b27e164.tar.zst Shaarli-83faedadff76c5bdca036f39f13943f63b27e164.zip |
Merge tag 'v0.9.7' into stable
Release v0.9.7
Diffstat (limited to 'application/Utils.php')
-rw-r--r-- | application/Utils.php | 284 |
1 files changed, 254 insertions, 30 deletions
diff --git a/application/Utils.php b/application/Utils.php index 0a5b476e..97b12fcf 100644 --- a/application/Utils.php +++ b/application/Utils.php | |||
@@ -91,6 +91,10 @@ function endsWith($haystack, $needle, $case = true) | |||
91 | */ | 91 | */ |
92 | function escape($input) | 92 | function escape($input) |
93 | { | 93 | { |
94 | if (is_bool($input)) { | ||
95 | return $input; | ||
96 | } | ||
97 | |||
94 | if (is_array($input)) { | 98 | if (is_array($input)) { |
95 | $out = array(); | 99 | $out = array(); |
96 | foreach($input as $key => $value) { | 100 | foreach($input as $key => $value) { |
@@ -178,56 +182,276 @@ function generateLocation($referer, $host, $loopTerms = array()) | |||
178 | } | 182 | } |
179 | 183 | ||
180 | /** | 184 | /** |
181 | * Validate session ID to prevent Full Path Disclosure. | 185 | * Sniff browser language to set the locale automatically. |
186 | * Note that is may not work on your server if the corresponding locale is not installed. | ||
187 | * | ||
188 | * @param string $headerLocale Locale send in HTTP headers (e.g. "fr,fr-fr;q=0.8,en;q=0.5,en-us;q=0.3"). | ||
189 | **/ | ||
190 | function autoLocale($headerLocale) | ||
191 | { | ||
192 | // Default if browser does not send HTTP_ACCEPT_LANGUAGE | ||
193 | $locales = array('en_US', 'en_US.utf8', 'en_US.UTF-8'); | ||
194 | if (! empty($headerLocale)) { | ||
195 | if (preg_match_all('/([a-z]{2,3})[-_]?([a-z]{2})?,?/i', $headerLocale, $matches, PREG_SET_ORDER)) { | ||
196 | $attempts = []; | ||
197 | foreach ($matches as $match) { | ||
198 | $first = [strtolower($match[1]), strtoupper($match[1])]; | ||
199 | $separators = ['_', '-']; | ||
200 | $encodings = ['utf8', 'UTF-8']; | ||
201 | if (!empty($match[2])) { | ||
202 | $second = [strtoupper($match[2]), strtolower($match[2])]; | ||
203 | $items = [$first, $separators, $second, ['.'], $encodings]; | ||
204 | } else { | ||
205 | $items = [$first, $separators, $first, ['.'], $encodings]; | ||
206 | } | ||
207 | $attempts = array_merge($attempts, iterator_to_array(cartesian_product_generator($items))); | ||
208 | } | ||
209 | |||
210 | if (! empty($attempts)) { | ||
211 | $locales = array_merge(array_map('implode', $attempts), $locales); | ||
212 | } | ||
213 | } | ||
214 | } | ||
215 | |||
216 | setlocale(LC_ALL, $locales); | ||
217 | } | ||
218 | |||
219 | /** | ||
220 | * Build a Generator object representing the cartesian product from given $items. | ||
182 | * | 221 | * |
183 | * See #298. | 222 | * Example: |
184 | * The session ID's format depends on the hash algorithm set in PHP settings | 223 | * [['a'], ['b', 'c']] |
224 | * will generate: | ||
225 | * [ | ||
226 | * ['a', 'b'], | ||
227 | * ['a', 'c'], | ||
228 | * ] | ||
185 | * | 229 | * |
186 | * @param string $sessionId Session ID | 230 | * @param array $items array of array of string |
187 | * | 231 | * |
188 | * @return true if valid, false otherwise. | 232 | * @return Generator representing the cartesian product of given array. |
189 | * | 233 | * |
190 | * @see http://php.net/manual/en/function.hash-algos.php | 234 | * @see https://en.wikipedia.org/wiki/Cartesian_product |
191 | * @see http://php.net/manual/en/session.configuration.php | ||
192 | */ | 235 | */ |
193 | function is_session_id_valid($sessionId) | 236 | function cartesian_product_generator($items) |
194 | { | 237 | { |
195 | if (empty($sessionId)) { | 238 | if (empty($items)) { |
239 | yield []; | ||
240 | } | ||
241 | $subArray = array_pop($items); | ||
242 | if (empty($subArray)) { | ||
243 | return; | ||
244 | } | ||
245 | foreach (cartesian_product_generator($items) as $item) { | ||
246 | foreach ($subArray as $value) { | ||
247 | yield $item + [count($item) => $value]; | ||
248 | } | ||
249 | } | ||
250 | } | ||
251 | |||
252 | /** | ||
253 | * Generates a default API secret. | ||
254 | * | ||
255 | * Note that the random-ish methods used in this function are predictable, | ||
256 | * which makes them NOT suitable for crypto. | ||
257 | * BUT the random string is salted with the salt and hashed with the username. | ||
258 | * It makes the generated API secret secured enough for Shaarli. | ||
259 | * | ||
260 | * PHP 7 provides random_int(), designed for cryptography. | ||
261 | * More info: http://stackoverflow.com/questions/4356289/php-random-string-generator | ||
262 | |||
263 | * @param string $username Shaarli login username | ||
264 | * @param string $salt Shaarli password hash salt | ||
265 | * | ||
266 | * @return string|bool Generated API secret, 12 char length. | ||
267 | * Or false if invalid parameters are provided (which will make the API unusable). | ||
268 | */ | ||
269 | function generate_api_secret($username, $salt) | ||
270 | { | ||
271 | if (empty($username) || empty($salt)) { | ||
196 | return false; | 272 | return false; |
197 | } | 273 | } |
198 | 274 | ||
199 | if (!$sessionId) { | 275 | return str_shuffle(substr(hash_hmac('sha512', uniqid($salt), $username), 10, 12)); |
276 | } | ||
277 | |||
278 | /** | ||
279 | * Trim string, replace sequences of whitespaces by a single space. | ||
280 | * PHP equivalent to `normalize-space` XSLT function. | ||
281 | * | ||
282 | * @param string $string Input string. | ||
283 | * | ||
284 | * @return mixed Normalized string. | ||
285 | */ | ||
286 | function normalize_spaces($string) | ||
287 | { | ||
288 | return preg_replace('/\s{2,}/', ' ', trim($string)); | ||
289 | } | ||
290 | |||
291 | /** | ||
292 | * Format the date according to the locale. | ||
293 | * | ||
294 | * Requires php-intl to display international datetimes, | ||
295 | * otherwise default format '%c' will be returned. | ||
296 | * | ||
297 | * @param DateTime $date to format. | ||
298 | * @param bool $time Displays time if true. | ||
299 | * @param bool $intl Use international format if true. | ||
300 | * | ||
301 | * @return bool|string Formatted date, or false if the input is invalid. | ||
302 | */ | ||
303 | function format_date($date, $time = true, $intl = true) | ||
304 | { | ||
305 | if (! $date instanceof DateTime) { | ||
200 | return false; | 306 | return false; |
201 | } | 307 | } |
202 | 308 | ||
203 | if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) { | 309 | if (! $intl || ! class_exists('IntlDateFormatter')) { |
310 | $format = $time ? '%c' : '%x'; | ||
311 | return strftime($format, $date->getTimestamp()); | ||
312 | } | ||
313 | |||
314 | $formatter = new IntlDateFormatter( | ||
315 | setlocale(LC_TIME, 0), | ||
316 | IntlDateFormatter::LONG, | ||
317 | $time ? IntlDateFormatter::LONG : IntlDateFormatter::NONE | ||
318 | ); | ||
319 | |||
320 | return $formatter->format($date); | ||
321 | } | ||
322 | |||
323 | /** | ||
324 | * Check if the input is an integer, no matter its real type. | ||
325 | * | ||
326 | * PHP is a bit messy regarding this: | ||
327 | * - is_int returns false if the input is a string | ||
328 | * - ctype_digit returns false if the input is an integer or negative | ||
329 | * | ||
330 | * @param mixed $input value | ||
331 | * | ||
332 | * @return bool true if the input is an integer, false otherwise | ||
333 | */ | ||
334 | function is_integer_mixed($input) | ||
335 | { | ||
336 | if (is_array($input) || is_bool($input) || is_object($input)) { | ||
204 | return false; | 337 | return false; |
205 | } | 338 | } |
339 | $input = strval($input); | ||
340 | return ctype_digit($input) || (startsWith($input, '-') && ctype_digit(substr($input, 1))); | ||
341 | } | ||
206 | 342 | ||
207 | return true; | 343 | /** |
344 | * Convert post_max_size/upload_max_filesize (e.g. '16M') parameters to bytes. | ||
345 | * | ||
346 | * @param string $val Size expressed in string. | ||
347 | * | ||
348 | * @return int Size expressed in bytes. | ||
349 | */ | ||
350 | function return_bytes($val) | ||
351 | { | ||
352 | if (is_integer_mixed($val) || $val === '0' || empty($val)) { | ||
353 | return $val; | ||
354 | } | ||
355 | $val = trim($val); | ||
356 | $last = strtolower($val[strlen($val)-1]); | ||
357 | $val = intval(substr($val, 0, -1)); | ||
358 | switch($last) { | ||
359 | case 'g': $val *= 1024; | ||
360 | case 'm': $val *= 1024; | ||
361 | case 'k': $val *= 1024; | ||
362 | } | ||
363 | return $val; | ||
208 | } | 364 | } |
209 | 365 | ||
210 | /** | 366 | /** |
211 | * Sniff browser language to set the locale automatically. | 367 | * Return a human readable size from bytes. |
212 | * Note that is may not work on your server if the corresponding locale is not installed. | ||
213 | * | 368 | * |
214 | * @param string $headerLocale Locale send in HTTP headers (e.g. "fr,fr-fr;q=0.8,en;q=0.5,en-us;q=0.3"). | 369 | * @param int $bytes value |
215 | **/ | 370 | * |
216 | function autoLocale($headerLocale) | 371 | * @return string Human readable size |
372 | */ | ||
373 | function human_bytes($bytes) | ||
217 | { | 374 | { |
218 | // Default if browser does not send HTTP_ACCEPT_LANGUAGE | 375 | if ($bytes === '') { |
219 | $attempts = array('en_US'); | 376 | return t('Setting not set'); |
220 | if (isset($headerLocale)) { | 377 | } |
221 | // (It's a bit crude, but it works very well. Preferred language is always presented first.) | 378 | if (! is_integer_mixed($bytes)) { |
222 | if (preg_match('/([a-z]{2})-?([a-z]{2})?/i', $headerLocale, $matches)) { | 379 | return $bytes; |
223 | $loc = $matches[1] . (!empty($matches[2]) ? '_' . strtoupper($matches[2]) : ''); | 380 | } |
224 | $attempts = array( | 381 | $bytes = intval($bytes); |
225 | $loc.'.UTF-8', $loc, str_replace('_', '-', $loc).'.UTF-8', str_replace('_', '-', $loc), | 382 | if ($bytes === 0) { |
226 | $loc . '_' . strtoupper($loc).'.UTF-8', $loc . '_' . strtoupper($loc), | 383 | return t('Unlimited'); |
227 | $loc . '_' . $loc.'.UTF-8', $loc . '_' . $loc, $loc . '-' . strtoupper($loc).'.UTF-8', | 384 | } |
228 | $loc . '-' . strtoupper($loc), $loc . '-' . $loc.'.UTF-8', $loc . '-' . $loc | 385 | |
229 | ); | 386 | $units = [t('B'), t('kiB'), t('MiB'), t('GiB')]; |
387 | for ($i = 0; $i < count($units) && $bytes >= 1024; ++$i) { | ||
388 | $bytes /= 1024; | ||
389 | } | ||
390 | |||
391 | return round($bytes) . $units[$i]; | ||
392 | } | ||
393 | |||
394 | /** | ||
395 | * Try to determine max file size for uploads (POST). | ||
396 | * Returns an integer (in bytes) or formatted depending on $format. | ||
397 | * | ||
398 | * @param mixed $limitPost post_max_size PHP setting | ||
399 | * @param mixed $limitUpload upload_max_filesize PHP setting | ||
400 | * @param bool $format Format max upload size to human readable size | ||
401 | * | ||
402 | * @return int|string max upload file size | ||
403 | */ | ||
404 | function get_max_upload_size($limitPost, $limitUpload, $format = true) | ||
405 | { | ||
406 | $size1 = return_bytes($limitPost); | ||
407 | $size2 = return_bytes($limitUpload); | ||
408 | // Return the smaller of two: | ||
409 | $maxsize = min($size1, $size2); | ||
410 | return $format ? human_bytes($maxsize) : $maxsize; | ||
411 | } | ||
412 | |||
413 | /** | ||
414 | * Sort the given array alphabetically using php-intl if available. | ||
415 | * Case sensitive. | ||
416 | * | ||
417 | * Note: doesn't support multidimensional arrays | ||
418 | * | ||
419 | * @param array $data Input array, passed by reference | ||
420 | * @param bool $reverse Reverse sort if set to true | ||
421 | * @param bool $byKeys Sort the array by keys if set to true, by value otherwise. | ||
422 | */ | ||
423 | function alphabetical_sort(&$data, $reverse = false, $byKeys = false) | ||
424 | { | ||
425 | $callback = function ($a, $b) use ($reverse) { | ||
426 | // Collator is part of PHP intl. | ||
427 | if (class_exists('Collator')) { | ||
428 | $collator = new Collator(setlocale(LC_COLLATE, 0)); | ||
429 | if (!intl_is_failure(intl_get_error_code())) { | ||
430 | return $collator->compare($a, $b) * ($reverse ? -1 : 1); | ||
431 | } | ||
230 | } | 432 | } |
433 | |||
434 | return strcasecmp($a, $b) * ($reverse ? -1 : 1); | ||
435 | }; | ||
436 | |||
437 | if ($byKeys) { | ||
438 | uksort($data, $callback); | ||
439 | } else { | ||
440 | usort($data, $callback); | ||
231 | } | 441 | } |
232 | setlocale(LC_ALL, $attempts); | 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); | ||
233 | } | 457 | } |