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('PROXY')) {
406 curl_setopt($ch, CURLOPT_PROXY
, 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 $context_options = array(
489 'ignore_errors' => true,
490 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
491 'protocol_version'=> 1.1)
494 if (!$post_query && $last_modified) {
495 $context_options['http']['header'] = "If-Modified-Since: $last_modified\r\n";
498 if (defined('PROXY')) {
499 $context_options['http']['proxy'] = PROXY
;
502 $context = stream_context_create($context_options);
504 $old_error = error_get_last();
506 $data = @file_get_contents
($url, false, $context);
508 if (isset($http_response_header) && is_array($http_response_header)) {
509 foreach ($http_response_header as $header) {
510 if (strstr($header, ": ") !== FALSE) {
511 list ($key, $value) = explode(": ", $header);
513 $key = strtolower($key);
515 if ($key == 'content-type') {
516 $fetch_last_content_type = $value;
517 // don't abort here b/c there might be more than one
518 // e.g. if we were being redirected -- last one is the right one
519 } else if ($key == 'last-modified') {
520 $fetch_last_modified = $value;
524 if (substr(strtolower($header), 0, 7) == 'http/1.') {
525 $fetch_last_error_code = (int) substr($header, 9, 3);
526 $fetch_last_error = $header;
531 if ($fetch_last_error_code != 200) {
532 $error = error_get_last();
534 if ($error['message'] != $old_error['message']) {
535 $fetch_last_error .= "; " . $error["message"];
538 $fetch_last_error_content = $data;
548 * Try to determine the favicon URL for a feed.
549 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
550 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
552 * @param string $url A feed or page URL
554 * @return mixed The favicon URL, or false if none was found.
556 function get_favicon_url($url) {
558 $favicon_url = false;
560 if ($html = @fetch_file_contents
($url)) {
562 libxml_use_internal_errors(true);
564 $doc = new DOMDocument();
565 $doc->loadHTML($html);
566 $xpath = new DOMXPath($doc);
568 $base = $xpath->query('/html/head/base[@href]');
569 foreach ($base as $b) {
570 $url = rewrite_relative_url($url, $b->getAttribute("href"));
574 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
575 if (count($entries) > 0) {
576 foreach ($entries as $entry) {
577 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
584 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
587 } // function get_favicon_url
589 function initialize_user_prefs($uid, $profile = false) {
591 if (get_schema_version() < 63) $profile_qpart = "";
594 $in_nested_tr = false;
597 $pdo->beginTransaction();
598 } catch (Exception
$e) {
599 $in_nested_tr = true;
602 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
604 $profile = $profile ?
$profile : null;
606 $u_sth = $pdo->prepare("SELECT pref_name
607 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
608 (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
609 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
611 $active_prefs = array();
613 while ($line = $u_sth->fetch()) {
614 array_push($active_prefs, $line["pref_name"]);
617 while ($line = $sth->fetch()) {
618 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
619 // print "adding " . $line["pref_name"] . "<br>";
621 if (get_schema_version() < 63) {
622 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
623 (owner_uid,pref_name,value) VALUES
625 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
628 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
629 (owner_uid,pref_name,value, profile) VALUES
631 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
637 if (!$in_nested_tr) $pdo->commit();
641 function get_ssl_certificate_id() {
642 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
643 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
644 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
645 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
646 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
648 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
649 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
650 $_SERVER["SSL_CLIENT_V_START"] .
651 $_SERVER["SSL_CLIENT_V_END"] .
652 $_SERVER["SSL_CLIENT_S_DN"]);
657 function authenticate_user($login, $password, $check_only = false) {
659 if (!SINGLE_USER_MODE
) {
662 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
664 $user_id = (int) $plugin->authenticate($login, $password);
667 $_SESSION["auth_module"] = strtolower(get_class($plugin));
672 if ($user_id && !$check_only) {
675 $_SESSION["uid"] = $user_id;
676 $_SESSION["version"] = VERSION_STATIC
;
679 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
681 $sth->execute([$user_id]);
682 $row = $sth->fetch();
684 $_SESSION["name"] = $row["login"];
685 $_SESSION["access_level"] = $row["access_level"];
686 $_SESSION["csrf_token"] = uniqid_short();
688 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
689 $usth->execute([$user_id]);
691 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
692 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
693 $_SESSION["pwd_hash"] = $row["pwd_hash"];
695 $_SESSION["last_version_check"] = time();
697 initialize_user_prefs($_SESSION["uid"]);
706 $_SESSION["uid"] = 1;
707 $_SESSION["name"] = "admin";
708 $_SESSION["access_level"] = 10;
710 $_SESSION["hide_hello"] = true;
711 $_SESSION["hide_logout"] = true;
713 $_SESSION["auth_module"] = false;
715 if (!$_SESSION["csrf_token"]) {
716 $_SESSION["csrf_token"] = uniqid_short();
719 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
721 initialize_user_prefs($_SESSION["uid"]);
727 // this is used for user http parameters unless HTML code is actually needed
728 function clean($param) {
729 if (is_array($param)) {
730 return array_map("strip_tags", $param);
731 } else if (is_string($param)) {
732 return strip_tags($param);
738 function make_password($length = 8) {
741 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
745 while ($i < $length) {
746 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
748 if (!strstr($password, $char)) {
756 // this is called after user is created to initialize default feeds, labels
759 // user preferences are checked on every login, not here
761 function initialize_user($uid) {
765 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
766 values (?, 'Tiny Tiny RSS: Forum',
767 'http://tt-rss.org/forum/rss.php')");
768 $sth->execute([$uid]);
771 function logout_user() {
773 if (isset($_COOKIE[session_name()])) {
774 setcookie(session_name(), '', time()-42000, '/');
778 function validate_csrf($csrf_token) {
779 return $csrf_token == $_SESSION['csrf_token'];
782 function load_user_plugins($owner_uid, $pluginhost = false) {
784 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
786 if ($owner_uid && SCHEMA_VERSION
>= 100) {
787 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
789 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
791 if (get_schema_version() > 100) {
792 $pluginhost->load_data();
797 function login_sequence() {
800 if (SINGLE_USER_MODE
) {
802 authenticate_user("admin", null);
804 load_user_plugins($_SESSION["uid"]);
806 if (!validate_session()) $_SESSION["uid"] = false;
808 if (!$_SESSION["uid"]) {
810 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
811 $_SESSION["ref_schema_version"] = get_schema_version(true);
813 authenticate_user(null, null, true);
816 if (!$_SESSION["uid"]) {
818 setcookie(session_name(), '', time()-42000, '/');
825 /* bump login timestamp */
826 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
827 $sth->execute([$_SESSION['uid']]);
829 $_SESSION["last_login_update"] = time();
832 if ($_SESSION["uid"]) {
834 load_user_plugins($_SESSION["uid"]);
838 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
840 (SELECT COUNT(id) FROM ttrss_feeds WHERE
841 ttrss_feeds.id = feed_id) = 0");
843 $sth->execute([$_SESSION['uid']]);
845 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
847 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
848 ttrss_feed_categories.id = feed_id) = 0");
850 $sth->execute([$_SESSION['uid']]);
856 function truncate_string($str, $max_len, $suffix = '…') {
857 if (mb_strlen($str, "utf-8") > $max_len) {
858 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
865 function truncate_middle($str, $max_len, $suffix = '…') {
866 if (strlen($str) > $max_len) {
867 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
873 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
876 $source_tz = new DateTimeZone($source_tz);
877 } catch (Exception
$e) {
878 $source_tz = new DateTimeZone('UTC');
882 $dest_tz = new DateTimeZone($dest_tz);
883 } catch (Exception
$e) {
884 $dest_tz = new DateTimeZone('UTC');
887 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
888 return $dt->format('U') +
$dest_tz->getOffset($dt);
891 function make_local_datetime($timestamp, $long, $owner_uid = false,
892 $no_smart_dt = false, $eta_min = false) {
894 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
895 if (!$timestamp) $timestamp = '1970-01-01 0:00';
900 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
902 $timestamp = substr($timestamp, 0, 19);
904 # We store date in UTC internally
905 $dt = new DateTime($timestamp, $utc_tz);
907 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
909 if ($user_tz_string != 'Automatic') {
912 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
913 } catch (Exception
$e) {
917 $tz_offset = $user_tz->getOffset($dt);
919 $tz_offset = (int) -$_SESSION["clientTzOffset"];
922 $user_timestamp = $dt->format('U') +
$tz_offset;
925 return smart_date_time($user_timestamp,
926 $tz_offset, $owner_uid, $eta_min);
929 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
931 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
933 return date($format, $user_timestamp);
937 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
938 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
940 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
941 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
942 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
943 return date("G:i", $timestamp);
944 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
945 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
946 return date($format, $timestamp);
948 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
949 return date($format, $timestamp);
953 function sql_bool_to_bool($s) {
954 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
957 function bool_to_sql_bool($s) {
961 // Session caching removed due to causing wrong redirects to upgrade
962 // script when get_schema_version() is called on an obsolete session
963 // created on a previous schema version.
964 function get_schema_version($nocache = false) {
965 global $schema_version;
969 if (!$schema_version && !$nocache) {
970 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
971 $version = $row["schema_version"];
972 $schema_version = $version;
975 return $schema_version;
979 function sanity_check() {
980 require_once 'errors.php';
984 $schema_version = get_schema_version(true);
986 if ($schema_version != SCHEMA_VERSION
) {
990 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
993 function file_is_locked($filename) {
994 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
995 if (function_exists('flock')) {
996 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
998 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1009 return true; // consider the file always locked and skip the test
1016 function make_lockfile($filename) {
1017 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1019 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1020 $stat_h = fstat($fp);
1021 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1023 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1024 if ($stat_h["ino"] != $stat_f["ino"] ||
1025 $stat_h["dev"] != $stat_f["dev"]) {
1031 if (function_exists('posix_getpid')) {
1032 fwrite($fp, posix_getpid() . "\n");
1040 function make_stampfile($filename) {
1041 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1043 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1044 fwrite($fp, time() . "\n");
1045 flock($fp, LOCK_UN
);
1053 function sql_random_function() {
1054 if (DB_TYPE
== "mysql") {
1061 function getFeedUnread($feed, $is_cat = false) {
1062 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1065 function checkbox_to_sql_bool($val) {
1066 return ($val == "on") ?
1 : 0;
1069 function uniqid_short() {
1070 return uniqid(base_convert(rand(), 10, 36));
1073 function make_init_params() {
1076 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1077 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1078 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1079 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1081 $params[strtolower($param)] = (int) get_pref($param);
1084 $params["icons_url"] = ICONS_URL
;
1085 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1086 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1087 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1088 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1089 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1090 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1091 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1093 $theme = get_pref( "USER_CSS_THEME", false, false);
1094 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1096 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1098 $params["php_platform"] = PHP_OS
;
1099 $params["php_version"] = PHP_VERSION
;
1101 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1105 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1106 ttrss_feeds WHERE owner_uid = ?");
1107 $sth->execute([$_SESSION['uid']]);
1108 $row = $sth->fetch();
1110 $max_feed_id = $row["mid"];
1111 $num_feeds = $row["nf"];
1113 $params["max_feed_id"] = (int) $max_feed_id;
1114 $params["num_feeds"] = (int) $num_feeds;
1116 $params["hotkeys"] = get_hotkeys_map();
1118 $params["csrf_token"] = $_SESSION["csrf_token"];
1119 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1121 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1123 $params["icon_alert"] = base64_img("images/alert.png");
1124 $params["icon_information"] = base64_img("images/information.png");
1125 $params["icon_cross"] = base64_img("images/cross.png");
1126 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1128 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1133 function get_hotkeys_info() {
1135 __("Navigation") => array(
1136 "next_feed" => __("Open next feed"),
1137 "prev_feed" => __("Open previous feed"),
1138 "next_article" => __("Open next article"),
1139 "prev_article" => __("Open previous article"),
1140 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1141 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1142 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1143 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1144 "search_dialog" => __("Show search dialog")),
1145 __("Article") => array(
1146 "toggle_mark" => __("Toggle starred"),
1147 "toggle_publ" => __("Toggle published"),
1148 "toggle_unread" => __("Toggle unread"),
1149 "edit_tags" => __("Edit tags"),
1150 "open_in_new_window" => __("Open in new window"),
1151 "catchup_below" => __("Mark below as read"),
1152 "catchup_above" => __("Mark above as read"),
1153 "article_scroll_down" => __("Scroll down"),
1154 "article_scroll_up" => __("Scroll up"),
1155 "select_article_cursor" => __("Select article under cursor"),
1156 "email_article" => __("Email article"),
1157 "close_article" => __("Close/collapse article"),
1158 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1159 "toggle_widescreen" => __("Toggle widescreen mode"),
1160 "toggle_embed_original" => __("Toggle embed original")),
1161 __("Article selection") => array(
1162 "select_all" => __("Select all articles"),
1163 "select_unread" => __("Select unread"),
1164 "select_marked" => __("Select starred"),
1165 "select_published" => __("Select published"),
1166 "select_invert" => __("Invert selection"),
1167 "select_none" => __("Deselect everything")),
1168 __("Feed") => array(
1169 "feed_refresh" => __("Refresh current feed"),
1170 "feed_unhide_read" => __("Un/hide read feeds"),
1171 "feed_subscribe" => __("Subscribe to feed"),
1172 "feed_edit" => __("Edit feed"),
1173 "feed_catchup" => __("Mark as read"),
1174 "feed_reverse" => __("Reverse headlines"),
1175 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1176 "feed_debug_update" => __("Debug feed update"),
1177 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1178 "catchup_all" => __("Mark all feeds as read"),
1179 "cat_toggle_collapse" => __("Un/collapse current category"),
1180 "toggle_combined_mode" => __("Toggle combined mode"),
1181 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1182 __("Go to") => array(
1183 "goto_all" => __("All articles"),
1184 "goto_fresh" => __("Fresh"),
1185 "goto_marked" => __("Starred"),
1186 "goto_published" => __("Published"),
1187 "goto_tagcloud" => __("Tag cloud"),
1188 "goto_prefs" => __("Preferences")),
1189 __("Other") => array(
1190 "create_label" => __("Create label"),
1191 "create_filter" => __("Create filter"),
1192 "collapse_sidebar" => __("Un/collapse sidebar"),
1193 "help_dialog" => __("Show help dialog"))
1196 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1197 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1203 function get_hotkeys_map() {
1205 // "navigation" => array(
1208 "n" => "next_article",
1209 "p" => "prev_article",
1210 "(38)|up" => "prev_article",
1211 "(40)|down" => "next_article",
1212 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1213 // "^(40)|Ctrl-down" => "next_article_noscroll",
1214 "(191)|/" => "search_dialog",
1215 // "article" => array(
1216 "s" => "toggle_mark",
1217 "*s" => "toggle_publ",
1218 "u" => "toggle_unread",
1219 "*t" => "edit_tags",
1220 "o" => "open_in_new_window",
1221 "c p" => "catchup_below",
1222 "c n" => "catchup_above",
1223 "*n" => "article_scroll_down",
1224 "*p" => "article_scroll_up",
1225 "*(38)|Shift+up" => "article_scroll_up",
1226 "*(40)|Shift+down" => "article_scroll_down",
1227 "a *w" => "toggle_widescreen",
1228 "a e" => "toggle_embed_original",
1229 "e" => "email_article",
1230 "a q" => "close_article",
1231 // "article_selection" => array(
1232 "a a" => "select_all",
1233 "a u" => "select_unread",
1234 "a *u" => "select_marked",
1235 "a p" => "select_published",
1236 "a i" => "select_invert",
1237 "a n" => "select_none",
1239 "f r" => "feed_refresh",
1240 "f a" => "feed_unhide_read",
1241 "f s" => "feed_subscribe",
1242 "f e" => "feed_edit",
1243 "f q" => "feed_catchup",
1244 "f x" => "feed_reverse",
1245 "f g" => "feed_toggle_vgroup",
1246 "f *d" => "feed_debug_update",
1247 "f *g" => "feed_debug_viewfeed",
1248 "f *c" => "toggle_combined_mode",
1249 "f c" => "toggle_cdm_expanded",
1250 "*q" => "catchup_all",
1251 "x" => "cat_toggle_collapse",
1253 "g a" => "goto_all",
1254 "g f" => "goto_fresh",
1255 "g s" => "goto_marked",
1256 "g p" => "goto_published",
1257 "g t" => "goto_tagcloud",
1258 "g *p" => "goto_prefs",
1259 // "other" => array(
1260 "(9)|Tab" => "select_article_cursor", // tab
1261 "c l" => "create_label",
1262 "c f" => "create_filter",
1263 "c s" => "collapse_sidebar",
1264 "^(191)|Ctrl+/" => "help_dialog",
1267 if (get_pref('COMBINED_DISPLAY_MODE')) {
1268 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1269 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1272 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1273 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1276 $prefixes = array();
1278 foreach (array_keys($hotkeys) as $hotkey) {
1279 $pair = explode(" ", $hotkey, 2);
1281 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1282 array_push($prefixes, $pair[0]);
1286 return array($prefixes, $hotkeys);
1289 function check_for_update() {
1290 if (defined("GIT_VERSION_TIMESTAMP")) {
1291 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1294 $content = json_decode($content, true);
1296 if ($content && isset($content["changeset"])) {
1297 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1298 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1300 return $content["changeset"]["id"];
1309 function make_runtime_info($disable_update_check = false) {
1314 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1315 ttrss_feeds WHERE owner_uid = ?");
1316 $sth->execute([$_SESSION['uid']]);
1317 $row = $sth->fetch();
1319 $max_feed_id = $row['mid'];
1320 $num_feeds = $row['nf'];
1322 $data["max_feed_id"] = (int) $max_feed_id;
1323 $data["num_feeds"] = (int) $num_feeds;
1325 $data['last_article_id'] = Article
::getLastArticleId();
1326 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1328 $data['dep_ts'] = calculate_dep_timestamp();
1329 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1331 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1333 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1334 $update_result = @check_for_update
();
1336 $data["update_result"] = $update_result;
1338 $_SESSION["last_version_check"] = time();
1341 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1343 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1345 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1347 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1350 $stamp_delta = time() - $stamp;
1352 if ($stamp_delta > 1800) {
1356 $_SESSION["daemon_stamp_check"] = time();
1359 $data['daemon_stamp_ok'] = $stamp_check;
1361 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1363 $data['daemon_stamp'] = $stamp_fmt;
1371 function search_to_sql($search, $search_language) {
1373 $keywords = str_getcsv(trim($search), " ");
1374 $query_keywords = array();
1375 $search_words = array();
1376 $search_query_leftover = array();
1380 if ($search_language)
1381 $search_language = $pdo->quote(mb_strtolower($search_language));
1383 $search_language = $pdo->quote("english");
1385 foreach ($keywords as $k) {
1386 if (strpos($k, "-") === 0) {
1393 $commandpair = explode(":", mb_strtolower($k), 2);
1395 switch ($commandpair[0]) {
1397 if ($commandpair[1]) {
1398 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1399 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1401 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1402 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1403 array_push($search_words, $k);
1407 if ($commandpair[1]) {
1408 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1409 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1411 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1412 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1413 array_push($search_words, $k);
1417 if ($commandpair[1]) {
1418 if ($commandpair[1] == "true")
1419 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1420 else if ($commandpair[1] == "false")
1421 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1423 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1424 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1426 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1427 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1428 if (!$not) array_push($search_words, $k);
1433 if ($commandpair[1]) {
1434 if ($commandpair[1] == "true")
1435 array_push($query_keywords, "($not (marked = true))");
1437 array_push($query_keywords, "($not (marked = false))");
1439 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1440 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1441 if (!$not) array_push($search_words, $k);
1445 if ($commandpair[1]) {
1446 if ($commandpair[1] == "true")
1447 array_push($query_keywords, "($not (published = true))");
1449 array_push($query_keywords, "($not (published = false))");
1452 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1453 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1454 if (!$not) array_push($search_words, $k);
1458 if ($commandpair[1]) {
1459 if ($commandpair[1] == "true")
1460 array_push($query_keywords, "($not (unread = true))");
1462 array_push($query_keywords, "($not (unread = false))");
1465 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1466 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1467 if (!$not) array_push($search_words, $k);
1471 if (strpos($k, "@") === 0) {
1473 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1474 $orig_ts = strtotime(substr($k, 1));
1475 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1477 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1479 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1482 if (DB_TYPE
== "pgsql") {
1483 $k = mb_strtolower($k);
1484 array_push($search_query_leftover, $not ?
"!$k" : $k);
1486 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1487 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1490 if (!$not) array_push($search_words, $k);
1495 if (count($search_query_leftover) > 0) {
1496 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1498 if (DB_TYPE
== "pgsql") {
1499 array_push($query_keywords,
1500 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1505 $search_query_part = implode("AND", $query_keywords);
1507 return array($search_query_part, $search_words);
1510 function iframe_whitelisted($entry) {
1511 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1513 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1516 foreach ($whitelist as $w) {
1517 if ($src == $w ||
$src == "www.$w")
1525 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1526 if (!$owner) $owner = $_SESSION["uid"];
1528 $res = trim($str); if (!$res) return '';
1530 $charset_hack = '<head>
1531 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1534 $res = trim($res); if (!$res) return '';
1536 libxml_use_internal_errors(true);
1538 $doc = new DOMDocument();
1539 $doc->loadHTML($charset_hack . $res);
1540 $xpath = new DOMXPath($doc);
1542 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1544 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1546 foreach ($entries as $entry) {
1548 if ($entry->hasAttribute('href')) {
1549 $entry->setAttribute('href',
1550 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1552 $entry->setAttribute('rel', 'noopener noreferrer');
1555 if ($entry->hasAttribute('src')) {
1556 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1557 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1559 if (file_exists($cached_filename)) {
1561 // this is strictly cosmetic
1562 if ($entry->tagName
== 'img') {
1564 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1566 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1572 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1574 if ($entry->hasAttribute('srcset')) {
1575 $entry->removeAttribute('srcset');
1578 if ($entry->hasAttribute('sizes')) {
1579 $entry->removeAttribute('sizes');
1583 $entry->setAttribute('src', $src);
1586 if ($entry->nodeName
== 'img') {
1587 $entry->setAttribute('referrerpolicy', 'no-referrer');
1589 if ($entry->hasAttribute('src')) {
1590 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1592 if (is_prefix_https() && !$is_https_url) {
1594 if ($entry->hasAttribute('srcset')) {
1595 $entry->removeAttribute('srcset');
1598 if ($entry->hasAttribute('sizes')) {
1599 $entry->removeAttribute('sizes');
1604 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1605 $force_remove_images ||
$_SESSION["bw_limit"]) {
1607 $p = $doc->createElement('p');
1609 $a = $doc->createElement('a');
1610 $a->setAttribute('href', $entry->getAttribute('src'));
1612 $a->appendChild(new DOMText($entry->getAttribute('src')));
1613 $a->setAttribute('target', '_blank');
1614 $a->setAttribute('rel', 'noopener noreferrer');
1616 $p->appendChild($a);
1618 $entry->parentNode
->replaceChild($p, $entry);
1622 if (strtolower($entry->nodeName
) == "a") {
1623 $entry->setAttribute("target", "_blank");
1624 $entry->setAttribute("rel", "noopener noreferrer");
1628 $entries = $xpath->query('//iframe');
1629 foreach ($entries as $entry) {
1630 if (!iframe_whitelisted($entry)) {
1631 $entry->setAttribute('sandbox', 'allow-scripts');
1633 if (is_prefix_https()) {
1634 $entry->setAttribute("src",
1635 str_replace("http://", "https://",
1636 $entry->getAttribute("src")));
1641 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1642 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1643 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1644 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1645 'dt', 'em', 'footer', 'figure', 'figcaption',
1646 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1647 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1648 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1649 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1650 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1651 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1653 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1655 $disallowed_attributes = array('id', 'style', 'class');
1657 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1658 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1659 if (is_array($retval)) {
1661 $allowed_elements = $retval[1];
1662 $disallowed_attributes = $retval[2];
1668 $doc->removeChild($doc->firstChild
); //remove doctype
1669 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1671 if ($highlight_words) {
1672 foreach ($highlight_words as $word) {
1674 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1676 $elements = $xpath->query("//*/text()");
1678 foreach ($elements as $child) {
1680 $fragment = $doc->createDocumentFragment();
1681 $text = $child->textContent
;
1683 while (($pos = mb_stripos($text, $word)) !== false) {
1684 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1685 $word = mb_substr($text, $pos, mb_strlen($word));
1686 $highlight = $doc->createElement('span');
1687 $highlight->appendChild(new DomText($word));
1688 $highlight->setAttribute('class', 'highlight');
1689 $fragment->appendChild($highlight);
1690 $text = mb_substr($text, $pos +
mb_strlen($word));
1693 if (!empty($text)) $fragment->appendChild(new DomText($text));
1695 $child->parentNode
->replaceChild($fragment, $child);
1700 $res = $doc->saveHTML();
1702 /* strip everything outside of <body>...</body> */
1704 $res_frag = array();
1705 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1706 return $res_frag[1];
1712 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1713 $xpath = new DOMXPath($doc);
1714 $entries = $xpath->query('//*');
1716 foreach ($entries as $entry) {
1717 if (!in_array($entry->nodeName
, $allowed_elements)) {
1718 $entry->parentNode
->removeChild($entry);
1721 if ($entry->hasAttributes()) {
1722 $attrs_to_remove = array();
1724 foreach ($entry->attributes
as $attr) {
1726 if (strpos($attr->nodeName
, 'on') === 0) {
1727 array_push($attrs_to_remove, $attr);
1730 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1731 array_push($attrs_to_remove, $attr);
1734 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1735 array_push($attrs_to_remove, $attr);
1739 foreach ($attrs_to_remove as $attr) {
1740 $entry->removeAttributeNode($attr);
1748 function trim_array($array) {
1750 array_walk($tmp, 'trim');
1754 function tag_is_valid($tag) {
1755 if ($tag == '') return false;
1756 if (is_numeric($tag)) return false;
1757 if (mb_strlen($tag) > 250) return false;
1759 if (!$tag) return false;
1764 function render_login_form() {
1765 header('Cache-Control: public');
1767 require_once "login_form.php";
1771 function T_sprintf() {
1772 $args = func_get_args();
1773 return vsprintf(__(array_shift($args)), $args);
1776 function print_checkpoint($n, $s) {
1777 $ts = microtime(true);
1778 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1782 function sanitize_tag($tag) {
1785 $tag = mb_strtolower($tag, 'utf-8');
1787 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1789 if (DB_TYPE
== "mysql") {
1790 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1796 function is_server_https() {
1797 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1800 function is_prefix_https() {
1801 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1804 // this returns SELF_URL_PATH sans ending slash
1805 function get_self_url_prefix() {
1806 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1807 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1809 return SELF_URL_PATH
;
1813 function encrypt_password($pass, $salt = '', $mode2 = false) {
1814 if ($salt && $mode2) {
1815 return "MODE2:" . hash('sha256', $salt . $pass);
1817 return "SHA1X:" . sha1("$salt:$pass");
1819 return "SHA1:" . sha1($pass);
1821 } // function encrypt_password
1823 function load_filters($feed_id, $owner_uid) {
1826 $feed_id = (int) $feed_id;
1827 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1830 $null_cat_qpart = "cat_id IS NULL OR";
1832 $null_cat_qpart = "";
1836 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1837 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1838 $sth->execute([$owner_uid]);
1840 $check_cats = array_merge(
1841 Feeds
::getParentCategories($cat_id, $owner_uid),
1844 $check_cats_str = join(",", $check_cats);
1845 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1847 while ($line = $sth->fetch()) {
1848 $filter_id = $line["id"];
1850 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1852 $sth2 = $pdo->prepare("SELECT
1853 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1854 FROM ttrss_filters2_rules AS r,
1855 ttrss_filter_types AS t
1857 (match_on IS NOT NULL OR
1858 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1859 (feed_id IS NULL OR feed_id = ?))) AND
1860 filter_type = t.id AND filter_id = ?");
1861 $sth2->execute([$feed_id, $filter_id]);
1866 while ($rule_line = $sth2->fetch()) {
1867 # print_r($rule_line);
1869 if ($rule_line["match_on"]) {
1870 $match_on = json_decode($rule_line["match_on"], true);
1872 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1875 $rule["reg_exp"] = $rule_line["reg_exp"];
1876 $rule["type"] = $rule_line["type_name"];
1877 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1879 array_push($rules, $rule);
1880 } else if (!$match_any_rule) {
1881 // this filter contains a rule that doesn't match to this feed/category combination
1882 // thus filter has to be rejected
1891 $rule["reg_exp"] = $rule_line["reg_exp"];
1892 $rule["type"] = $rule_line["type_name"];
1893 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1895 array_push($rules, $rule);
1899 if (count($rules) > 0) {
1900 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1901 FROM ttrss_filters2_actions AS a,
1902 ttrss_filter_actions AS t
1904 action_id = t.id AND filter_id = ?");
1905 $sth2->execute([$filter_id]);
1907 while ($action_line = $sth2->fetch()) {
1908 # print_r($action_line);
1911 $action["type"] = $action_line["type_name"];
1912 $action["param"] = $action_line["action_param"];
1914 array_push($actions, $action);
1919 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1920 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1921 $filter["rules"] = $rules;
1922 $filter["actions"] = $actions;
1924 if (count($rules) > 0 && count($actions) > 0) {
1925 array_push($filters, $filter);
1932 function get_score_pic($score) {
1934 return "score_high.png";
1935 } else if ($score > 0) {
1936 return "score_half_high.png";
1937 } else if ($score < -100) {
1938 return "score_low.png";
1939 } else if ($score < 0) {
1940 return "score_half_low.png";
1942 return "score_neutral.png";
1946 function init_plugins() {
1947 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1952 function add_feed_category($feed_cat, $parent_cat_id = false) {
1954 if (!$feed_cat) return false;
1956 $feed_cat = mb_substr($feed_cat, 0, 250);
1957 if (!$parent_cat_id) $parent_cat_id = null;
1960 $tr_in_progress = false;
1963 $pdo->beginTransaction();
1964 } catch (Exception
$e) {
1965 $tr_in_progress = true;
1968 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1969 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
1970 AND title = :title AND owner_uid = :uid");
1971 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
1973 if (!$sth->fetch()) {
1975 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1977 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
1979 if (!$tr_in_progress) $pdo->commit();
1990 * Fixes incomplete URLs by prepending "http://".
1991 * Also replaces feed:// with http://, and
1992 * prepends a trailing slash if the url is a domain name only.
1994 * @param string $url Possibly incomplete URL
1996 * @return string Fixed URL.
1998 function fix_url($url) {
2000 // support schema-less urls
2001 if (strpos($url, '//') === 0) {
2002 $url = 'https:' . $url;
2005 if (strpos($url, '://') === false) {
2006 $url = 'http://' . $url;
2007 } else if (substr($url, 0, 5) == 'feed:') {
2008 $url = 'http:' . substr($url, 5);
2011 //prepend slash if the URL has no slash in it
2012 // "http://www.example" -> "http://www.example/"
2013 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2017 //convert IDNA hostname to punycode if possible
2018 if (function_exists("idn_to_ascii")) {
2019 $parts = parse_url($url);
2020 if (mb_detect_encoding($parts['host']) != 'ASCII')
2022 $parts['host'] = idn_to_ascii($parts['host']);
2023 $url = build_url($parts);
2027 if ($url != "http:///")
2033 function validate_feed_url($url) {
2034 $parts = parse_url($url);
2036 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2040 /* function save_email_address($email) {
2041 // FIXME: implement persistent storage of emails
2043 if (!$_SESSION['stored_emails'])
2044 $_SESSION['stored_emails'] = array();
2046 if (!in_array($email, $_SESSION['stored_emails']))
2047 array_push($_SESSION['stored_emails'], $email);
2051 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2053 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2055 $is_cat = bool_to_sql_bool($is_cat);
2059 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2060 WHERE feed_id = ? AND is_cat = ?
2061 AND owner_uid = ?");
2062 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2064 if ($row = $sth->fetch()) {
2065 return $row["access_key"];
2067 $key = uniqid_short();
2069 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2070 (access_key, feed_id, is_cat, owner_uid)
2071 VALUES (?, ?, ?, ?)");
2073 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2079 function get_feeds_from_html($url, $content)
2081 $url = fix_url($url);
2082 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2084 libxml_use_internal_errors(true);
2086 $doc = new DOMDocument();
2087 $doc->loadHTML($content);
2088 $xpath = new DOMXPath($doc);
2089 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2090 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2091 $feedUrls = array();
2092 foreach ($entries as $entry) {
2093 if ($entry->hasAttribute('href')) {
2094 $title = $entry->getAttribute('title');
2096 $title = $entry->getAttribute('type');
2098 $feedUrl = rewrite_relative_url(
2099 $baseUrl, $entry->getAttribute('href')
2101 $feedUrls[$feedUrl] = $title;
2107 function is_html($content) {
2108 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2111 function url_is_html($url, $login = false, $pass = false) {
2112 return is_html(fetch_file_contents($url, false, $login, $pass));
2115 function build_url($parts) {
2116 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2119 function cleanup_url_path($path) {
2120 $path = str_replace("/./", "/", $path);
2121 $path = str_replace("//", "/", $path);
2127 * Converts a (possibly) relative URL to a absolute one.
2129 * @param string $url Base URL (i.e. from where the document is)
2130 * @param string $rel_url Possibly relative URL in the document
2132 * @return string Absolute URL
2134 function rewrite_relative_url($url, $rel_url) {
2135 if (strpos($rel_url, "://") !== false) {
2137 } else if (strpos($rel_url, "//") === 0) {
2138 # protocol-relative URL (rare but they exist)
2140 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2141 # magnet:, feed:, etc
2143 } else if (strpos($rel_url, "/") === 0) {
2144 $parts = parse_url($url);
2145 $parts['path'] = $rel_url;
2146 $parts['path'] = cleanup_url_path($parts['path']);
2148 return build_url($parts);
2151 $parts = parse_url($url);
2152 if (!isset($parts['path'])) {
2153 $parts['path'] = '/';
2155 $dir = $parts['path'];
2156 if (substr($dir, -1) !== '/') {
2157 $dir = dirname($parts['path']);
2158 $dir !== '/' && $dir .= '/';
2160 $parts['path'] = $dir . $rel_url;
2161 $parts['path'] = cleanup_url_path($parts['path']);
2163 return build_url($parts);
2167 function cleanup_tags($days = 14, $limit = 1000) {
2169 $days = (int) $days;
2171 if (DB_TYPE
== "pgsql") {
2172 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2173 } else if (DB_TYPE
== "mysql") {
2174 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2181 while ($limit > 0) {
2184 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2185 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2186 WHERE post_int_id = int_id AND $interval_query AND
2187 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2188 $sth->execute([$limit]);
2192 while ($line = $sth->fetch()) {
2193 array_push($ids, $line['id']);
2196 if (count($ids) > 0) {
2197 $ids = join(",", $ids);
2199 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2200 $tags_deleted = $usth->rowCount();
2205 $limit -= $limit_part;
2208 return $tags_deleted;
2211 function print_user_stylesheet() {
2212 $value = get_pref('USER_STYLESHEET');
2215 print "<style type=\"text/css\">";
2216 print str_replace("<br/>", "\n", $value);
2222 function filter_to_sql($filter, $owner_uid) {
2227 if (DB_TYPE
== "pgsql")
2230 $reg_qpart = "REGEXP";
2232 foreach ($filter["rules"] AS $rule) {
2233 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2234 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2235 $rule['reg_exp']) !== FALSE;
2237 if ($regexp_valid) {
2239 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2241 switch ($rule["type"]) {
2243 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2244 $rule['reg_exp'] . "')";
2247 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2248 $rule['reg_exp'] . "')";
2251 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2252 $rule['reg_exp'] . "') OR LOWER(" .
2253 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2256 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2257 $rule['reg_exp'] . "')";
2260 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2261 $rule['reg_exp'] . "')";
2264 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2265 $rule['reg_exp'] . "')";
2269 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2271 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2272 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2275 if (isset($rule["cat_id"])) {
2277 if ($rule["cat_id"] > 0) {
2278 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2279 array_push($children, $rule["cat_id"]);
2280 $children = array_map("intval", $children);
2282 $children = join(",", $children);
2284 $cat_qpart = "cat_id IN ($children)";
2286 $cat_qpart = "cat_id IS NULL";
2289 $qpart .= " AND $cat_qpart";
2292 $qpart .= " AND feed_id IS NOT NULL";
2294 array_push($query, "($qpart)");
2299 if (count($query) > 0) {
2300 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2302 $fullquery = "(false)";
2305 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2310 if (!function_exists('gzdecode')) {
2311 function gzdecode($string) { // no support for 2nd argument
2312 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2313 base64_encode($string));
2317 function get_random_bytes($length) {
2318 if (function_exists('openssl_random_pseudo_bytes')) {
2319 return openssl_random_pseudo_bytes($length);
2323 for ($i = 0; $i < $length; $i++
)
2324 $output .= chr(mt_rand(0, 255));
2330 function read_stdin() {
2331 $fp = fopen("php://stdin", "r");
2334 $line = trim(fgets($fp));
2342 function implements_interface($class, $interface) {
2343 return in_array($interface, class_implements($class));
2346 function get_minified_js($files) {
2347 require_once 'lib/jshrink/Minifier.php';
2351 foreach ($files as $js) {
2352 if (!isset($_GET['debug'])) {
2353 $cached_file = CACHE_DIR
. "/js/".basename($js);
2355 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2357 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2359 if ($header && $contents) {
2360 list($htag, $hversion) = explode(":", $header);
2362 if ($htag == "tt-rss" && $hversion == VERSION
) {
2369 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2370 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2374 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2381 function calculate_dep_timestamp() {
2382 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2386 foreach ($files as $file) {
2387 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2393 function T_js_decl($s1, $s2) {
2395 $s1 = preg_replace("/\n/", "", $s1);
2396 $s2 = preg_replace("/\n/", "", $s2);
2398 $s1 = preg_replace("/\"/", "\\\"", $s1);
2399 $s2 = preg_replace("/\"/", "\\\"", $s2);
2401 return "T_messages[\"$s1\"] = \"$s2\";\n";
2405 function init_js_translations() {
2407 print 'var T_messages = new Object();
2410 if (T_messages[msg]) {
2411 return T_messages[msg];
2417 function ngettext(msg1, msg2, n) {
2418 return __((parseInt(n) > 1) ? msg2 : msg1);
2421 $l10n = _get_reader();
2423 for ($i = 0; $i < $l10n->total
; $i++
) {
2424 $orig = $l10n->get_original_string($i);
2425 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2426 $key = explode(chr(0), $orig);
2427 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2428 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2430 $translation = __($orig);
2431 print T_js_decl($orig, $translation);
2436 function get_theme_path($theme) {
2437 if ($theme == "default.php")
2438 return "css/default.css";
2440 $check = "themes/$theme";
2441 if (file_exists($check)) return $check;
2443 $check = "themes.local/$theme";
2444 if (file_exists($check)) return $check;
2447 function theme_valid($theme) {
2448 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2450 if (in_array($theme, $bundled_themes)) return true;
2452 $file = "themes/" . basename($theme);
2454 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2456 if (file_exists($file) && is_readable($file)) {
2457 $fh = fopen($file, "r");
2460 $header = fgets($fh);
2463 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2471 * @SuppressWarnings(unused)
2473 function error_json($code) {
2474 require_once "errors.php";
2476 @$message = $ERRORS[$code];
2478 return json_encode(array("error" =>
2479 array("code" => $code, "message" => $message)));
2483 /*function abs_to_rel_path($dir) {
2484 $tmp = str_replace(dirname(__DIR__), "", $dir);
2486 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2491 function get_upload_error_message($code) {
2494 0 => __('There is no error, the file uploaded with success'),
2495 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2496 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2497 3 => __('The uploaded file was only partially uploaded'),
2498 4 => __('No file was uploaded'),
2499 6 => __('Missing a temporary folder'),
2500 7 => __('Failed to write file to disk.'),
2501 8 => __('A PHP extension stopped the file upload.'),
2504 return $errors[$code];
2507 function base64_img($filename) {
2508 if (file_exists($filename)) {
2509 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2511 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2517 /* this is essentially a wrapper for readfile() which allows plugins to hook
2518 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2520 hook function should return true if request was handled (or at least attempted to)
2522 note that this can be called without user context so the plugin to handle this
2523 should be loaded systemwide in config.php */
2524 function send_local_file($filename) {
2525 if (file_exists($filename)) {
2526 $tmppluginhost = new PluginHost();
2528 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2529 $tmppluginhost->load_data();
2531 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2532 if ($plugin->hook_send_local_file($filename)) return true;
2535 $mimetype = mime_content_type($filename);
2536 header("Content-type: $mimetype");
2538 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2539 header("Last-Modified: $stamp", true);
2541 return readfile($filename);
2547 function check_mysql_tables() {
2550 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2551 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2552 $sth->execute([DB_NAME
]);
2556 while ($line = $sth->fetch()) {
2557 array_push($bad_tables, $line);
2563 function validate_field($string, $allowed, $default = "") {
2564 if (in_array($string, $allowed))
2570 function arr_qmarks($arr) {
2571 return str_repeat('?,', count($arr) - 1) . '?';