2 define('EXPECTED_CONFIG_VERSION', 26);
3 define('SCHEMA_VERSION', 133);
5 define('LABEL_BASE_INDEX', -1024);
6 define('PLUGIN_FEED_BASE_INDEX', -128);
8 define('COOKIE_LIFETIME_LONG', 86400*365);
10 $fetch_last_error = false;
11 $fetch_last_error_code = false;
12 $fetch_last_content_type = false;
13 $fetch_last_error_content = false; // curl only for the time being
14 $fetch_curl_used = false;
15 $suppress_debugging = false;
17 libxml_disable_entity_loader(true);
19 // separate test because this is included before sanity checks
20 if (function_exists("mb_internal_encoding")) mb_internal_encoding("UTF-8");
22 date_default_timezone_set('UTC');
23 if (defined('E_DEPRECATED')) {
24 error_reporting(E_ALL
& ~E_NOTICE
& ~E_DEPRECATED
);
26 error_reporting(E_ALL
& ~E_NOTICE
);
29 require_once 'config.php';
32 * Define a constant if not already defined
34 function define_default($name, $value) {
35 defined($name) or define($name, $value);
38 /* Some tunables you can override in config.php using define(): */
40 define_default('FEED_FETCH_TIMEOUT', 45);
41 // How may seconds to wait for response when requesting feed from a site
42 define_default('FEED_FETCH_NO_CACHE_TIMEOUT', 15);
43 // How may seconds to wait for response when requesting feed from a
44 // site when that feed wasn't cached before
45 define_default('FILE_FETCH_TIMEOUT', 45);
46 // Default timeout when fetching files from remote sites
47 define_default('FILE_FETCH_CONNECT_TIMEOUT', 15);
48 // How many seconds to wait for initial response from website when
49 // fetching files from remote sites
50 define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
51 // stop updating feeds if users haven't logged in for X days
52 define_default('DAEMON_FEED_LIMIT', 500);
53 // feed limit for one update batch
54 define_default('DAEMON_SLEEP_INTERVAL', 120);
55 // default sleep interval between feed updates (sec)
56 define_default('MIN_CACHE_FILE_SIZE', 1024);
57 // do not cache files smaller than that (bytes)
58 define_default('CACHE_MAX_DAYS', 7);
59 // max age in days for various automatically cached (temporary) files
60 define_default('MAX_CONDITIONAL_INTERVAL', 3600*12);
61 // max interval between forced unconditional updates for servers
62 // not complying with http if-modified-since (seconds)
64 /* tunables end here */
66 if (DB_TYPE
== "pgsql") {
67 define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
69 define('SUBSTRING_FOR_DATE', 'SUBSTRING');
73 * Return available translations names.
76 * @return array A array of available translations.
78 function get_translations() {
80 "auto" => "Detect automatically",
81 "ar_SA" => "العربيّة (Arabic)",
82 "bg_BG" => "Bulgarian",
87 "el_GR" => "Ελληνικά",
88 "es_ES" => "Español (España)",
91 "fr_FR" => "Français",
92 "hu_HU" => "Magyar (Hungarian)",
93 "it_IT" => "Italiano",
94 "ja_JP" => "日本語 (Japanese)",
95 "lv_LV" => "Latviešu",
96 "nb_NO" => "Norwegian bokmål",
100 "pt_BR" => "Portuguese/Brazil",
101 "pt_PT" => "Portuguese/Portugal",
102 "zh_CN" => "Simplified Chinese",
103 "zh_TW" => "Traditional Chinese",
104 "sv_SE" => "Svenska",
106 "tr_TR" => "Türkçe");
111 require_once "lib/accept-to-gettext.php";
112 require_once "lib/gettext/gettext.inc";
114 function startup_gettext() {
116 # Get locale from Accept-Language header
117 $lang = al2gt(array_keys(get_translations()), "text/html");
119 if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
120 $lang = _TRANSLATION_OVERRIDE_DEFAULT
;
123 if ($_SESSION["uid"] && get_schema_version() >= 120) {
124 $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
126 if ($pref_lang && $pref_lang != 'auto') {
132 if (defined('LC_MESSAGES')) {
133 _setlocale(LC_MESSAGES
, $lang);
134 } else if (defined('LC_ALL')) {
135 _setlocale(LC_ALL
, $lang);
138 _bindtextdomain("messages", "locale");
140 _textdomain("messages");
141 _bind_textdomain_codeset("messages", "UTF-8");
145 require_once 'db-prefs.php';
146 require_once 'version.php';
147 require_once 'controls.php';
149 define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION
. ' (http://tt-rss.org/)');
150 ini_set('user_agent', SELF_USER_AGENT
);
152 $schema_version = false;
154 function _debug_suppress($suppress) {
155 global $suppress_debugging;
157 $suppress_debugging = $suppress;
161 * Print a timestamped debug message.
163 * @param string $msg The debug message.
166 function _debug($msg, $show = true) {
167 global $suppress_debugging;
169 //echo "[$suppress_debugging] $msg $show\n";
171 if ($suppress_debugging) return false;
173 $ts = strftime("%H:%M:%S", time());
174 if (function_exists('posix_getpid')) {
175 $ts = "$ts/" . posix_getpid();
178 if ($show && !(defined('QUIET') && QUIET
)) {
179 print "[$ts] $msg\n";
182 if (defined('LOGFILE')) {
183 $fp = fopen(LOGFILE
, 'a+');
188 if (function_exists("flock")) {
191 // try to lock logfile for writing
192 while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB
)) {
203 fputs($fp, "[$ts] $msg\n");
205 if (function_exists("flock")) {
216 * Purge a feed old posts.
218 * @param mixed $link A database connection.
219 * @param mixed $feed_id The id of the purged feed.
220 * @param mixed $purge_interval Olderness of purged posts.
221 * @param boolean $debug Set to True to enable the debug. False by default.
225 function purge_feed($feed_id, $purge_interval, $debug = false) {
227 if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
231 $sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
232 $sth->execute([$feed_id]);
236 if ($row = $sth->fetch()) {
237 $owner_uid = $row["owner_uid"];
240 if ($purge_interval == -1 ||
!$purge_interval) {
242 CCache
::update($feed_id, $owner_uid);
247 if (!$owner_uid) return;
249 if (FORCE_ARTICLE_PURGE
== 0) {
250 $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
253 $purge_unread = true;
254 $purge_interval = FORCE_ARTICLE_PURGE
;
258 $query_limit = " unread = false AND ";
262 $purge_interval = (int) $purge_interval;
264 if (DB_TYPE
== "pgsql") {
265 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
267 WHERE ttrss_entries.id = ref_id AND
271 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
272 $sth->execute([$feed_id]);
275 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
276 USING ttrss_user_entries, ttrss_entries
277 WHERE ttrss_entries.id = ref_id AND
281 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
282 $sth->execute([$feed_id]);
286 $rows = $sth->rowCount();
288 CCache
::update($feed_id, $owner_uid);
291 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
295 } // function purge_feed
297 function feed_purge_interval($feed_id) {
301 $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
303 $sth->execute([$feed_id]);
305 if ($row = $sth->fetch()) {
306 $purge_interval = $row["purge_interval"];
307 $owner_uid = $row["owner_uid"];
309 if ($purge_interval == 0) $purge_interval = get_pref(
310 'PURGE_OLD_DAYS', $owner_uid);
312 return $purge_interval;
319 // TODO: multiple-argument way is deprecated, first parameter is a hash now
320 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
321 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
323 global $fetch_last_error;
324 global $fetch_last_error_code;
325 global $fetch_last_error_content;
326 global $fetch_last_content_type;
327 global $fetch_last_modified;
328 global $fetch_curl_used;
330 $fetch_last_error = false;
331 $fetch_last_error_code = -1;
332 $fetch_last_error_content = "";
333 $fetch_last_content_type = "";
334 $fetch_curl_used = false;
335 $fetch_last_modified = "";
337 if (!is_array($options)) {
339 // falling back on compatibility shim
340 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
343 for ($i = 0; $i < func_num_args(); $i++
) {
344 $tmp[$option_names[$i]] = func_get_arg($i);
350 "url" => func_get_arg(0),
351 "type" => @func_get_arg(1),
352 "login" => @func_get_arg(2),
353 "pass" => @func_get_arg(3),
354 "post_query" => @func_get_arg(4),
355 "timeout" => @func_get_arg(5),
356 "timestamp" => @func_get_arg(6),
357 "useragent" => @func_get_arg(7)
361 $url = $options["url"];
362 $type = isset($options["type"]) ?
$options["type"] : false;
363 $login = isset($options["login"]) ?
$options["login"] : false;
364 $pass = isset($options["pass"]) ?
$options["pass"] : false;
365 $post_query = isset($options["post_query"]) ?
$options["post_query"] : false;
366 $timeout = isset($options["timeout"]) ?
$options["timeout"] : false;
367 $last_modified = isset($options["last_modified"]) ?
$options["last_modified"] : "";
368 $useragent = isset($options["useragent"]) ?
$options["useragent"] : false;
369 $followlocation = isset($options["followlocation"]) ?
$options["followlocation"] : true;
371 $url = ltrim($url, ' ');
372 $url = str_replace(' ', '%20', $url);
374 if (strpos($url, "//") === 0)
375 $url = 'http:' . $url;
377 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
379 $fetch_curl_used = true;
381 $ch = curl_init($url);
383 if ($last_modified && !$post_query) {
384 curl_setopt($ch, CURLOPT_HTTPHEADER
,
385 array("If-Modified-Since: $last_modified"));
388 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
389 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
390 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
391 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
392 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
393 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
394 curl_setopt($ch, CURLOPT_HEADER
, true);
395 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
396 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
398 curl_setopt($ch, CURLOPT_ENCODING
, "");
399 //curl_setopt($ch, CURLOPT_REFERER, $url);
401 if (!ini_get("open_basedir")) {
402 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
405 if (defined('_CURL_HTTP_PROXY')) {
406 curl_setopt($ch, CURLOPT_PROXY
, _CURL_HTTP_PROXY
);
410 curl_setopt($ch, CURLOPT_POST
, true);
411 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
415 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
417 $ret = @curl_exec
($ch);
419 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE
);
420 $headers = explode("\r\n", substr($ret, 0, $headers_length));
421 $contents = substr($ret, $headers_length);
423 foreach ($headers as $header) {
424 if (strstr($header, ": ") !== FALSE) {
425 list ($key, $value) = explode(": ", $header);
427 if (strtolower($key) == "last-modified") {
428 $fetch_last_modified = $value;
432 if (substr(strtolower($header), 0, 7) == 'http/1.') {
433 $fetch_last_error_code = (int) substr($header, 9, 3);
434 $fetch_last_error = $header;
438 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
439 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
440 $contents = @curl_exec
($ch);
443 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
444 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
446 $fetch_last_error_code = $http_code;
448 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
450 if (curl_errno($ch) != 0) {
451 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
454 $fetch_last_error_content = $contents;
460 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
470 $fetch_curl_used = false;
472 if ($login && $pass){
473 $url_parts = array();
475 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
477 $pass = urlencode($pass);
479 if ($url_parts[1] && $url_parts[2]) {
480 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
484 // TODO: should this support POST requests or not? idk
486 if (!$post_query && $last_modified) {
487 $context = stream_context_create(array(
490 'ignore_errors' => true,
491 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
492 'protocol_version'=> 1.1,
493 'header' => "If-Modified-Since: $last_modified\r\n")
496 $context = stream_context_create(array(
499 'ignore_errors' => true,
500 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
501 'protocol_version'=> 1.1
505 $old_error = error_get_last();
507 $data = @file_get_contents
($url, false, $context);
509 if (isset($http_response_header) && is_array($http_response_header)) {
510 foreach ($http_response_header as $header) {
511 if (strstr($header, ": ") !== FALSE) {
512 list ($key, $value) = explode(": ", $header);
514 $key = strtolower($key);
516 if ($key == 'content-type') {
517 $fetch_last_content_type = $value;
518 // don't abort here b/c there might be more than one
519 // e.g. if we were being redirected -- last one is the right one
520 } else if ($key == 'last-modified') {
521 $fetch_last_modified = $value;
525 if (substr(strtolower($header), 0, 7) == 'http/1.') {
526 $fetch_last_error_code = (int) substr($header, 9, 3);
527 $fetch_last_error = $header;
532 if ($fetch_last_error_code != 200) {
533 $error = error_get_last();
535 if ($error['message'] != $old_error['message']) {
536 $fetch_last_error .= "; " . $error["message"];
539 $fetch_last_error_content = $data;
549 * Try to determine the favicon URL for a feed.
550 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
551 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
553 * @param string $url A feed or page URL
555 * @return mixed The favicon URL, or false if none was found.
557 function get_favicon_url($url) {
559 $favicon_url = false;
561 if ($html = @fetch_file_contents
($url)) {
563 libxml_use_internal_errors(true);
565 $doc = new DOMDocument();
566 $doc->loadHTML($html);
567 $xpath = new DOMXPath($doc);
569 $base = $xpath->query('/html/head/base[@href]');
570 foreach ($base as $b) {
571 $url = rewrite_relative_url($url, $b->getAttribute("href"));
575 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
576 if (count($entries) > 0) {
577 foreach ($entries as $entry) {
578 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
585 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
588 } // function get_favicon_url
590 function initialize_user_prefs($uid, $profile = false) {
592 if (get_schema_version() < 63) $profile_qpart = "";
595 $in_nested_tr = false;
598 $pdo->beginTransaction();
599 } catch (Exception
$e) {
600 $in_nested_tr = true;
603 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
605 $profile = $profile ?
$profile : null;
607 $u_sth = $pdo->prepare("SELECT pref_name
608 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
609 (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
610 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
612 $active_prefs = array();
614 while ($line = $u_sth->fetch()) {
615 array_push($active_prefs, $line["pref_name"]);
618 while ($line = $sth->fetch()) {
619 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
620 // print "adding " . $line["pref_name"] . "<br>";
622 if (get_schema_version() < 63) {
623 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
624 (owner_uid,pref_name,value) VALUES
626 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
629 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
630 (owner_uid,pref_name,value, profile) VALUES
632 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
638 if (!$in_nested_tr) $pdo->commit();
642 function get_ssl_certificate_id() {
643 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
644 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
645 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
646 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
647 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
649 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
650 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
651 $_SERVER["SSL_CLIENT_V_START"] .
652 $_SERVER["SSL_CLIENT_V_END"] .
653 $_SERVER["SSL_CLIENT_S_DN"]);
658 function authenticate_user($login, $password, $check_only = false) {
660 if (!SINGLE_USER_MODE
) {
663 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
665 $user_id = (int) $plugin->authenticate($login, $password);
668 $_SESSION["auth_module"] = strtolower(get_class($plugin));
673 if ($user_id && !$check_only) {
676 $_SESSION["uid"] = $user_id;
677 $_SESSION["version"] = VERSION_STATIC
;
680 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
682 $sth->execute([$user_id]);
683 $row = $sth->fetch();
685 $_SESSION["name"] = $row["login"];
686 $_SESSION["access_level"] = $row["access_level"];
687 $_SESSION["csrf_token"] = uniqid_short();
689 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
690 $usth->execute([$user_id]);
692 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
693 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
694 $_SESSION["pwd_hash"] = $row["pwd_hash"];
696 $_SESSION["last_version_check"] = time();
698 initialize_user_prefs($_SESSION["uid"]);
707 $_SESSION["uid"] = 1;
708 $_SESSION["name"] = "admin";
709 $_SESSION["access_level"] = 10;
711 $_SESSION["hide_hello"] = true;
712 $_SESSION["hide_logout"] = true;
714 $_SESSION["auth_module"] = false;
716 if (!$_SESSION["csrf_token"]) {
717 $_SESSION["csrf_token"] = uniqid_short();
720 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
722 initialize_user_prefs($_SESSION["uid"]);
728 // this is used for user http parameters unless HTML code is actually needed
729 function clean($param) {
730 if (is_array($param)) {
731 return array_map("strip_tags", $param);
732 } else if (is_string($param)) {
733 return strip_tags($param);
739 function make_password($length = 8) {
742 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
746 while ($i < $length) {
747 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
749 if (!strstr($password, $char)) {
757 // this is called after user is created to initialize default feeds, labels
760 // user preferences are checked on every login, not here
762 function initialize_user($uid) {
766 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
767 values (?, 'Tiny Tiny RSS: Forum',
768 'http://tt-rss.org/forum/rss.php')");
769 $sth->execute([$uid]);
772 function logout_user() {
774 if (isset($_COOKIE[session_name()])) {
775 setcookie(session_name(), '', time()-42000, '/');
779 function validate_csrf($csrf_token) {
780 return $csrf_token == $_SESSION['csrf_token'];
783 function load_user_plugins($owner_uid, $pluginhost = false) {
785 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
787 if ($owner_uid && SCHEMA_VERSION
>= 100) {
788 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
790 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
792 if (get_schema_version() > 100) {
793 $pluginhost->load_data();
798 function login_sequence() {
801 if (SINGLE_USER_MODE
) {
803 authenticate_user("admin", null);
805 load_user_plugins($_SESSION["uid"]);
807 if (!validate_session()) $_SESSION["uid"] = false;
809 if (!$_SESSION["uid"]) {
811 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
812 $_SESSION["ref_schema_version"] = get_schema_version(true);
814 authenticate_user(null, null, true);
817 if (!$_SESSION["uid"]) {
819 setcookie(session_name(), '', time()-42000, '/');
826 /* bump login timestamp */
827 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
828 $sth->execute([$_SESSION['uid']]);
830 $_SESSION["last_login_update"] = time();
833 if ($_SESSION["uid"]) {
835 load_user_plugins($_SESSION["uid"]);
839 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
841 (SELECT COUNT(id) FROM ttrss_feeds WHERE
842 ttrss_feeds.id = feed_id) = 0");
844 $sth->execute([$_SESSION['uid']]);
846 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
848 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
849 ttrss_feed_categories.id = feed_id) = 0");
851 $sth->execute([$_SESSION['uid']]);
857 function truncate_string($str, $max_len, $suffix = '…') {
858 if (mb_strlen($str, "utf-8") > $max_len) {
859 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
866 function truncate_middle($str, $max_len, $suffix = '…') {
867 if (strlen($str) > $max_len) {
868 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
874 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
877 $source_tz = new DateTimeZone($source_tz);
878 } catch (Exception
$e) {
879 $source_tz = new DateTimeZone('UTC');
883 $dest_tz = new DateTimeZone($dest_tz);
884 } catch (Exception
$e) {
885 $dest_tz = new DateTimeZone('UTC');
888 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
889 return $dt->format('U') +
$dest_tz->getOffset($dt);
892 function make_local_datetime($timestamp, $long, $owner_uid = false,
893 $no_smart_dt = false, $eta_min = false) {
895 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
896 if (!$timestamp) $timestamp = '1970-01-01 0:00';
901 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
903 $timestamp = substr($timestamp, 0, 19);
905 # We store date in UTC internally
906 $dt = new DateTime($timestamp, $utc_tz);
908 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
910 if ($user_tz_string != 'Automatic') {
913 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
914 } catch (Exception
$e) {
918 $tz_offset = $user_tz->getOffset($dt);
920 $tz_offset = (int) -$_SESSION["clientTzOffset"];
923 $user_timestamp = $dt->format('U') +
$tz_offset;
926 return smart_date_time($user_timestamp,
927 $tz_offset, $owner_uid, $eta_min);
930 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
932 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
934 return date($format, $user_timestamp);
938 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
939 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
941 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
942 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
943 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
944 return date("G:i", $timestamp);
945 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
946 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
947 return date($format, $timestamp);
949 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
950 return date($format, $timestamp);
954 function sql_bool_to_bool($s) {
955 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
958 function bool_to_sql_bool($s) {
962 // Session caching removed due to causing wrong redirects to upgrade
963 // script when get_schema_version() is called on an obsolete session
964 // created on a previous schema version.
965 function get_schema_version($nocache = false) {
966 global $schema_version;
970 if (!$schema_version && !$nocache) {
971 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
972 $version = $row["schema_version"];
973 $schema_version = $version;
976 return $schema_version;
980 function sanity_check() {
981 require_once 'errors.php';
985 $schema_version = get_schema_version(true);
987 if ($schema_version != SCHEMA_VERSION
) {
991 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
994 function file_is_locked($filename) {
995 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
996 if (function_exists('flock')) {
997 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
999 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1000 flock($fp, LOCK_UN
);
1010 return true; // consider the file always locked and skip the test
1017 function make_lockfile($filename) {
1018 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1020 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1021 $stat_h = fstat($fp);
1022 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1024 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1025 if ($stat_h["ino"] != $stat_f["ino"] ||
1026 $stat_h["dev"] != $stat_f["dev"]) {
1032 if (function_exists('posix_getpid')) {
1033 fwrite($fp, posix_getpid() . "\n");
1041 function make_stampfile($filename) {
1042 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1044 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1045 fwrite($fp, time() . "\n");
1046 flock($fp, LOCK_UN
);
1054 function sql_random_function() {
1055 if (DB_TYPE
== "mysql") {
1062 function getFeedUnread($feed, $is_cat = false) {
1063 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1066 function checkbox_to_sql_bool($val) {
1067 return ($val == "on") ?
1 : 0;
1070 function uniqid_short() {
1071 return uniqid(base_convert(rand(), 10, 36));
1074 function make_init_params() {
1077 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1078 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1079 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1080 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1082 $params[strtolower($param)] = (int) get_pref($param);
1085 $params["icons_url"] = ICONS_URL
;
1086 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1087 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1088 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1089 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1090 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1091 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1092 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1094 $theme = get_pref( "USER_CSS_THEME", false, false);
1095 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1097 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1099 $params["php_platform"] = PHP_OS
;
1100 $params["php_version"] = PHP_VERSION
;
1102 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1106 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1107 ttrss_feeds WHERE owner_uid = ?");
1108 $sth->execute([$_SESSION['uid']]);
1109 $row = $sth->fetch();
1111 $max_feed_id = $row["mid"];
1112 $num_feeds = $row["nf"];
1114 $params["max_feed_id"] = (int) $max_feed_id;
1115 $params["num_feeds"] = (int) $num_feeds;
1117 $params["hotkeys"] = get_hotkeys_map();
1119 $params["csrf_token"] = $_SESSION["csrf_token"];
1120 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1122 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1124 $params["icon_alert"] = base64_img("images/alert.png");
1125 $params["icon_information"] = base64_img("images/information.png");
1126 $params["icon_cross"] = base64_img("images/cross.png");
1127 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1129 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1134 function get_hotkeys_info() {
1136 __("Navigation") => array(
1137 "next_feed" => __("Open next feed"),
1138 "prev_feed" => __("Open previous feed"),
1139 "next_article" => __("Open next article"),
1140 "prev_article" => __("Open previous article"),
1141 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1142 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1143 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1144 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1145 "search_dialog" => __("Show search dialog")),
1146 __("Article") => array(
1147 "toggle_mark" => __("Toggle starred"),
1148 "toggle_publ" => __("Toggle published"),
1149 "toggle_unread" => __("Toggle unread"),
1150 "edit_tags" => __("Edit tags"),
1151 "open_in_new_window" => __("Open in new window"),
1152 "catchup_below" => __("Mark below as read"),
1153 "catchup_above" => __("Mark above as read"),
1154 "article_scroll_down" => __("Scroll down"),
1155 "article_scroll_up" => __("Scroll up"),
1156 "select_article_cursor" => __("Select article under cursor"),
1157 "email_article" => __("Email article"),
1158 "close_article" => __("Close/collapse article"),
1159 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1160 "toggle_widescreen" => __("Toggle widescreen mode"),
1161 "toggle_embed_original" => __("Toggle embed original")),
1162 __("Article selection") => array(
1163 "select_all" => __("Select all articles"),
1164 "select_unread" => __("Select unread"),
1165 "select_marked" => __("Select starred"),
1166 "select_published" => __("Select published"),
1167 "select_invert" => __("Invert selection"),
1168 "select_none" => __("Deselect everything")),
1169 __("Feed") => array(
1170 "feed_refresh" => __("Refresh current feed"),
1171 "feed_unhide_read" => __("Un/hide read feeds"),
1172 "feed_subscribe" => __("Subscribe to feed"),
1173 "feed_edit" => __("Edit feed"),
1174 "feed_catchup" => __("Mark as read"),
1175 "feed_reverse" => __("Reverse headlines"),
1176 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1177 "feed_debug_update" => __("Debug feed update"),
1178 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1179 "catchup_all" => __("Mark all feeds as read"),
1180 "cat_toggle_collapse" => __("Un/collapse current category"),
1181 "toggle_combined_mode" => __("Toggle combined mode"),
1182 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1183 __("Go to") => array(
1184 "goto_all" => __("All articles"),
1185 "goto_fresh" => __("Fresh"),
1186 "goto_marked" => __("Starred"),
1187 "goto_published" => __("Published"),
1188 "goto_tagcloud" => __("Tag cloud"),
1189 "goto_prefs" => __("Preferences")),
1190 __("Other") => array(
1191 "create_label" => __("Create label"),
1192 "create_filter" => __("Create filter"),
1193 "collapse_sidebar" => __("Un/collapse sidebar"),
1194 "help_dialog" => __("Show help dialog"))
1197 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1198 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1204 function get_hotkeys_map() {
1206 // "navigation" => array(
1209 "n" => "next_article",
1210 "p" => "prev_article",
1211 "(38)|up" => "prev_article",
1212 "(40)|down" => "next_article",
1213 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1214 // "^(40)|Ctrl-down" => "next_article_noscroll",
1215 "(191)|/" => "search_dialog",
1216 // "article" => array(
1217 "s" => "toggle_mark",
1218 "*s" => "toggle_publ",
1219 "u" => "toggle_unread",
1220 "*t" => "edit_tags",
1221 "o" => "open_in_new_window",
1222 "c p" => "catchup_below",
1223 "c n" => "catchup_above",
1224 "*n" => "article_scroll_down",
1225 "*p" => "article_scroll_up",
1226 "*(38)|Shift+up" => "article_scroll_up",
1227 "*(40)|Shift+down" => "article_scroll_down",
1228 "a *w" => "toggle_widescreen",
1229 "a e" => "toggle_embed_original",
1230 "e" => "email_article",
1231 "a q" => "close_article",
1232 // "article_selection" => array(
1233 "a a" => "select_all",
1234 "a u" => "select_unread",
1235 "a *u" => "select_marked",
1236 "a p" => "select_published",
1237 "a i" => "select_invert",
1238 "a n" => "select_none",
1240 "f r" => "feed_refresh",
1241 "f a" => "feed_unhide_read",
1242 "f s" => "feed_subscribe",
1243 "f e" => "feed_edit",
1244 "f q" => "feed_catchup",
1245 "f x" => "feed_reverse",
1246 "f g" => "feed_toggle_vgroup",
1247 "f *d" => "feed_debug_update",
1248 "f *g" => "feed_debug_viewfeed",
1249 "f *c" => "toggle_combined_mode",
1250 "f c" => "toggle_cdm_expanded",
1251 "*q" => "catchup_all",
1252 "x" => "cat_toggle_collapse",
1254 "g a" => "goto_all",
1255 "g f" => "goto_fresh",
1256 "g s" => "goto_marked",
1257 "g p" => "goto_published",
1258 "g t" => "goto_tagcloud",
1259 "g *p" => "goto_prefs",
1260 // "other" => array(
1261 "(9)|Tab" => "select_article_cursor", // tab
1262 "c l" => "create_label",
1263 "c f" => "create_filter",
1264 "c s" => "collapse_sidebar",
1265 "^(191)|Ctrl+/" => "help_dialog",
1268 if (get_pref('COMBINED_DISPLAY_MODE')) {
1269 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1270 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1273 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1274 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1277 $prefixes = array();
1279 foreach (array_keys($hotkeys) as $hotkey) {
1280 $pair = explode(" ", $hotkey, 2);
1282 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1283 array_push($prefixes, $pair[0]);
1287 return array($prefixes, $hotkeys);
1290 function check_for_update() {
1291 if (defined("GIT_VERSION_TIMESTAMP")) {
1292 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1295 $content = json_decode($content, true);
1297 if ($content && isset($content["changeset"])) {
1298 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1299 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1301 return $content["changeset"]["id"];
1310 function make_runtime_info($disable_update_check = false) {
1315 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1316 ttrss_feeds WHERE owner_uid = ?");
1317 $sth->execute([$_SESSION['uid']]);
1318 $row = $sth->fetch();
1320 $max_feed_id = $row['mid'];
1321 $num_feeds = $row['nf'];
1323 $data["max_feed_id"] = (int) $max_feed_id;
1324 $data["num_feeds"] = (int) $num_feeds;
1326 $data['last_article_id'] = Article
::getLastArticleId();
1327 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1329 $data['dep_ts'] = calculate_dep_timestamp();
1330 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1332 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1334 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1335 $update_result = @check_for_update
();
1337 $data["update_result"] = $update_result;
1339 $_SESSION["last_version_check"] = time();
1342 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1344 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1346 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1348 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1351 $stamp_delta = time() - $stamp;
1353 if ($stamp_delta > 1800) {
1357 $_SESSION["daemon_stamp_check"] = time();
1360 $data['daemon_stamp_ok'] = $stamp_check;
1362 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1364 $data['daemon_stamp'] = $stamp_fmt;
1372 function search_to_sql($search, $search_language) {
1374 $keywords = str_getcsv(trim($search), " ");
1375 $query_keywords = array();
1376 $search_words = array();
1377 $search_query_leftover = array();
1381 if ($search_language)
1382 $search_language = $pdo->quote(mb_strtolower($search_language));
1384 $search_language = "english";
1386 foreach ($keywords as $k) {
1387 if (strpos($k, "-") === 0) {
1394 $commandpair = explode(":", mb_strtolower($k), 2);
1396 switch ($commandpair[0]) {
1398 if ($commandpair[1]) {
1399 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1400 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1402 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1403 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1404 array_push($search_words, $k);
1408 if ($commandpair[1]) {
1409 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1410 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1412 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1413 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1414 array_push($search_words, $k);
1418 if ($commandpair[1]) {
1419 if ($commandpair[1] == "true")
1420 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1421 else if ($commandpair[1] == "false")
1422 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1424 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1425 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1427 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1428 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1429 if (!$not) array_push($search_words, $k);
1434 if ($commandpair[1]) {
1435 if ($commandpair[1] == "true")
1436 array_push($query_keywords, "($not (marked = true))");
1438 array_push($query_keywords, "($not (marked = false))");
1440 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1441 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1442 if (!$not) array_push($search_words, $k);
1446 if ($commandpair[1]) {
1447 if ($commandpair[1] == "true")
1448 array_push($query_keywords, "($not (published = true))");
1450 array_push($query_keywords, "($not (published = false))");
1453 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1454 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1455 if (!$not) array_push($search_words, $k);
1459 if ($commandpair[1]) {
1460 if ($commandpair[1] == "true")
1461 array_push($query_keywords, "($not (unread = true))");
1463 array_push($query_keywords, "($not (unread = false))");
1466 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1467 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1468 if (!$not) array_push($search_words, $k);
1472 if (strpos($k, "@") === 0) {
1474 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1475 $orig_ts = strtotime(substr($k, 1));
1476 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1478 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1480 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1483 if (DB_TYPE
== "pgsql") {
1484 $k = mb_strtolower($k);
1485 array_push($search_query_leftover, $not ?
"!$k" : $k);
1487 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1488 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1491 if (!$not) array_push($search_words, $k);
1496 if (count($search_query_leftover) > 0) {
1497 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1499 if (DB_TYPE
== "pgsql") {
1500 array_push($query_keywords,
1501 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1506 $search_query_part = implode("AND", $query_keywords);
1508 return array($search_query_part, $search_words);
1511 function iframe_whitelisted($entry) {
1512 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1514 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1517 foreach ($whitelist as $w) {
1518 if ($src == $w ||
$src == "www.$w")
1526 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1527 if (!$owner) $owner = $_SESSION["uid"];
1529 $res = trim($str); if (!$res) return '';
1531 $charset_hack = '<head>
1532 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1535 $res = trim($res); if (!$res) return '';
1537 libxml_use_internal_errors(true);
1539 $doc = new DOMDocument();
1540 $doc->loadHTML($charset_hack . $res);
1541 $xpath = new DOMXPath($doc);
1543 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1545 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1547 foreach ($entries as $entry) {
1549 if ($entry->hasAttribute('href')) {
1550 $entry->setAttribute('href',
1551 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1553 $entry->setAttribute('rel', 'noopener noreferrer');
1556 if ($entry->hasAttribute('src')) {
1557 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1558 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1560 if (file_exists($cached_filename)) {
1562 // this is strictly cosmetic
1563 if ($entry->tagName
== 'img') {
1565 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1567 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1573 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1575 if ($entry->hasAttribute('srcset')) {
1576 $entry->removeAttribute('srcset');
1579 if ($entry->hasAttribute('sizes')) {
1580 $entry->removeAttribute('sizes');
1584 $entry->setAttribute('src', $src);
1587 if ($entry->nodeName
== 'img') {
1588 $entry->setAttribute('referrerpolicy', 'no-referrer');
1590 if ($entry->hasAttribute('src')) {
1591 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1593 if (is_prefix_https() && !$is_https_url) {
1595 if ($entry->hasAttribute('srcset')) {
1596 $entry->removeAttribute('srcset');
1599 if ($entry->hasAttribute('sizes')) {
1600 $entry->removeAttribute('sizes');
1605 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1606 $force_remove_images ||
$_SESSION["bw_limit"]) {
1608 $p = $doc->createElement('p');
1610 $a = $doc->createElement('a');
1611 $a->setAttribute('href', $entry->getAttribute('src'));
1613 $a->appendChild(new DOMText($entry->getAttribute('src')));
1614 $a->setAttribute('target', '_blank');
1615 $a->setAttribute('rel', 'noopener noreferrer');
1617 $p->appendChild($a);
1619 $entry->parentNode
->replaceChild($p, $entry);
1623 if (strtolower($entry->nodeName
) == "a") {
1624 $entry->setAttribute("target", "_blank");
1625 $entry->setAttribute("rel", "noopener noreferrer");
1629 $entries = $xpath->query('//iframe');
1630 foreach ($entries as $entry) {
1631 if (!iframe_whitelisted($entry)) {
1632 $entry->setAttribute('sandbox', 'allow-scripts');
1634 if (is_prefix_https()) {
1635 $entry->setAttribute("src",
1636 str_replace("http://", "https://",
1637 $entry->getAttribute("src")));
1642 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1643 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1644 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1645 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1646 'dt', 'em', 'footer', 'figure', 'figcaption',
1647 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1648 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1649 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1650 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1651 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1652 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1654 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1656 $disallowed_attributes = array('id', 'style', 'class');
1658 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1659 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1660 if (is_array($retval)) {
1662 $allowed_elements = $retval[1];
1663 $disallowed_attributes = $retval[2];
1669 $doc->removeChild($doc->firstChild
); //remove doctype
1670 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1672 if ($highlight_words) {
1673 foreach ($highlight_words as $word) {
1675 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1677 $elements = $xpath->query("//*/text()");
1679 foreach ($elements as $child) {
1681 $fragment = $doc->createDocumentFragment();
1682 $text = $child->textContent
;
1684 while (($pos = mb_stripos($text, $word)) !== false) {
1685 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1686 $word = mb_substr($text, $pos, mb_strlen($word));
1687 $highlight = $doc->createElement('span');
1688 $highlight->appendChild(new DomText($word));
1689 $highlight->setAttribute('class', 'highlight');
1690 $fragment->appendChild($highlight);
1691 $text = mb_substr($text, $pos +
mb_strlen($word));
1694 if (!empty($text)) $fragment->appendChild(new DomText($text));
1696 $child->parentNode
->replaceChild($fragment, $child);
1701 $res = $doc->saveHTML();
1703 /* strip everything outside of <body>...</body> */
1705 $res_frag = array();
1706 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1707 return $res_frag[1];
1713 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1714 $xpath = new DOMXPath($doc);
1715 $entries = $xpath->query('//*');
1717 foreach ($entries as $entry) {
1718 if (!in_array($entry->nodeName
, $allowed_elements)) {
1719 $entry->parentNode
->removeChild($entry);
1722 if ($entry->hasAttributes()) {
1723 $attrs_to_remove = array();
1725 foreach ($entry->attributes
as $attr) {
1727 if (strpos($attr->nodeName
, 'on') === 0) {
1728 array_push($attrs_to_remove, $attr);
1731 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1732 array_push($attrs_to_remove, $attr);
1735 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1736 array_push($attrs_to_remove, $attr);
1740 foreach ($attrs_to_remove as $attr) {
1741 $entry->removeAttributeNode($attr);
1749 function trim_array($array) {
1751 array_walk($tmp, 'trim');
1755 function tag_is_valid($tag) {
1756 if ($tag == '') return false;
1757 if (is_numeric($tag)) return false;
1758 if (mb_strlen($tag) > 250) return false;
1760 if (!$tag) return false;
1765 function render_login_form() {
1766 header('Cache-Control: public');
1768 require_once "login_form.php";
1772 function T_sprintf() {
1773 $args = func_get_args();
1774 return vsprintf(__(array_shift($args)), $args);
1777 function print_checkpoint($n, $s) {
1778 $ts = microtime(true);
1779 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1783 function sanitize_tag($tag) {
1786 $tag = mb_strtolower($tag, 'utf-8');
1788 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1790 if (DB_TYPE
== "mysql") {
1791 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1797 function is_server_https() {
1798 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1801 function is_prefix_https() {
1802 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1805 // this returns SELF_URL_PATH sans ending slash
1806 function get_self_url_prefix() {
1807 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1808 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1810 return SELF_URL_PATH
;
1814 function encrypt_password($pass, $salt = '', $mode2 = false) {
1815 if ($salt && $mode2) {
1816 return "MODE2:" . hash('sha256', $salt . $pass);
1818 return "SHA1X:" . sha1("$salt:$pass");
1820 return "SHA1:" . sha1($pass);
1822 } // function encrypt_password
1824 function load_filters($feed_id, $owner_uid) {
1827 $feed_id = (int) $feed_id;
1828 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1831 $null_cat_qpart = "cat_id IS NULL OR";
1833 $null_cat_qpart = "";
1837 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1838 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1839 $sth->execute([$owner_uid]);
1841 $check_cats = array_merge(
1842 Feeds
::getParentCategories($cat_id, $owner_uid),
1845 $check_cats_str = join(",", $check_cats);
1846 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1848 while ($line = $sth->fetch()) {
1849 $filter_id = $line["id"];
1851 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1853 $sth2 = $pdo->prepare("SELECT
1854 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1855 FROM ttrss_filters2_rules AS r,
1856 ttrss_filter_types AS t
1858 (match_on IS NOT NULL OR
1859 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1860 (feed_id IS NULL OR feed_id = ?))) AND
1861 filter_type = t.id AND filter_id = ?");
1862 $sth2->execute([$feed_id, $filter_id]);
1867 while ($rule_line = $sth2->fetch()) {
1868 # print_r($rule_line);
1870 if ($rule_line["match_on"]) {
1871 $match_on = json_decode($rule_line["match_on"], true);
1873 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1876 $rule["reg_exp"] = $rule_line["reg_exp"];
1877 $rule["type"] = $rule_line["type_name"];
1878 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1880 array_push($rules, $rule);
1881 } else if (!$match_any_rule) {
1882 // this filter contains a rule that doesn't match to this feed/category combination
1883 // thus filter has to be rejected
1892 $rule["reg_exp"] = $rule_line["reg_exp"];
1893 $rule["type"] = $rule_line["type_name"];
1894 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1896 array_push($rules, $rule);
1900 if (count($rules) > 0) {
1901 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1902 FROM ttrss_filters2_actions AS a,
1903 ttrss_filter_actions AS t
1905 action_id = t.id AND filter_id = ?");
1906 $sth2->execute([$filter_id]);
1908 while ($action_line = $sth2->fetch()) {
1909 # print_r($action_line);
1912 $action["type"] = $action_line["type_name"];
1913 $action["param"] = $action_line["action_param"];
1915 array_push($actions, $action);
1920 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1921 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1922 $filter["rules"] = $rules;
1923 $filter["actions"] = $actions;
1925 if (count($rules) > 0 && count($actions) > 0) {
1926 array_push($filters, $filter);
1933 function get_score_pic($score) {
1935 return "score_high.png";
1936 } else if ($score > 0) {
1937 return "score_half_high.png";
1938 } else if ($score < -100) {
1939 return "score_low.png";
1940 } else if ($score < 0) {
1941 return "score_half_low.png";
1943 return "score_neutral.png";
1947 function init_plugins() {
1948 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1953 function add_feed_category($feed_cat, $parent_cat_id = false) {
1955 if (!$feed_cat) return false;
1957 $feed_cat = mb_substr($feed_cat, 0, 250);
1958 if (!$parent_cat_id) $parent_cat_id = null;
1961 $tr_in_progress = false;
1964 $pdo->beginTransaction();
1965 } catch (Exception
$e) {
1966 $tr_in_progress = true;
1969 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1970 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
1971 AND title = :title AND owner_uid = :uid");
1972 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
1974 if (!$sth->fetch()) {
1976 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1978 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
1980 if (!$tr_in_progress) $pdo->commit();
1991 * Fixes incomplete URLs by prepending "http://".
1992 * Also replaces feed:// with http://, and
1993 * prepends a trailing slash if the url is a domain name only.
1995 * @param string $url Possibly incomplete URL
1997 * @return string Fixed URL.
1999 function fix_url($url) {
2001 // support schema-less urls
2002 if (strpos($url, '//') === 0) {
2003 $url = 'https:' . $url;
2006 if (strpos($url, '://') === false) {
2007 $url = 'http://' . $url;
2008 } else if (substr($url, 0, 5) == 'feed:') {
2009 $url = 'http:' . substr($url, 5);
2012 //prepend slash if the URL has no slash in it
2013 // "http://www.example" -> "http://www.example/"
2014 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2018 //convert IDNA hostname to punycode if possible
2019 if (function_exists("idn_to_ascii")) {
2020 $parts = parse_url($url);
2021 if (mb_detect_encoding($parts['host']) != 'ASCII')
2023 $parts['host'] = idn_to_ascii($parts['host']);
2024 $url = build_url($parts);
2028 if ($url != "http:///")
2034 function validate_feed_url($url) {
2035 $parts = parse_url($url);
2037 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2041 /* function save_email_address($email) {
2042 // FIXME: implement persistent storage of emails
2044 if (!$_SESSION['stored_emails'])
2045 $_SESSION['stored_emails'] = array();
2047 if (!in_array($email, $_SESSION['stored_emails']))
2048 array_push($_SESSION['stored_emails'], $email);
2052 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2054 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2056 $is_cat = bool_to_sql_bool($is_cat);
2060 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2061 WHERE feed_id = ? AND is_cat = ?
2062 AND owner_uid = ?");
2063 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2065 if ($row = $sth->fetch()) {
2066 return $row["access_key"];
2068 $key = uniqid_short();
2070 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2071 (access_key, feed_id, is_cat, owner_uid)
2072 VALUES (?, ?, ?, ?)");
2074 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2080 function get_feeds_from_html($url, $content)
2082 $url = fix_url($url);
2083 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2085 libxml_use_internal_errors(true);
2087 $doc = new DOMDocument();
2088 $doc->loadHTML($content);
2089 $xpath = new DOMXPath($doc);
2090 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2091 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2092 $feedUrls = array();
2093 foreach ($entries as $entry) {
2094 if ($entry->hasAttribute('href')) {
2095 $title = $entry->getAttribute('title');
2097 $title = $entry->getAttribute('type');
2099 $feedUrl = rewrite_relative_url(
2100 $baseUrl, $entry->getAttribute('href')
2102 $feedUrls[$feedUrl] = $title;
2108 function is_html($content) {
2109 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2112 function url_is_html($url, $login = false, $pass = false) {
2113 return is_html(fetch_file_contents($url, false, $login, $pass));
2116 function build_url($parts) {
2117 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2120 function cleanup_url_path($path) {
2121 $path = str_replace("/./", "/", $path);
2122 $path = str_replace("//", "/", $path);
2128 * Converts a (possibly) relative URL to a absolute one.
2130 * @param string $url Base URL (i.e. from where the document is)
2131 * @param string $rel_url Possibly relative URL in the document
2133 * @return string Absolute URL
2135 function rewrite_relative_url($url, $rel_url) {
2136 if (strpos($rel_url, "://") !== false) {
2138 } else if (strpos($rel_url, "//") === 0) {
2139 # protocol-relative URL (rare but they exist)
2141 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2142 # magnet:, feed:, etc
2144 } else if (strpos($rel_url, "/") === 0) {
2145 $parts = parse_url($url);
2146 $parts['path'] = $rel_url;
2147 $parts['path'] = cleanup_url_path($parts['path']);
2149 return build_url($parts);
2152 $parts = parse_url($url);
2153 if (!isset($parts['path'])) {
2154 $parts['path'] = '/';
2156 $dir = $parts['path'];
2157 if (substr($dir, -1) !== '/') {
2158 $dir = dirname($parts['path']);
2159 $dir !== '/' && $dir .= '/';
2161 $parts['path'] = $dir . $rel_url;
2162 $parts['path'] = cleanup_url_path($parts['path']);
2164 return build_url($parts);
2168 function cleanup_tags($days = 14, $limit = 1000) {
2170 $days = (int) $days;
2172 if (DB_TYPE
== "pgsql") {
2173 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2174 } else if (DB_TYPE
== "mysql") {
2175 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2182 while ($limit > 0) {
2185 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2186 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2187 WHERE post_int_id = int_id AND $interval_query AND
2188 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2189 $sth->execute([$limit]);
2193 while ($line = $sth->fetch()) {
2194 array_push($ids, $line['id']);
2197 if (count($ids) > 0) {
2198 $ids = join(",", $ids);
2200 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2201 $tags_deleted = $usth->rowCount();
2206 $limit -= $limit_part;
2209 return $tags_deleted;
2212 function print_user_stylesheet() {
2213 $value = get_pref('USER_STYLESHEET');
2216 print "<style type=\"text/css\">";
2217 print str_replace("<br/>", "\n", $value);
2223 function filter_to_sql($filter, $owner_uid) {
2228 if (DB_TYPE
== "pgsql")
2231 $reg_qpart = "REGEXP";
2233 foreach ($filter["rules"] AS $rule) {
2234 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2235 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2236 $rule['reg_exp']) !== FALSE;
2238 if ($regexp_valid) {
2240 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2242 switch ($rule["type"]) {
2244 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2245 $rule['reg_exp'] . "')";
2248 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2249 $rule['reg_exp'] . "')";
2252 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2253 $rule['reg_exp'] . "') OR LOWER(" .
2254 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2257 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2258 $rule['reg_exp'] . "')";
2261 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2262 $rule['reg_exp'] . "')";
2265 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2266 $rule['reg_exp'] . "')";
2270 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2272 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2273 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2276 if (isset($rule["cat_id"])) {
2278 if ($rule["cat_id"] > 0) {
2279 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2280 array_push($children, $rule["cat_id"]);
2281 $children = array_map("intval", $children);
2283 $children = join(",", $children);
2285 $cat_qpart = "cat_id IN ($children)";
2287 $cat_qpart = "cat_id IS NULL";
2290 $qpart .= " AND $cat_qpart";
2293 $qpart .= " AND feed_id IS NOT NULL";
2295 array_push($query, "($qpart)");
2300 if (count($query) > 0) {
2301 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2303 $fullquery = "(false)";
2306 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2311 if (!function_exists('gzdecode')) {
2312 function gzdecode($string) { // no support for 2nd argument
2313 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2314 base64_encode($string));
2318 function get_random_bytes($length) {
2319 if (function_exists('openssl_random_pseudo_bytes')) {
2320 return openssl_random_pseudo_bytes($length);
2324 for ($i = 0; $i < $length; $i++
)
2325 $output .= chr(mt_rand(0, 255));
2331 function read_stdin() {
2332 $fp = fopen("php://stdin", "r");
2335 $line = trim(fgets($fp));
2343 function implements_interface($class, $interface) {
2344 return in_array($interface, class_implements($class));
2347 function get_minified_js($files) {
2348 require_once 'lib/jshrink/Minifier.php';
2352 foreach ($files as $js) {
2353 if (!isset($_GET['debug'])) {
2354 $cached_file = CACHE_DIR
. "/js/".basename($js);
2356 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2358 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2360 if ($header && $contents) {
2361 list($htag, $hversion) = explode(":", $header);
2363 if ($htag == "tt-rss" && $hversion == VERSION
) {
2370 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2371 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2375 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2382 function calculate_dep_timestamp() {
2383 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2387 foreach ($files as $file) {
2388 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2394 function T_js_decl($s1, $s2) {
2396 $s1 = preg_replace("/\n/", "", $s1);
2397 $s2 = preg_replace("/\n/", "", $s2);
2399 $s1 = preg_replace("/\"/", "\\\"", $s1);
2400 $s2 = preg_replace("/\"/", "\\\"", $s2);
2402 return "T_messages[\"$s1\"] = \"$s2\";\n";
2406 function init_js_translations() {
2408 print 'var T_messages = new Object();
2411 if (T_messages[msg]) {
2412 return T_messages[msg];
2418 function ngettext(msg1, msg2, n) {
2419 return __((parseInt(n) > 1) ? msg2 : msg1);
2422 $l10n = _get_reader();
2424 for ($i = 0; $i < $l10n->total
; $i++
) {
2425 $orig = $l10n->get_original_string($i);
2426 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2427 $key = explode(chr(0), $orig);
2428 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2429 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2431 $translation = __($orig);
2432 print T_js_decl($orig, $translation);
2437 function get_theme_path($theme) {
2438 if ($theme == "default.php")
2439 return "css/default.css";
2441 $check = "themes/$theme";
2442 if (file_exists($check)) return $check;
2444 $check = "themes.local/$theme";
2445 if (file_exists($check)) return $check;
2448 function theme_valid($theme) {
2449 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2451 if (in_array($theme, $bundled_themes)) return true;
2453 $file = "themes/" . basename($theme);
2455 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2457 if (file_exists($file) && is_readable($file)) {
2458 $fh = fopen($file, "r");
2461 $header = fgets($fh);
2464 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2472 * @SuppressWarnings(unused)
2474 function error_json($code) {
2475 require_once "errors.php";
2477 @$message = $ERRORS[$code];
2479 return json_encode(array("error" =>
2480 array("code" => $code, "message" => $message)));
2484 /*function abs_to_rel_path($dir) {
2485 $tmp = str_replace(dirname(__DIR__), "", $dir);
2487 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2492 function get_upload_error_message($code) {
2495 0 => __('There is no error, the file uploaded with success'),
2496 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2497 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2498 3 => __('The uploaded file was only partially uploaded'),
2499 4 => __('No file was uploaded'),
2500 6 => __('Missing a temporary folder'),
2501 7 => __('Failed to write file to disk.'),
2502 8 => __('A PHP extension stopped the file upload.'),
2505 return $errors[$code];
2508 function base64_img($filename) {
2509 if (file_exists($filename)) {
2510 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2512 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2518 /* this is essentially a wrapper for readfile() which allows plugins to hook
2519 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2521 hook function should return true if request was handled (or at least attempted to)
2523 note that this can be called without user context so the plugin to handle this
2524 should be loaded systemwide in config.php */
2525 function send_local_file($filename) {
2526 if (file_exists($filename)) {
2527 $tmppluginhost = new PluginHost();
2529 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2530 $tmppluginhost->load_data();
2532 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2533 if ($plugin->hook_send_local_file($filename)) return true;
2536 $mimetype = mime_content_type($filename);
2537 header("Content-type: $mimetype");
2539 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2540 header("Last-Modified: $stamp", true);
2542 return readfile($filename);
2548 function check_mysql_tables() {
2551 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2552 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2553 $sth->execute([DB_NAME
]);
2557 while ($line = $sth->fetch()) {
2558 array_push($bad_tables, $line);
2564 function validate_field($string, $allowed, $default = "") {
2565 if (in_array($string, $allowed))
2571 function arr_qmarks($arr) {
2572 return str_repeat('?,', count($arr) - 1) . '?';