*/ function get_translations(): array { return [ 'auto' => __('Detect automatically'), 'ar_SA' => 'العربيّة (Arabic)', 'be' => 'Беларуская', 'bg_BG' => 'Bulgarian', 'da_DA' => 'Dansk', 'ca_CA' => 'Català', 'cs_CZ' => 'Česky', 'en_US' => 'English', 'el_GR' => 'Ελληνικά', 'es' => 'Español (España)', 'de_DE' => 'Deutsch', 'fa' => 'Persian (Farsi)', 'fr_FR' => 'Français', 'gl' => 'Galego', 'hu_HU' => 'Magyar (Hungarian)', 'it_IT' => 'Italiano', 'ja_JP' => '日本語 (Japanese)', 'lv_LV' => 'Latviešu', 'nb_NO' => 'Norwegian bokmål', 'nl_NL' => 'Dutch', 'pl_PL' => 'Polski', 'ru_RU' => 'Русский', 'pt_BR' => 'Portuguese/Brazil', 'pt_PT' => 'Portuguese/Portugal', 'zh_CN' => 'Simplified Chinese', 'zh_TW' => 'Traditional Chinese', 'uk_UA' => 'Українська', 'sv_SE' => 'Svenska', 'fi_FI' => 'Suomi', 'ta' => 'Tamil', 'tr_TR' => 'Türkçe', ]; } /** * Attempt to initialize translations using the locale specified by the user (via preference) * or detected using 'Accept-Language' request header. */ function startup_gettext(): void { $selected_locale = ''; if (!empty($_SESSION['uid'])) { $pref_locale = Prefs::get(Prefs::USER_LANGUAGE, $_SESSION['uid'], $_SESSION['profile'] ?? null); if (!empty($pref_locale) && $pref_locale != 'auto') $selected_locale = $pref_locale; } // https://www.codingwithjesse.com/blog/use-accept-language-header/ if (!$selected_locale && !empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { $valid_langs = []; $translations = array_keys(get_translations()); array_shift($translations); // remove "auto" // full locale first foreach ($translations as $t) { $lang = strtolower(str_replace("_", "-", (string)$t)); $valid_langs[$lang] = $t; $lang = substr($lang, 0, 2); $valid_langs[$lang] ??= $t; } // break up string into pieces (languages and q factors) preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i', $_SERVER['HTTP_ACCEPT_LANGUAGE'], $lang_parse); if (count($lang_parse[1])) { // create a list like "en" => 0.8 $langs = array_combine($lang_parse[1], $lang_parse[4]); // set default to 1 for any without q factor foreach ($langs as $lang => $val) { if ($val === '') $langs[$lang] = 1; } // sort list based on value arsort($langs, SORT_NUMERIC); foreach (array_keys($langs) as $lang) { $lang = strtolower($lang); foreach ($valid_langs as $vlang => $vlocale) { if ($vlang == $lang) { $selected_locale = $vlocale; break 2; } } } } } if ($selected_locale) { if (defined('LC_MESSAGES')) { _setlocale(LC_MESSAGES, $selected_locale); } else if (defined('LC_ALL')) { _setlocale(LC_ALL, $selected_locale); } _bindtextdomain("messages", "locale"); _textdomain("messages"); _bind_textdomain_codeset("messages", "UTF-8"); } } /* compat shims */ /** * @deprecated by Config::get_version() * * @return array|string */ function get_version(): array|string { return Config::get_version(); } /** @deprecated by Config::get_schema_version() */ function get_schema_version(): int { return Config::get_schema_version(); } /** @deprecated by Debug::log() */ function _debug(string $msg): void { Debug::log($msg); } /** @deprecated by Feeds::_get_counters() * @param int|string $feed feed id or tag name * @param bool $is_cat * @return int * @throws PDOException */ function getFeedUnread(int|string $feed, bool $is_cat = false): int { return Feeds::_get_counters($feed, $is_cat, true, $_SESSION["uid"]); } /** * @deprecated by Sanitizer::sanitize() * * @param array|null $highlight_words Words to highlight in the HTML output. * * @return false|string The HTML, or false if an error occurred. */ function sanitize(string $str, bool $force_remove_images = false, ?int $owner = null, ?string $site_url = null, ?array $highlight_words = null, ?int $article_id = null): false|string { return Sanitizer::sanitize($str, $force_remove_images, $owner, $site_url, $highlight_words, $article_id); } /** * @deprecated by UrlHelper::fetch() * * @param array|string $params * @return false|string false if something went wrong, otherwise string contents */ function fetch_file_contents(array|string $params): false|string { return UrlHelper::fetch($params); } /** * @deprecated by UrlHelper::rewrite_relative() * * Converts a (possibly) relative URL to a absolute one, using provided base URL. * Provides some exceptions for additional schemes like data: if called with owning element/attribute. * * @param string $base_url Base URL (i.e. from where the document is) * @param string $rel_url Possibly relative URL in the document * * @return string Absolute URL */ function rewrite_relative_url($base_url, $rel_url) { return UrlHelper::rewrite_relative($base_url, $rel_url); } /** * @deprecated by UrlHelper::validate() * * @return false|string false if something went wrong, otherwise the URL string */ function validate_url(string $url): false|string { return UrlHelper::validate($url); } /** @deprecated by UserHelper::authenticate() */ function authenticate_user(?string $login = null, ?string $password = null, bool $check_only = false, ?string $service = null): bool { return UserHelper::authenticate($login, $password, $check_only, $service); } /** @deprecated by TimeHelper::smart_date_time() */ function smart_date_time(int $timestamp, int $tz_offset = 0, ?int $owner_uid = null, bool $eta_min = false): string { return TimeHelper::smart_date_time($timestamp, $tz_offset, $owner_uid, $eta_min); } /** @deprecated by TimeHelper::make_local_datetime() */ function make_local_datetime(string $timestamp, bool $long = false, ?int $owner_uid = null, bool $no_smart_dt = false, bool $eta_min = false): string { return TimeHelper::make_local_datetime($timestamp, $long, $owner_uid, $no_smart_dt, $eta_min); } // this returns Config::SELF_URL_PATH sans ending slash /** @deprecated by Config::get_self_url() */ function get_self_url_prefix(): string { return Config::get_self_url(); } /* end compat shims */ /** * Strip tags from and trim a string or array of strings. * * This is used for user HTTP parameters unless HTML code is actually needed. * * @template T * @param T $param * @return (T is array ? array : (T is string ? string : T)) */ function clean(mixed $param): mixed { $filter = static fn($v) => is_string($v) ? trim(strip_tags($v)) : $v; return match (true) { is_array($param) => array_map($filter, $param), is_string($param) => $filter($param), default => $param, }; } function with_trailing_slash(string $str) : string { return str_ends_with($str, '/') ? $str : "$str/"; } function make_password(int $length = 12): string { $password = ""; $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ*%+^"; $i = 0; while ($i < $length) { try { $idx = function_exists("random_int") ? random_int(0, strlen($possible) - 1) : mt_rand(0, strlen($possible) - 1); } catch (Exception) { $idx = mt_rand(0, strlen($possible) - 1); } $char = substr($possible, $idx, 1); if (!strstr($password, $char)) { $password .= $char; $i++; } } return $password; } function validate_csrf(?string $csrf_token): bool { return isset($csrf_token) && hash_equals($_SESSION['csrf_token'] ?? "", $csrf_token); } /** * @param string $str The string to potentially truncate * @param int $max_len The maximum allowed string length * @param string $suffix The suffix to append if the string was truncated * @return string The potentially-truncated string */ function truncate_string(string $str, int $max_len, string $suffix = '…'): string { if (mb_strlen($str, "utf-8") > $max_len) { return mb_substr($str, 0, $max_len, "utf-8") . $suffix; } else { return $str; } } /** * Multibyte-safe substring replacement * * @param string $original The original string * @param string $replacement The string used to replace the selected substring * @param int $position The starting position of the substring to replace * @param int $length The length of the substring to replace * @return string */ function mb_substr_replace(string $original, string $replacement, int $position, int $length): string { $startString = mb_substr($original, 0, $position, "UTF-8"); $endString = mb_substr($original, $position + $length, mb_strlen($original), "UTF-8"); $out = $startString . $replacement . $endString; return $out; } /** * Truncate from the middle of a string if it exceeds a max length * * @param string $str The string to potentially truncate * @param int $max_len The maximum allowed length of the string * @param string $suffix The suffix to append if the string was truncated * @return string */ function truncate_middle(string $str, int $max_len, string $suffix = '…'): string { if (mb_strlen($str) > $max_len) { return mb_substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len); } else { return $str; } } /** * Convert values accepted by tt-rss as true/false to PHP booleans. * * @see https://tt-rss.org/docs/API-Reference.html#boolean-values * @param null|string $s null values are considered false */ function sql_bool_to_bool(?string $s): bool { return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer } /** * workaround for PDO casting all query parameters to string unless type is specified explicitly, * which breaks booleans having false value because they become empty string literals ("") causing * DB type mismatches and breaking SQL queries */ function bool_to_sql_bool(bool $s): int { return (int)$s; } /** * Check if a lock file is locked * * @param string $filename The lock file to check * @return bool Whether the lock file is locked */ function file_is_locked(string $filename): bool { if (file_exists(Config::get(Config::LOCK_DIRECTORY) . "/$filename")) { $fp = @fopen(Config::get(Config::LOCK_DIRECTORY) . "/$filename", "r"); if ($fp) { if (flock($fp, LOCK_EX | LOCK_NB)) { flock($fp, LOCK_UN); fclose($fp); return false; } fclose($fp); return true; } else { return false; } } else { return false; } } /** * Create a lock file. * * @return resource|false A file pointer resource on success, or false on error. */ function make_lockfile(string $filename) { $fp = fopen(Config::get(Config::LOCK_DIRECTORY) . "/$filename", "w"); if ($fp && flock($fp, LOCK_EX | LOCK_NB)) { $stat_h = fstat($fp); $stat_f = stat(Config::get(Config::LOCK_DIRECTORY) . "/$filename"); if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { if ($stat_h["ino"] != $stat_f["ino"] || $stat_h["dev"] != $stat_f["dev"]) { return false; } } if (function_exists('posix_getpid')) { fwrite($fp, posix_getpid() . "\n"); } return $fp; } else { return false; } } /** * checkbox-specific workaround for PDO casting all query parameters to string unless type is * specified explicitly, which breaks booleans having false value because they become empty * string literals ("") causing DB type mismatches and breaking SQL queries * @param mixed $val */ function checkbox_to_sql_bool($val): int { return ($val === "on") ? 1 : 0; } function uniqid_short(): string { return uniqid(base_convert((string)random_int(0, mt_getrandmax()), 10, 36)); } /** * Translation-aware sprintf. * * @param string $format The format string to translate * @param mixed ...$args Optional values to use in the string * @see https://www.php.net/manual/function.sprintf.php */ function T_sprintf(string $format): string { $args = func_get_args(); array_shift($args); return vsprintf(__($format), $args); } /** * Translation-aware sprintf with message pluralization. * @see T_sprintf() */ function T_nsprintf(string $singular_format, string $plural_format, int $number): string { $args = func_get_args(); array_splice($args, 0, 3); return vsprintf(_ngettext($singular_format, $plural_format, $number), $args); } function init_plugins(): bool { PluginHost::getInstance()->load(Config::get(Config::PLUGINS), PluginHost::KIND_ALL); return true; } if (!function_exists('gzdecode')) { /** * @return false|string The decoded string or false if an error occurred. */ function gzdecode(string $string): false|string { // no support for 2nd argument return file_get_contents('compress.zlib://data:who/cares;base64,'. base64_encode($string)); } } function get_random_bytes(int $length): string { if (function_exists('random_bytes')) { return random_bytes($length); } else if (function_exists('openssl_random_pseudo_bytes')) { return openssl_random_pseudo_bytes($length); } else { $output = ""; for ($i = 0; $i < $length; $i++) $output .= chr(mt_rand(0, 255)); return $output; } } function read_stdin(): ?string { $fp = fopen("php://stdin", "r"); if ($fp) { $line = trim(fgets($fp)); fclose($fp); return $line; } return null; } function implements_interface(object|string $class, string $interface): bool { $class_implemented_interfaces = class_implements($class); if ($class_implemented_interfaces) { return in_array($interface, $class_implemented_interfaces); } return false; } function get_theme_path(string $theme, string $default = ""): string { $check = "themes/$theme"; if (file_exists($check)) return $check; $check = "themes.local/$theme"; if (file_exists($check)) return $check; return $default; } function theme_exists(string $theme): bool { return file_exists("themes/$theme") || file_exists("themes.local/$theme"); } /** * @param array $arr */ function arr_qmarks(array $arr): string { return str_repeat('?,', count($arr) - 1) . '?'; } function get_scripts_timestamp(): int { $files = glob("js/*.js"); $ts = 0; foreach ($files as $file) { $file_ts = filemtime($file); if ($file_ts > $ts) $ts = $file_ts; } return $ts; }