+ $refererHost = parse_url($referer, PHP_URL_HOST);
+ if (!empty($referer) && (strpos($refererHost, $host) !== false || startsWith('?', $refererHost))) {
+ $finalReferer = $referer;
+ }
+
+ return $finalReferer;
+}
+
+/**
+ * Validate session ID to prevent Full Path Disclosure.
+ *
+ * See #298.
+ * The session ID's format depends on the hash algorithm set in PHP settings
+ *
+ * @param string $sessionId Session ID
+ *
+ * @return true if valid, false otherwise.
+ *
+ * @see http://php.net/manual/en/function.hash-algos.php
+ * @see http://php.net/manual/en/session.configuration.php
+ */
+function is_session_id_valid($sessionId)
+{
+ if (empty($sessionId)) {
+ return false;
+ }
+
+ if (!$sessionId) {
+ return false;
+ }
+
+ if (!preg_match('/^[a-zA-Z0-9,-]{2,128}$/', $sessionId)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Sniff browser language to set the locale automatically.
+ * Note that is may not work on your server if the corresponding locale is not installed.
+ *
+ * @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").
+ **/
+function autoLocale($headerLocale)
+{
+ // Default if browser does not send HTTP_ACCEPT_LANGUAGE
+ $locales = array('en_US', 'en_US.utf8', 'en_US.UTF-8');
+ if (! empty($headerLocale)) {
+ if (preg_match_all('/([a-z]{2,3})[-_]?([a-z]{2})?,?/i', $headerLocale, $matches, PREG_SET_ORDER)) {
+ $attempts = [];
+ foreach ($matches as $match) {
+ $first = [strtolower($match[1]), strtoupper($match[1])];
+ $separators = ['_', '-'];
+ $encodings = ['utf8', 'UTF-8'];
+ if (!empty($match[2])) {
+ $second = [strtoupper($match[2]), strtolower($match[2])];
+ $items = [$first, $separators, $second, ['.'], $encodings];
+ } else {
+ $items = [$first, $separators, $first, ['.'], $encodings];
+ }
+ $attempts = array_merge($attempts, iterator_to_array(cartesian_product_generator($items)));
+ }
+
+ if (! empty($attempts)) {
+ $locales = array_merge(array_map('implode', $attempts), $locales);
+ }
+ }
+ }
+
+ setlocale(LC_ALL, $locales);
+}
+
+/**
+ * Build a Generator object representing the cartesian product from given $items.
+ *
+ * Example:
+ * [['a'], ['b', 'c']]
+ * will generate:
+ * [
+ * ['a', 'b'],
+ * ['a', 'c'],
+ * ]
+ *
+ * @param array $items array of array of string
+ *
+ * @return Generator representing the cartesian product of given array.
+ *
+ * @see https://en.wikipedia.org/wiki/Cartesian_product
+ */
+function cartesian_product_generator($items)
+{
+ if (empty($items)) {
+ yield [];
+ }
+ $subArray = array_pop($items);
+ if (empty($subArray)) {
+ return;
+ }
+ foreach (cartesian_product_generator($items) as $item) {
+ foreach ($subArray as $value) {
+ yield $item + [count($item) => $value];
+ }
+ }
+}
+
+/**
+ * Generates a default API secret.
+ *
+ * Note that the random-ish methods used in this function are predictable,
+ * which makes them NOT suitable for crypto.
+ * BUT the random string is salted with the salt and hashed with the username.
+ * It makes the generated API secret secured enough for Shaarli.
+ *
+ * PHP 7 provides random_int(), designed for cryptography.
+ * More info: http://stackoverflow.com/questions/4356289/php-random-string-generator
+
+ * @param string $username Shaarli login username
+ * @param string $salt Shaarli password hash salt
+ *
+ * @return string|bool Generated API secret, 12 char length.
+ * Or false if invalid parameters are provided (which will make the API unusable).
+ */
+function generate_api_secret($username, $salt)
+{
+ if (empty($username) || empty($salt)) {
+ return false;
+ }
+
+ return str_shuffle(substr(hash_hmac('sha512', uniqid($salt), $username), 10, 12));
+}
+
+/**
+ * Trim string, replace sequences of whitespaces by a single space.
+ * PHP equivalent to `normalize-space` XSLT function.
+ *
+ * @param string $string Input string.
+ *
+ * @return mixed Normalized string.
+ */
+function normalize_spaces($string)
+{
+ return preg_replace('/\s{2,}/', ' ', trim($string));
+}
+
+/**
+ * Format the date according to the locale.
+ *
+ * Requires php-intl to display international datetimes,
+ * otherwise default format '%c' will be returned.
+ *
+ * @param DateTime $date to format.
+ * @param bool $time Displays time if true.
+ * @param bool $intl Use international format if true.
+ *
+ * @return bool|string Formatted date, or false if the input is invalid.
+ */
+function format_date($date, $time = true, $intl = true)
+{
+ if (! $date instanceof DateTime) {
+ return false;
+ }
+
+ if (! $intl || ! class_exists('IntlDateFormatter')) {
+ $format = $time ? '%c' : '%x';
+ return strftime($format, $date->getTimestamp());