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 function make_password($length = 8) {
731 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
735 while ($i < $length) {
736 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
738 if (!strstr($password, $char)) {
746 // this is called after user is created to initialize default feeds, labels
749 // user preferences are checked on every login, not here
751 function initialize_user($uid) {
755 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
756 values (?, 'Tiny Tiny RSS: Forum',
757 'http://tt-rss.org/forum/rss.php')");
758 $sth->execute([$uid]);
761 function logout_user() {
763 if (isset($_COOKIE[session_name()])) {
764 setcookie(session_name(), '', time()-42000, '/');
768 function validate_csrf($csrf_token) {
769 return $csrf_token == $_SESSION['csrf_token'];
772 function load_user_plugins($owner_uid, $pluginhost = false) {
774 if (!$pluginhost) $pluginhost = PluginHost::getInstance();
776 if ($owner_uid && SCHEMA_VERSION >= 100) {
777 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
779 $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
781 if (get_schema_version() > 100) {
782 $pluginhost->load_data();
787 function login_sequence() {
790 if (SINGLE_USER_MODE) {
792 authenticate_user("admin", null);
794 load_user_plugins($_SESSION["uid"]);
796 if (!validate_session()) $_SESSION["uid"] = false;
798 if (!$_SESSION["uid"]) {
800 if (AUTH_AUTO_LOGIN && authenticate_user(null, null)) {
801 $_SESSION["ref_schema_version"] = get_schema_version(true);
803 authenticate_user(null, null, true);
806 if (!$_SESSION["uid"]) {
808 setcookie(session_name(), '', time()-42000, '/');
815 /* bump login timestamp */
816 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
817 $sth->execute([$_SESSION['uid']]);
819 $_SESSION["last_login_update"] = time();
822 if ($_SESSION["uid"]) {
824 load_user_plugins($_SESSION["uid"]);
828 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
830 (SELECT COUNT(id) FROM ttrss_feeds WHERE
831 ttrss_feeds.id = feed_id) = 0");
833 $sth->execute([$_SESSION['uid']]);
835 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
837 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
838 ttrss_feed_categories.id = feed_id) = 0");
840 $sth->execute([$_SESSION['uid']]);
846 function truncate_string($str, $max_len, $suffix = '…') {
847 if (mb_strlen($str, "utf-8") > $max_len) {
848 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
855 function truncate_middle($str, $max_len, $suffix = '…') {
856 if (strlen($str) > $max_len) {
857 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
863 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
866 $source_tz = new DateTimeZone($source_tz);
867 } catch (Exception $e) {
868 $source_tz = new DateTimeZone('UTC');
872 $dest_tz = new DateTimeZone($dest_tz);
873 } catch (Exception $e) {
874 $dest_tz = new DateTimeZone('UTC');
877 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
878 return $dt->format('U') + $dest_tz->getOffset($dt);
881 function make_local_datetime($timestamp, $long, $owner_uid = false,
882 $no_smart_dt = false, $eta_min = false) {
884 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
885 if (!$timestamp) $timestamp = '1970-01-01 0:00';
890 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
892 $timestamp = substr($timestamp, 0, 19);
894 # We store date in UTC internally
895 $dt = new DateTime($timestamp, $utc_tz);
897 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
899 if ($user_tz_string != 'Automatic') {
902 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
903 } catch (Exception $e) {
907 $tz_offset = $user_tz->getOffset($dt);
909 $tz_offset = (int) -$_SESSION["clientTzOffset"];
912 $user_timestamp = $dt->format('U') + $tz_offset;
915 return smart_date_time($user_timestamp,
916 $tz_offset, $owner_uid, $eta_min);
919 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
921 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
923 return date($format, $user_timestamp);
927 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
928 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
930 if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
931 return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
932 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
933 return date("G:i", $timestamp);
934 } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
935 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
936 return date($format, $timestamp);
938 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
939 return date($format, $timestamp);
943 function sql_bool_to_bool($s) {
944 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
947 function bool_to_sql_bool($s) {
948 return (bool)$s; //no-op for PDO
951 // Session caching removed due to causing wrong redirects to upgrade
952 // script when get_schema_version() is called on an obsolete session
953 // created on a previous schema version.
954 function get_schema_version($nocache = false) {
955 global $schema_version;
959 if (!$schema_version && !$nocache) {
960 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
961 $version = $row["schema_version"];
962 $schema_version = $version;
965 return $schema_version;
969 function sanity_check() {
970 require_once 'errors.php';
974 $schema_version = get_schema_version(true);
976 if ($schema_version != SCHEMA_VERSION) {
980 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
983 function file_is_locked($filename) {
984 if (file_exists(LOCK_DIRECTORY . "/$filename")) {
985 if (function_exists('flock')) {
986 $fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
988 if (flock($fp, LOCK_EX | LOCK_NB)) {
999 return true; // consider the file always locked and skip the test
1006 function make_lockfile($filename) {
1007 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
1009 if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
1010 $stat_h = fstat($fp);
1011 $stat_f = stat(LOCK_DIRECTORY . "/$filename");
1013 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
1014 if ($stat_h["ino"] != $stat_f["ino"] ||
1015 $stat_h["dev"] != $stat_f["dev"]) {
1021 if (function_exists('posix_getpid')) {
1022 fwrite($fp, posix_getpid() . "\n");
1030 function make_stampfile($filename) {
1031 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
1033 if (flock($fp, LOCK_EX | LOCK_NB)) {
1034 fwrite($fp, time() . "\n");
1035 flock($fp, LOCK_UN);
1043 function sql_random_function() {
1044 if (DB_TYPE == "mysql") {
1051 function getFeedUnread($feed, $is_cat = false) {
1052 return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1055 function checkbox_to_sql_bool($val) {
1056 return ($val == "on") ? true : false;
1059 function uniqid_short() {
1060 return uniqid(base_convert(rand(), 10, 36));
1063 function make_init_params() {
1066 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1067 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1068 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1069 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1071 $params[strtolower($param)] = (int) get_pref($param);
1074 $params["icons_url"] = ICONS_URL;
1075 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
1076 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1077 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1078 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1079 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1080 $params["label_base_index"] = (int) LABEL_BASE_INDEX;
1082 $theme = get_pref( "USER_CSS_THEME", false, false);
1083 $params["theme"] = theme_valid("$theme") ? $theme : "";
1085 $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
1087 $params["php_platform"] = PHP_OS;
1088 $params["php_version"] = PHP_VERSION;
1090 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1094 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1095 ttrss_feeds WHERE owner_uid = ?");
1096 $sth->execute([$_SESSION['uid']]);
1097 $row = $sth->fetch();
1099 $max_feed_id = $row["mid"];
1100 $num_feeds = $row["nf"];
1102 $params["max_feed_id"] = (int) $max_feed_id;
1103 $params["num_feeds"] = (int) $num_feeds;
1105 $params["hotkeys"] = get_hotkeys_map();
1107 $params["csrf_token"] = $_SESSION["csrf_token"];
1108 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1110 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
1112 $params["icon_alert"] = base64_img("images/alert.png");
1113 $params["icon_information"] = base64_img("images/information.png");
1114 $params["icon_cross"] = base64_img("images/cross.png");
1115 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1117 $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1122 function get_hotkeys_info() {
1124 __("Navigation") => array(
1125 "next_feed" => __("Open next feed"),
1126 "prev_feed" => __("Open previous feed"),
1127 "next_article" => __("Open next article"),
1128 "prev_article" => __("Open previous article"),
1129 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1130 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1131 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1132 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1133 "search_dialog" => __("Show search dialog")),
1134 __("Article") => array(
1135 "toggle_mark" => __("Toggle starred"),
1136 "toggle_publ" => __("Toggle published"),
1137 "toggle_unread" => __("Toggle unread"),
1138 "edit_tags" => __("Edit tags"),
1139 "open_in_new_window" => __("Open in new window"),
1140 "catchup_below" => __("Mark below as read"),
1141 "catchup_above" => __("Mark above as read"),
1142 "article_scroll_down" => __("Scroll down"),
1143 "article_scroll_up" => __("Scroll up"),
1144 "select_article_cursor" => __("Select article under cursor"),
1145 "email_article" => __("Email article"),
1146 "close_article" => __("Close/collapse article"),
1147 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1148 "toggle_widescreen" => __("Toggle widescreen mode"),
1149 "toggle_embed_original" => __("Toggle embed original")),
1150 __("Article selection") => array(
1151 "select_all" => __("Select all articles"),
1152 "select_unread" => __("Select unread"),
1153 "select_marked" => __("Select starred"),
1154 "select_published" => __("Select published"),
1155 "select_invert" => __("Invert selection"),
1156 "select_none" => __("Deselect everything")),
1157 __("Feed") => array(
1158 "feed_refresh" => __("Refresh current feed"),
1159 "feed_unhide_read" => __("Un/hide read feeds"),
1160 "feed_subscribe" => __("Subscribe to feed"),
1161 "feed_edit" => __("Edit feed"),
1162 "feed_catchup" => __("Mark as read"),
1163 "feed_reverse" => __("Reverse headlines"),
1164 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1165 "feed_debug_update" => __("Debug feed update"),
1166 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1167 "catchup_all" => __("Mark all feeds as read"),
1168 "cat_toggle_collapse" => __("Un/collapse current category"),
1169 "toggle_combined_mode" => __("Toggle combined mode"),
1170 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1171 __("Go to") => array(
1172 "goto_all" => __("All articles"),
1173 "goto_fresh" => __("Fresh"),
1174 "goto_marked" => __("Starred"),
1175 "goto_published" => __("Published"),
1176 "goto_tagcloud" => __("Tag cloud"),
1177 "goto_prefs" => __("Preferences")),
1178 __("Other") => array(
1179 "create_label" => __("Create label"),
1180 "create_filter" => __("Create filter"),
1181 "collapse_sidebar" => __("Un/collapse sidebar"),
1182 "help_dialog" => __("Show help dialog"))
1185 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1186 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1192 function get_hotkeys_map() {
1194 // "navigation" => array(
1197 "n" => "next_article",
1198 "p" => "prev_article",
1199 "(38)|up" => "prev_article",
1200 "(40)|down" => "next_article",
1201 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1202 // "^(40)|Ctrl-down" => "next_article_noscroll",
1203 "(191)|/" => "search_dialog",
1204 // "article" => array(
1205 "s" => "toggle_mark",
1206 "*s" => "toggle_publ",
1207 "u" => "toggle_unread",
1208 "*t" => "edit_tags",
1209 "o" => "open_in_new_window",
1210 "c p" => "catchup_below",
1211 "c n" => "catchup_above",
1212 "*n" => "article_scroll_down",
1213 "*p" => "article_scroll_up",
1214 "*(38)|Shift+up" => "article_scroll_up",
1215 "*(40)|Shift+down" => "article_scroll_down",
1216 "a *w" => "toggle_widescreen",
1217 "a e" => "toggle_embed_original",
1218 "e" => "email_article",
1219 "a q" => "close_article",
1220 // "article_selection" => array(
1221 "a a" => "select_all",
1222 "a u" => "select_unread",
1223 "a *u" => "select_marked",
1224 "a p" => "select_published",
1225 "a i" => "select_invert",
1226 "a n" => "select_none",
1228 "f r" => "feed_refresh",
1229 "f a" => "feed_unhide_read",
1230 "f s" => "feed_subscribe",
1231 "f e" => "feed_edit",
1232 "f q" => "feed_catchup",
1233 "f x" => "feed_reverse",
1234 "f g" => "feed_toggle_vgroup",
1235 "f *d" => "feed_debug_update",
1236 "f *g" => "feed_debug_viewfeed",
1237 "f *c" => "toggle_combined_mode",
1238 "f c" => "toggle_cdm_expanded",
1239 "*q" => "catchup_all",
1240 "x" => "cat_toggle_collapse",
1242 "g a" => "goto_all",
1243 "g f" => "goto_fresh",
1244 "g s" => "goto_marked",
1245 "g p" => "goto_published",
1246 "g t" => "goto_tagcloud",
1247 "g *p" => "goto_prefs",
1248 // "other" => array(
1249 "(9)|Tab" => "select_article_cursor", // tab
1250 "c l" => "create_label",
1251 "c f" => "create_filter",
1252 "c s" => "collapse_sidebar",
1253 "^(191)|Ctrl+/" => "help_dialog",
1256 if (get_pref('COMBINED_DISPLAY_MODE')) {
1257 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1258 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1261 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1262 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1265 $prefixes = array();
1267 foreach (array_keys($hotkeys) as $hotkey) {
1268 $pair = explode(" ", $hotkey, 2);
1270 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1271 array_push($prefixes, $pair[0]);
1275 return array($prefixes, $hotkeys);
1278 function check_for_update() {
1279 if (defined("GIT_VERSION_TIMESTAMP")) {
1280 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1283 $content = json_decode($content, true);
1285 if ($content && isset($content["changeset"])) {
1286 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
1287 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
1289 return $content["changeset"]["id"];
1298 function make_runtime_info($disable_update_check = false) {
1303 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1304 ttrss_feeds WHERE owner_uid = ?");
1305 $sth->execute([$_SESSION['uid']]);
1306 $row = $sth->fetch();
1308 $max_feed_id = $row['mid'];
1309 $num_feeds = $row['nf'];
1311 $data["max_feed_id"] = (int) $max_feed_id;
1312 $data["num_feeds"] = (int) $num_feeds;
1314 $data['last_article_id'] = Article::getLastArticleId();
1315 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1317 $data['dep_ts'] = calculate_dep_timestamp();
1318 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1320 $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1322 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1323 $update_result = @check_for_update();
1325 $data["update_result"] = $update_result;
1327 $_SESSION["last_version_check"] = time();
1330 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1332 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1334 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1336 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1339 $stamp_delta = time() - $stamp;
1341 if ($stamp_delta > 1800) {
1345 $_SESSION["daemon_stamp_check"] = time();
1348 $data['daemon_stamp_ok'] = $stamp_check;
1350 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1352 $data['daemon_stamp'] = $stamp_fmt;
1360 function search_to_sql($search, $search_language) {
1362 $keywords = str_getcsv(trim($search), " ");
1363 $query_keywords = array();
1364 $search_words = array();
1365 $search_query_leftover = array();
1369 if ($search_language)
1370 $search_language = $pdo->quote(mb_strtolower($search_language));
1372 $search_language = "english";
1374 foreach ($keywords as $k) {
1375 if (strpos($k, "-") === 0) {
1382 $commandpair = explode(":", mb_strtolower($k), 2);
1384 switch ($commandpair[0]) {
1386 if ($commandpair[1]) {
1387 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1388 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1390 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1391 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1392 array_push($search_words, $k);
1396 if ($commandpair[1]) {
1397 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1398 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1400 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1401 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1402 array_push($search_words, $k);
1406 if ($commandpair[1]) {
1407 if ($commandpair[1] == "true")
1408 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1409 else if ($commandpair[1] == "false")
1410 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1412 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1413 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1415 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1416 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1417 if (!$not) array_push($search_words, $k);
1422 if ($commandpair[1]) {
1423 if ($commandpair[1] == "true")
1424 array_push($query_keywords, "($not (marked = true))");
1426 array_push($query_keywords, "($not (marked = false))");
1428 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1429 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1430 if (!$not) array_push($search_words, $k);
1434 if ($commandpair[1]) {
1435 if ($commandpair[1] == "true")
1436 array_push($query_keywords, "($not (published = true))");
1438 array_push($query_keywords, "($not (published = false))");
1441 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1442 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1443 if (!$not) array_push($search_words, $k);
1447 if ($commandpair[1]) {
1448 if ($commandpair[1] == "true")
1449 array_push($query_keywords, "($not (unread = true))");
1451 array_push($query_keywords, "($not (unread = false))");
1454 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1455 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1456 if (!$not) array_push($search_words, $k);
1460 if (strpos($k, "@") === 0) {
1462 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1463 $orig_ts = strtotime(substr($k, 1));
1464 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1466 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1468 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
1471 if (DB_TYPE == "pgsql") {
1472 $k = mb_strtolower($k);
1473 array_push($search_query_leftover, $not ? "!$k" : $k);
1475 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1476 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1479 if (!$not) array_push($search_words, $k);
1484 if (count($search_query_leftover) > 0) {
1485 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1487 if (DB_TYPE == "pgsql") {
1488 array_push($query_keywords,
1489 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1494 $search_query_part = implode("AND", $query_keywords);
1496 return array($search_query_part, $search_words);
1499 function iframe_whitelisted($entry) {
1500 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1502 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1505 foreach ($whitelist as $w) {
1506 if ($src == $w || $src == "www.$w")
1514 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1515 if (!$owner) $owner = $_SESSION["uid"];
1517 $res = trim($str); if (!$res) return '';
1519 $charset_hack = '<head>
1520 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1523 $res = trim($res); if (!$res) return '';
1525 libxml_use_internal_errors(true);
1527 $doc = new DOMDocument();
1528 $doc->loadHTML($charset_hack . $res);
1529 $xpath = new DOMXPath($doc);
1531 $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
1533 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1535 foreach ($entries as $entry) {
1537 if ($entry->hasAttribute('href')) {
1538 $entry->setAttribute('href',
1539 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1541 $entry->setAttribute('rel', 'noopener noreferrer');
1544 if ($entry->hasAttribute('src')) {
1545 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1546 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
1548 if (file_exists($cached_filename)) {
1550 // this is strictly cosmetic
1551 if ($entry->tagName == 'img') {
1553 } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
1555 } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
1561 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1563 if ($entry->hasAttribute('srcset')) {
1564 $entry->removeAttribute('srcset');
1567 if ($entry->hasAttribute('sizes')) {
1568 $entry->removeAttribute('sizes');
1572 $entry->setAttribute('src', $src);
1575 if ($entry->nodeName == 'img') {
1577 if ($entry->hasAttribute('src')) {
1578 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1580 if (is_prefix_https() && !$is_https_url) {
1582 if ($entry->hasAttribute('srcset')) {
1583 $entry->removeAttribute('srcset');
1586 if ($entry->hasAttribute('sizes')) {
1587 $entry->removeAttribute('sizes');
1592 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1593 $force_remove_images || $_SESSION["bw_limit"]) {
1595 $p = $doc->createElement('p');
1597 $a = $doc->createElement('a');
1598 $a->setAttribute('href', $entry->getAttribute('src'));
1600 $a->appendChild(new DOMText($entry->getAttribute('src')));
1601 $a->setAttribute('target', '_blank');
1602 $a->setAttribute('rel', 'noopener noreferrer');
1604 $p->appendChild($a);
1606 $entry->parentNode->replaceChild($p, $entry);
1610 if (strtolower($entry->nodeName) == "a") {
1611 $entry->setAttribute("target", "_blank");
1612 $entry->setAttribute("rel", "noopener noreferrer");
1616 $entries = $xpath->query('//iframe');
1617 foreach ($entries as $entry) {
1618 if (!iframe_whitelisted($entry)) {
1619 $entry->setAttribute('sandbox', 'allow-scripts');
1621 if (is_prefix_https()) {
1622 $entry->setAttribute("src",
1623 str_replace("http://", "https://",
1624 $entry->getAttribute("src")));
1629 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1630 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1631 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1632 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1633 'dt', 'em', 'footer', 'figure', 'figcaption',
1634 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1635 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1636 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1637 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1638 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1639 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1641 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1643 $disallowed_attributes = array('id', 'style', 'class');
1645 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1646 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1647 if (is_array($retval)) {
1649 $allowed_elements = $retval[1];
1650 $disallowed_attributes = $retval[2];
1656 $doc->removeChild($doc->firstChild); //remove doctype
1657 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1659 if ($highlight_words) {
1660 foreach ($highlight_words as $word) {
1662 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1664 $elements = $xpath->query("//*/text()");
1666 foreach ($elements as $child) {
1668 $fragment = $doc->createDocumentFragment();
1669 $text = $child->textContent;
1671 while (($pos = mb_stripos($text, $word)) !== false) {
1672 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1673 $word = mb_substr($text, $pos, mb_strlen($word));
1674 $highlight = $doc->createElement('span');
1675 $highlight->appendChild(new DomText($word));
1676 $highlight->setAttribute('class', 'highlight');
1677 $fragment->appendChild($highlight);
1678 $text = mb_substr($text, $pos + mb_strlen($word));
1681 if (!empty($text)) $fragment->appendChild(new DomText($text));
1683 $child->parentNode->replaceChild($fragment, $child);
1688 $res = $doc->saveHTML();
1690 /* strip everything outside of <body>...</body> */
1692 $res_frag = array();
1693 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1694 return $res_frag[1];
1700 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1701 $xpath = new DOMXPath($doc);
1702 $entries = $xpath->query('//*');
1704 foreach ($entries as $entry) {
1705 if (!in_array($entry->nodeName, $allowed_elements)) {
1706 $entry->parentNode->removeChild($entry);
1709 if ($entry->hasAttributes()) {
1710 $attrs_to_remove = array();
1712 foreach ($entry->attributes as $attr) {
1714 if (strpos($attr->nodeName, 'on') === 0) {
1715 array_push($attrs_to_remove, $attr);
1718 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1719 array_push($attrs_to_remove, $attr);
1722 if (in_array($attr->nodeName, $disallowed_attributes)) {
1723 array_push($attrs_to_remove, $attr);
1727 foreach ($attrs_to_remove as $attr) {
1728 $entry->removeAttributeNode($attr);
1736 function trim_array($array) {
1738 array_walk($tmp, 'trim');
1742 function tag_is_valid($tag) {
1743 if ($tag == '') return false;
1744 if (is_numeric($tag)) return false;
1745 if (mb_strlen($tag) > 250) return false;
1747 if (!$tag) return false;
1752 function render_login_form() {
1753 header('Cache-Control: public');
1755 require_once "login_form.php";
1759 function T_sprintf() {
1760 $args = func_get_args();
1761 return vsprintf(__(array_shift($args)), $args);
1764 function print_checkpoint($n, $s) {
1765 $ts = microtime(true);
1766 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1770 function sanitize_tag($tag) {
1773 $tag = mb_strtolower($tag, 'utf-8');
1775 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1777 if (DB_TYPE == "mysql") {
1778 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1784 function is_server_https() {
1785 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1788 function is_prefix_https() {
1789 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1792 // this returns SELF_URL_PATH sans ending slash
1793 function get_self_url_prefix() {
1794 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1795 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1797 return SELF_URL_PATH;
1801 function encrypt_password($pass, $salt = '', $mode2 = false) {
1802 if ($salt && $mode2) {
1803 return "MODE2:" . hash('sha256', $salt . $pass);
1805 return "SHA1X:" . sha1("$salt:$pass");
1807 return "SHA1:" . sha1($pass);
1809 } // function encrypt_password
1811 function load_filters($feed_id, $owner_uid) {
1814 $feed_id = (int) $feed_id;
1815 $cat_id = (int)Feeds::getFeedCategory($feed_id);
1818 $null_cat_qpart = "cat_id IS NULL OR";
1820 $null_cat_qpart = "";
1824 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1825 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1826 $sth->execute([$owner_uid]);
1828 $check_cats = array_merge(
1829 Feeds::getParentCategories($cat_id, $owner_uid),
1832 $check_cats_str = join(",", $check_cats);
1833 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1835 while ($line = $sth->fetch()) {
1836 $filter_id = $line["id"];
1838 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1840 $sth2 = $pdo->prepare("SELECT
1841 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1842 FROM ttrss_filters2_rules AS r,
1843 ttrss_filter_types AS t
1845 (match_on IS NOT NULL OR
1846 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1847 (feed_id IS NULL OR feed_id = ?))) AND
1848 filter_type = t.id AND filter_id = ?");
1849 $sth2->execute([$feed_id, $filter_id]);
1854 while ($rule_line = $sth2->fetch()) {
1855 # print_r($rule_line);
1857 if ($rule_line["match_on"]) {
1858 $match_on = json_decode($rule_line["match_on"], true);
1860 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1863 $rule["reg_exp"] = $rule_line["reg_exp"];
1864 $rule["type"] = $rule_line["type_name"];
1865 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1867 array_push($rules, $rule);
1868 } else if (!$match_any_rule) {
1869 // this filter contains a rule that doesn't match to this feed/category combination
1870 // thus filter has to be rejected
1879 $rule["reg_exp"] = $rule_line["reg_exp"];
1880 $rule["type"] = $rule_line["type_name"];
1881 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1883 array_push($rules, $rule);
1887 if (count($rules) > 0) {
1888 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1889 FROM ttrss_filters2_actions AS a,
1890 ttrss_filter_actions AS t
1892 action_id = t.id AND filter_id = ?");
1893 $sth2->execute([$filter_id]);
1895 while ($action_line = $sth2->fetch()) {
1896 # print_r($action_line);
1899 $action["type"] = $action_line["type_name"];
1900 $action["param"] = $action_line["action_param"];
1902 array_push($actions, $action);
1907 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1908 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1909 $filter["rules"] = $rules;
1910 $filter["actions"] = $actions;
1912 if (count($rules) > 0 && count($actions) > 0) {
1913 array_push($filters, $filter);
1920 function get_score_pic($score) {
1922 return "score_high.png";
1923 } else if ($score > 0) {
1924 return "score_half_high.png";
1925 } else if ($score < -100) {
1926 return "score_low.png";
1927 } else if ($score < 0) {
1928 return "score_half_low.png";
1930 return "score_neutral.png";
1934 function feed_has_icon($id) {
1935 return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0;
1938 function init_plugins() {
1939 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1944 function add_feed_category($feed_cat, $parent_cat_id = false) {
1946 if (!$feed_cat) return false;
1948 $feed_cat = mb_substr($feed_cat, 0, 250);
1949 if (!$parent_cat_id) $parent_cat_id = null;
1952 $tr_in_progress = false;
1955 $pdo->beginTransaction();
1956 } catch (Exception $e) {
1957 $tr_in_progress = true;
1960 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1961 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
1962 AND title = :title AND owner_uid = :uid");
1963 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
1965 if (!$sth->fetch()) {
1967 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1969 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
1971 if (!$tr_in_progress) $pdo->commit();
1982 * Fixes incomplete URLs by prepending "http://".
1983 * Also replaces feed:// with http://, and
1984 * prepends a trailing slash if the url is a domain name only.
1986 * @param string $url Possibly incomplete URL
1988 * @return string Fixed URL.
1990 function fix_url($url) {
1992 // support schema-less urls
1993 if (strpos($url, '//') === 0) {
1994 $url = 'https:' . $url;
1997 if (strpos($url, '://') === false) {
1998 $url = 'http://' . $url;
1999 } else if (substr($url, 0, 5) == 'feed:') {
2000 $url = 'http:' . substr($url, 5);
2003 //prepend slash if the URL has no slash in it
2004 // "http://www.example" -> "http://www.example/"
2005 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2009 //convert IDNA hostname to punycode if possible
2010 if (function_exists("idn_to_ascii")) {
2011 $parts = parse_url($url);
2012 if (mb_detect_encoding($parts['host']) != 'ASCII')
2014 $parts['host'] = idn_to_ascii($parts['host']);
2015 $url = build_url($parts);
2019 if ($url != "http:///")
2025 function validate_feed_url($url) {
2026 $parts = parse_url($url);
2028 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2032 /* function save_email_address($email) {
2033 // FIXME: implement persistent storage of emails
2035 if (!$_SESSION['stored_emails'])
2036 $_SESSION['stored_emails'] = array();
2038 if (!in_array($email, $_SESSION['stored_emails']))
2039 array_push($_SESSION['stored_emails'], $email);
2043 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2045 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2047 $is_cat = bool_to_sql_bool($is_cat);
2051 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2052 WHERE feed_id = ? AND is_cat = ?
2053 AND owner_uid = ?");
2054 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2056 if ($row = $sth->fetch()) {
2057 return $row["access_key"];
2059 $key = uniqid_short();
2061 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2062 (access_key, feed_id, is_cat, owner_uid)
2063 VALUES (?, ?, ?, ?)");
2065 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2071 function get_feeds_from_html($url, $content)
2073 $url = fix_url($url);
2074 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2076 libxml_use_internal_errors(true);
2078 $doc = new DOMDocument();
2079 $doc->loadHTML($content);
2080 $xpath = new DOMXPath($doc);
2081 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2082 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2083 $feedUrls = array();
2084 foreach ($entries as $entry) {
2085 if ($entry->hasAttribute('href')) {
2086 $title = $entry->getAttribute('title');
2088 $title = $entry->getAttribute('type');
2090 $feedUrl = rewrite_relative_url(
2091 $baseUrl, $entry->getAttribute('href')
2093 $feedUrls[$feedUrl] = $title;
2099 function is_html($content) {
2100 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2103 function url_is_html($url, $login = false, $pass = false) {
2104 return is_html(fetch_file_contents($url, false, $login, $pass));
2107 function build_url($parts) {
2108 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2111 function cleanup_url_path($path) {
2112 $path = str_replace("/./", "/", $path);
2113 $path = str_replace("//", "/", $path);
2119 * Converts a (possibly) relative URL to a absolute one.
2121 * @param string $url Base URL (i.e. from where the document is)
2122 * @param string $rel_url Possibly relative URL in the document
2124 * @return string Absolute URL
2126 function rewrite_relative_url($url, $rel_url) {
2127 if (strpos($rel_url, "://") !== false) {
2129 } else if (strpos($rel_url, "//") === 0) {
2130 # protocol-relative URL (rare but they exist)
2132 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2133 # magnet:, feed:, etc
2135 } else if (strpos($rel_url, "/") === 0) {
2136 $parts = parse_url($url);
2137 $parts['path'] = $rel_url;
2138 $parts['path'] = cleanup_url_path($parts['path']);
2140 return build_url($parts);
2143 $parts = parse_url($url);
2144 if (!isset($parts['path'])) {
2145 $parts['path'] = '/';
2147 $dir = $parts['path'];
2148 if (substr($dir, -1) !== '/') {
2149 $dir = dirname($parts['path']);
2150 $dir !== '/' && $dir .= '/';
2152 $parts['path'] = $dir . $rel_url;
2153 $parts['path'] = cleanup_url_path($parts['path']);
2155 return build_url($parts);
2159 function cleanup_tags($days = 14, $limit = 1000) {
2161 $days = (int) $days;
2163 if (DB_TYPE == "pgsql") {
2164 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2165 } else if (DB_TYPE == "mysql") {
2166 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2173 while ($limit > 0) {
2176 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2177 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2178 WHERE post_int_id = int_id AND $interval_query AND
2179 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2180 $sth->execute([$limit]);
2184 while ($line = $sth->fetch()) {
2185 array_push($ids, $line['id']);
2188 if (count($ids) > 0) {
2189 $ids = join(",", $ids);
2191 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2192 $tags_deleted = $usth->rowCount();
2197 $limit -= $limit_part;
2200 return $tags_deleted;
2203 function print_user_stylesheet() {
2204 $value = get_pref('USER_STYLESHEET');
2207 print "<style type=\"text/css\">";
2208 print str_replace("<br/>", "\n", $value);
2214 function filter_to_sql($filter, $owner_uid) {
2219 if (DB_TYPE == "pgsql")
2222 $reg_qpart = "REGEXP";
2224 foreach ($filter["rules"] AS $rule) {
2225 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2226 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2227 $rule['reg_exp']) !== FALSE;
2229 if ($regexp_valid) {
2231 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2233 switch ($rule["type"]) {
2235 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2236 $rule['reg_exp'] . "')";
2239 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2240 $rule['reg_exp'] . "')";
2243 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2244 $rule['reg_exp'] . "') OR LOWER(" .
2245 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2248 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2249 $rule['reg_exp'] . "')";
2252 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2253 $rule['reg_exp'] . "')";
2256 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2257 $rule['reg_exp'] . "')";
2261 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2263 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2264 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2267 if (isset($rule["cat_id"])) {
2269 if ($rule["cat_id"] > 0) {
2270 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2271 array_push($children, $rule["cat_id"]);
2273 $children = join(",", $children);
2275 $cat_qpart = "cat_id IN ($children)";
2277 $cat_qpart = "cat_id IS NULL";
2280 $qpart .= " AND $cat_qpart";
2283 $qpart .= " AND feed_id IS NOT NULL";
2285 array_push($query, "($qpart)");
2290 if (count($query) > 0) {
2291 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2293 $fullquery = "(false)";
2296 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2301 if (!function_exists('gzdecode')) {
2302 function gzdecode($string) { // no support for 2nd argument
2303 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2304 base64_encode($string));
2308 function get_random_bytes($length) {
2309 if (function_exists('openssl_random_pseudo_bytes')) {
2310 return openssl_random_pseudo_bytes($length);
2314 for ($i = 0; $i < $length; $i++)
2315 $output .= chr(mt_rand(0, 255));
2321 function read_stdin() {
2322 $fp = fopen("php://stdin", "r");
2325 $line = trim(fgets($fp));
2333 function implements_interface($class, $interface) {
2334 return in_array($interface, class_implements($class));
2337 function get_minified_js($files) {
2338 require_once 'lib/jshrink/Minifier.php';
2342 foreach ($files as $js) {
2343 if (!isset($_GET['debug'])) {
2344 $cached_file = CACHE_DIR . "/js/".basename($js).".js";
2346 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2348 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2350 if ($header && $contents) {
2351 list($htag, $hversion) = explode(":", $header);
2353 if ($htag == "tt-rss" && $hversion == VERSION) {
2360 $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js"));
2361 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2365 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2372 function calculate_dep_timestamp() {
2373 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2377 foreach ($files as $file) {
2378 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2384 function T_js_decl($s1, $s2) {
2386 $s1 = preg_replace("/\n/", "", $s1);
2387 $s2 = preg_replace("/\n/", "", $s2);
2389 $s1 = preg_replace("/\"/", "\\\"", $s1);
2390 $s2 = preg_replace("/\"/", "\\\"", $s2);
2392 return "T_messages[\"$s1\"] = \"$s2\";\n";
2396 function init_js_translations() {
2398 print 'var T_messages = new Object();
2401 if (T_messages[msg]) {
2402 return T_messages[msg];
2408 function ngettext(msg1, msg2, n) {
2409 return __((parseInt(n) > 1) ? msg2 : msg1);
2412 $l10n = _get_reader();
2414 for ($i = 0; $i < $l10n->total; $i++) {
2415 $orig = $l10n->get_original_string($i);
2416 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2417 $key = explode(chr(0), $orig);
2418 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2419 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2421 $translation = __($orig);
2422 print T_js_decl($orig, $translation);
2427 function get_theme_path($theme) {
2428 $check = "themes/$theme";
2429 if (file_exists($check)) return $check;
2431 $check = "themes.local/$theme";
2432 if (file_exists($check)) return $check;
2435 function theme_valid($theme) {
2436 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2438 if (in_array($theme, $bundled_themes)) return true;
2440 $file = "themes/" . basename($theme);
2442 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2444 if (file_exists($file) && is_readable($file)) {
2445 $fh = fopen($file, "r");
2448 $header = fgets($fh);
2451 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2459 * @SuppressWarnings(unused)
2461 function error_json($code) {
2462 require_once "errors.php";
2464 @$message = $ERRORS[$code];
2466 return json_encode(array("error" =>
2467 array("code" => $code, "message" => $message)));
2471 /*function abs_to_rel_path($dir) {
2472 $tmp = str_replace(dirname(__DIR__), "", $dir);
2474 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2479 function get_upload_error_message($code) {
2482 0 => __('There is no error, the file uploaded with success'),
2483 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2484 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2485 3 => __('The uploaded file was only partially uploaded'),
2486 4 => __('No file was uploaded'),
2487 6 => __('Missing a temporary folder'),
2488 7 => __('Failed to write file to disk.'),
2489 8 => __('A PHP extension stopped the file upload.'),
2492 return $errors[$code];
2495 function base64_img($filename) {
2496 if (file_exists($filename)) {
2497 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2499 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2505 /* this is essentially a wrapper for readfile() which allows plugins to hook
2506 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2508 hook function should return true if request was handled (or at least attempted to)
2510 note that this can be called without user context so the plugin to handle this
2511 should be loaded systemwide in config.php */
2512 function send_local_file($filename) {
2513 if (file_exists($filename)) {
2514 $tmppluginhost = new PluginHost();
2516 $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
2517 $tmppluginhost->load_data();
2519 foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
2520 if ($plugin->hook_send_local_file($filename)) return true;
2523 $mimetype = mime_content_type($filename);
2524 header("Content-type: $mimetype");
2526 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2527 header("Last-Modified: $stamp", true);
2529 return readfile($filename);
2535 function check_mysql_tables() {
2538 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2539 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2540 $sth->execute([DB_NAME]);
2544 while ($line = $sth->fetch()) {
2545 array_push($bad_tables, $line);
2551 function arr_qmarks($arr) {
2552 return str_repeat('?,', count($arr) - 1) . '?';