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*6);
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; //no-op for PDO
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 if (db_escape_string("testTEST") != "testTEST") {
984 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
987 function file_is_locked($filename) {
988 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
989 if (function_exists('flock')) {
990 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
992 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1003 return true; // consider the file always locked and skip the test
1010 function make_lockfile($filename) {
1011 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1013 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1014 $stat_h = fstat($fp);
1015 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1017 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1018 if ($stat_h["ino"] != $stat_f["ino"] ||
1019 $stat_h["dev"] != $stat_f["dev"]) {
1025 if (function_exists('posix_getpid')) {
1026 fwrite($fp, posix_getpid() . "\n");
1034 function make_stampfile($filename) {
1035 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1037 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1038 fwrite($fp, time() . "\n");
1039 flock($fp, LOCK_UN
);
1047 function sql_random_function() {
1048 if (DB_TYPE
== "mysql") {
1055 function getFeedUnread($feed, $is_cat = false) {
1056 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1059 function checkbox_to_sql_bool($val) {
1060 return ($val == "on") ?
true : false;
1063 function uniqid_short() {
1064 return uniqid(base_convert(rand(), 10, 36));
1067 function make_init_params() {
1070 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1071 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1072 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1073 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1075 $params[strtolower($param)] = (int) get_pref($param);
1078 $params["icons_url"] = ICONS_URL
;
1079 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1080 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1081 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1082 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1083 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1084 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1086 $theme = get_pref( "USER_CSS_THEME", false, false);
1087 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1089 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1091 $params["php_platform"] = PHP_OS
;
1092 $params["php_version"] = PHP_VERSION
;
1094 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1098 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1099 ttrss_feeds WHERE owner_uid = ?");
1100 $sth->execute([$_SESSION['uid']]);
1101 $row = $sth->fetch();
1103 $max_feed_id = $row["mid"];
1104 $num_feeds = $row["nf"];
1106 $params["max_feed_id"] = (int) $max_feed_id;
1107 $params["num_feeds"] = (int) $num_feeds;
1109 $params["hotkeys"] = get_hotkeys_map();
1111 $params["csrf_token"] = $_SESSION["csrf_token"];
1112 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1114 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1116 $params["icon_alert"] = base64_img("images/alert.png");
1117 $params["icon_information"] = base64_img("images/information.png");
1118 $params["icon_cross"] = base64_img("images/cross.png");
1119 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1121 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1126 function get_hotkeys_info() {
1128 __("Navigation") => array(
1129 "next_feed" => __("Open next feed"),
1130 "prev_feed" => __("Open previous feed"),
1131 "next_article" => __("Open next article"),
1132 "prev_article" => __("Open previous article"),
1133 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1134 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1135 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1136 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1137 "search_dialog" => __("Show search dialog")),
1138 __("Article") => array(
1139 "toggle_mark" => __("Toggle starred"),
1140 "toggle_publ" => __("Toggle published"),
1141 "toggle_unread" => __("Toggle unread"),
1142 "edit_tags" => __("Edit tags"),
1143 "open_in_new_window" => __("Open in new window"),
1144 "catchup_below" => __("Mark below as read"),
1145 "catchup_above" => __("Mark above as read"),
1146 "article_scroll_down" => __("Scroll down"),
1147 "article_scroll_up" => __("Scroll up"),
1148 "select_article_cursor" => __("Select article under cursor"),
1149 "email_article" => __("Email article"),
1150 "close_article" => __("Close/collapse article"),
1151 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1152 "toggle_widescreen" => __("Toggle widescreen mode"),
1153 "toggle_embed_original" => __("Toggle embed original")),
1154 __("Article selection") => array(
1155 "select_all" => __("Select all articles"),
1156 "select_unread" => __("Select unread"),
1157 "select_marked" => __("Select starred"),
1158 "select_published" => __("Select published"),
1159 "select_invert" => __("Invert selection"),
1160 "select_none" => __("Deselect everything")),
1161 __("Feed") => array(
1162 "feed_refresh" => __("Refresh current feed"),
1163 "feed_unhide_read" => __("Un/hide read feeds"),
1164 "feed_subscribe" => __("Subscribe to feed"),
1165 "feed_edit" => __("Edit feed"),
1166 "feed_catchup" => __("Mark as read"),
1167 "feed_reverse" => __("Reverse headlines"),
1168 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1169 "feed_debug_update" => __("Debug feed update"),
1170 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1171 "catchup_all" => __("Mark all feeds as read"),
1172 "cat_toggle_collapse" => __("Un/collapse current category"),
1173 "toggle_combined_mode" => __("Toggle combined mode"),
1174 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1175 __("Go to") => array(
1176 "goto_all" => __("All articles"),
1177 "goto_fresh" => __("Fresh"),
1178 "goto_marked" => __("Starred"),
1179 "goto_published" => __("Published"),
1180 "goto_tagcloud" => __("Tag cloud"),
1181 "goto_prefs" => __("Preferences")),
1182 __("Other") => array(
1183 "create_label" => __("Create label"),
1184 "create_filter" => __("Create filter"),
1185 "collapse_sidebar" => __("Un/collapse sidebar"),
1186 "help_dialog" => __("Show help dialog"))
1189 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1190 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1196 function get_hotkeys_map() {
1198 // "navigation" => array(
1201 "n" => "next_article",
1202 "p" => "prev_article",
1203 "(38)|up" => "prev_article",
1204 "(40)|down" => "next_article",
1205 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1206 // "^(40)|Ctrl-down" => "next_article_noscroll",
1207 "(191)|/" => "search_dialog",
1208 // "article" => array(
1209 "s" => "toggle_mark",
1210 "*s" => "toggle_publ",
1211 "u" => "toggle_unread",
1212 "*t" => "edit_tags",
1213 "o" => "open_in_new_window",
1214 "c p" => "catchup_below",
1215 "c n" => "catchup_above",
1216 "*n" => "article_scroll_down",
1217 "*p" => "article_scroll_up",
1218 "*(38)|Shift+up" => "article_scroll_up",
1219 "*(40)|Shift+down" => "article_scroll_down",
1220 "a *w" => "toggle_widescreen",
1221 "a e" => "toggle_embed_original",
1222 "e" => "email_article",
1223 "a q" => "close_article",
1224 // "article_selection" => array(
1225 "a a" => "select_all",
1226 "a u" => "select_unread",
1227 "a *u" => "select_marked",
1228 "a p" => "select_published",
1229 "a i" => "select_invert",
1230 "a n" => "select_none",
1232 "f r" => "feed_refresh",
1233 "f a" => "feed_unhide_read",
1234 "f s" => "feed_subscribe",
1235 "f e" => "feed_edit",
1236 "f q" => "feed_catchup",
1237 "f x" => "feed_reverse",
1238 "f g" => "feed_toggle_vgroup",
1239 "f *d" => "feed_debug_update",
1240 "f *g" => "feed_debug_viewfeed",
1241 "f *c" => "toggle_combined_mode",
1242 "f c" => "toggle_cdm_expanded",
1243 "*q" => "catchup_all",
1244 "x" => "cat_toggle_collapse",
1246 "g a" => "goto_all",
1247 "g f" => "goto_fresh",
1248 "g s" => "goto_marked",
1249 "g p" => "goto_published",
1250 "g t" => "goto_tagcloud",
1251 "g *p" => "goto_prefs",
1252 // "other" => array(
1253 "(9)|Tab" => "select_article_cursor", // tab
1254 "c l" => "create_label",
1255 "c f" => "create_filter",
1256 "c s" => "collapse_sidebar",
1257 "^(191)|Ctrl+/" => "help_dialog",
1260 if (get_pref('COMBINED_DISPLAY_MODE')) {
1261 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1262 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1265 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1266 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1269 $prefixes = array();
1271 foreach (array_keys($hotkeys) as $hotkey) {
1272 $pair = explode(" ", $hotkey, 2);
1274 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1275 array_push($prefixes, $pair[0]);
1279 return array($prefixes, $hotkeys);
1282 function check_for_update() {
1283 if (defined("GIT_VERSION_TIMESTAMP")) {
1284 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1287 $content = json_decode($content, true);
1289 if ($content && isset($content["changeset"])) {
1290 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1291 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1293 return $content["changeset"]["id"];
1302 function make_runtime_info($disable_update_check = false) {
1307 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1308 ttrss_feeds WHERE owner_uid = ?");
1309 $sth->execute([$_SESSION['uid']]);
1310 $row = $sth->fetch();
1312 $max_feed_id = $row['mid'];
1313 $num_feeds = $row['nf'];
1315 $data["max_feed_id"] = (int) $max_feed_id;
1316 $data["num_feeds"] = (int) $num_feeds;
1318 $data['last_article_id'] = Article
::getLastArticleId();
1319 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1321 $data['dep_ts'] = calculate_dep_timestamp();
1322 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1324 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1326 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1327 $update_result = @check_for_update
();
1329 $data["update_result"] = $update_result;
1331 $_SESSION["last_version_check"] = time();
1334 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1336 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1338 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1340 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1343 $stamp_delta = time() - $stamp;
1345 if ($stamp_delta > 1800) {
1349 $_SESSION["daemon_stamp_check"] = time();
1352 $data['daemon_stamp_ok'] = $stamp_check;
1354 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1356 $data['daemon_stamp'] = $stamp_fmt;
1364 function search_to_sql($search, $search_language) {
1366 $keywords = str_getcsv(trim($search), " ");
1367 $query_keywords = array();
1368 $search_words = array();
1369 $search_query_leftover = array();
1373 if ($search_language)
1374 $search_language = $pdo->quote(mb_strtolower($search_language));
1376 $search_language = "english";
1378 foreach ($keywords as $k) {
1379 if (strpos($k, "-") === 0) {
1386 $commandpair = explode(":", mb_strtolower($k), 2);
1388 switch ($commandpair[0]) {
1390 if ($commandpair[1]) {
1391 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
1392 $pdo->quote(mb_strtolower($commandpair[1]))."%'))");
1394 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1395 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1396 array_push($search_words, $k);
1400 if ($commandpair[1]) {
1401 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
1402 $pdo->quote(mb_strtolower($commandpair[1]))."%'))");
1404 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1405 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1406 array_push($search_words, $k);
1410 if ($commandpair[1]) {
1411 if ($commandpair[1] == "true")
1412 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1413 else if ($commandpair[1] == "false")
1414 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1416 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
1417 $pdo->quote(mb_strtolower($commandpair[1]))."%'))");
1419 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1420 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1421 if (!$not) array_push($search_words, $k);
1426 if ($commandpair[1]) {
1427 if ($commandpair[1] == "true")
1428 array_push($query_keywords, "($not (marked = true))");
1430 array_push($query_keywords, "($not (marked = false))");
1432 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1433 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1434 if (!$not) array_push($search_words, $k);
1438 if ($commandpair[1]) {
1439 if ($commandpair[1] == "true")
1440 array_push($query_keywords, "($not (published = true))");
1442 array_push($query_keywords, "($not (published = false))");
1445 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1446 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1447 if (!$not) array_push($search_words, $k);
1451 if ($commandpair[1]) {
1452 if ($commandpair[1] == "true")
1453 array_push($query_keywords, "($not (unread = true))");
1455 array_push($query_keywords, "($not (unread = false))");
1458 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1459 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1460 if (!$not) array_push($search_words, $k);
1464 if (strpos($k, "@") === 0) {
1466 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1467 $orig_ts = strtotime(substr($k, 1));
1468 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1470 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1472 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1475 if (DB_TYPE
== "pgsql") {
1476 $k = mb_strtolower($k);
1477 array_push($search_query_leftover, $not ?
"!$k" : $k);
1479 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1480 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1483 if (!$not) array_push($search_words, $k);
1488 if (count($search_query_leftover) > 0) {
1489 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1491 if (DB_TYPE
== "pgsql") {
1492 array_push($query_keywords,
1493 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
1498 $search_query_part = implode("AND", $query_keywords);
1500 return array($search_query_part, $search_words);
1503 function iframe_whitelisted($entry) {
1504 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1506 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1509 foreach ($whitelist as $w) {
1510 if ($src == $w ||
$src == "www.$w")
1518 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1519 if (!$owner) $owner = $_SESSION["uid"];
1521 $res = trim($str); if (!$res) return '';
1523 $charset_hack = '<head>
1524 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1527 $res = trim($res); if (!$res) return '';
1529 libxml_use_internal_errors(true);
1531 $doc = new DOMDocument();
1532 $doc->loadHTML($charset_hack . $res);
1533 $xpath = new DOMXPath($doc);
1535 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1537 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1539 foreach ($entries as $entry) {
1541 if ($entry->hasAttribute('href')) {
1542 $entry->setAttribute('href',
1543 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1545 $entry->setAttribute('rel', 'noopener noreferrer');
1548 if ($entry->hasAttribute('src')) {
1549 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1550 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1552 if (file_exists($cached_filename)) {
1554 // this is strictly cosmetic
1555 if ($entry->tagName
== 'img') {
1557 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1559 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1565 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1567 if ($entry->hasAttribute('srcset')) {
1568 $entry->removeAttribute('srcset');
1571 if ($entry->hasAttribute('sizes')) {
1572 $entry->removeAttribute('sizes');
1576 $entry->setAttribute('src', $src);
1579 if ($entry->nodeName
== 'img') {
1581 if ($entry->hasAttribute('src')) {
1582 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1584 if (is_prefix_https() && !$is_https_url) {
1586 if ($entry->hasAttribute('srcset')) {
1587 $entry->removeAttribute('srcset');
1590 if ($entry->hasAttribute('sizes')) {
1591 $entry->removeAttribute('sizes');
1596 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1597 $force_remove_images ||
$_SESSION["bw_limit"]) {
1599 $p = $doc->createElement('p');
1601 $a = $doc->createElement('a');
1602 $a->setAttribute('href', $entry->getAttribute('src'));
1604 $a->appendChild(new DOMText($entry->getAttribute('src')));
1605 $a->setAttribute('target', '_blank');
1606 $a->setAttribute('rel', 'noopener noreferrer');
1608 $p->appendChild($a);
1610 $entry->parentNode
->replaceChild($p, $entry);
1614 if (strtolower($entry->nodeName
) == "a") {
1615 $entry->setAttribute("target", "_blank");
1616 $entry->setAttribute("rel", "noopener noreferrer");
1620 $entries = $xpath->query('//iframe');
1621 foreach ($entries as $entry) {
1622 if (!iframe_whitelisted($entry)) {
1623 $entry->setAttribute('sandbox', 'allow-scripts');
1625 if (is_prefix_https()) {
1626 $entry->setAttribute("src",
1627 str_replace("http://", "https://",
1628 $entry->getAttribute("src")));
1633 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1634 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1635 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1636 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1637 'dt', 'em', 'footer', 'figure', 'figcaption',
1638 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1639 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1640 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1641 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1642 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1643 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1645 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1647 $disallowed_attributes = array('id', 'style', 'class');
1649 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1650 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1651 if (is_array($retval)) {
1653 $allowed_elements = $retval[1];
1654 $disallowed_attributes = $retval[2];
1660 $doc->removeChild($doc->firstChild
); //remove doctype
1661 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1663 if ($highlight_words) {
1664 foreach ($highlight_words as $word) {
1666 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1668 $elements = $xpath->query("//*/text()");
1670 foreach ($elements as $child) {
1672 $fragment = $doc->createDocumentFragment();
1673 $text = $child->textContent
;
1675 while (($pos = mb_stripos($text, $word)) !== false) {
1676 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1677 $word = mb_substr($text, $pos, mb_strlen($word));
1678 $highlight = $doc->createElement('span');
1679 $highlight->appendChild(new DomText($word));
1680 $highlight->setAttribute('class', 'highlight');
1681 $fragment->appendChild($highlight);
1682 $text = mb_substr($text, $pos +
mb_strlen($word));
1685 if (!empty($text)) $fragment->appendChild(new DomText($text));
1687 $child->parentNode
->replaceChild($fragment, $child);
1692 $res = $doc->saveHTML();
1694 /* strip everything outside of <body>...</body> */
1696 $res_frag = array();
1697 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1698 return $res_frag[1];
1704 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1705 $xpath = new DOMXPath($doc);
1706 $entries = $xpath->query('//*');
1708 foreach ($entries as $entry) {
1709 if (!in_array($entry->nodeName
, $allowed_elements)) {
1710 $entry->parentNode
->removeChild($entry);
1713 if ($entry->hasAttributes()) {
1714 $attrs_to_remove = array();
1716 foreach ($entry->attributes
as $attr) {
1718 if (strpos($attr->nodeName
, 'on') === 0) {
1719 array_push($attrs_to_remove, $attr);
1722 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1723 array_push($attrs_to_remove, $attr);
1726 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1727 array_push($attrs_to_remove, $attr);
1731 foreach ($attrs_to_remove as $attr) {
1732 $entry->removeAttributeNode($attr);
1740 function trim_array($array) {
1742 array_walk($tmp, 'trim');
1746 function tag_is_valid($tag) {
1747 if ($tag == '') return false;
1748 if (is_numeric($tag)) return false;
1749 if (mb_strlen($tag) > 250) return false;
1751 if (!$tag) return false;
1756 function render_login_form() {
1757 header('Cache-Control: public');
1759 require_once "login_form.php";
1763 function T_sprintf() {
1764 $args = func_get_args();
1765 return vsprintf(__(array_shift($args)), $args);
1768 function print_checkpoint($n, $s) {
1769 $ts = microtime(true);
1770 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1774 function sanitize_tag($tag) {
1777 $tag = mb_strtolower($tag, 'utf-8');
1779 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1781 if (DB_TYPE
== "mysql") {
1782 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1788 function is_server_https() {
1789 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1792 function is_prefix_https() {
1793 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1796 // this returns SELF_URL_PATH sans ending slash
1797 function get_self_url_prefix() {
1798 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1799 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1801 return SELF_URL_PATH
;
1805 function encrypt_password($pass, $salt = '', $mode2 = false) {
1806 if ($salt && $mode2) {
1807 return "MODE2:" . hash('sha256', $salt . $pass);
1809 return "SHA1X:" . sha1("$salt:$pass");
1811 return "SHA1:" . sha1($pass);
1813 } // function encrypt_password
1815 function load_filters($feed_id, $owner_uid) {
1818 $feed_id = (int) $feed_id;
1819 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1822 $null_cat_qpart = "cat_id IS NULL OR";
1824 $null_cat_qpart = "";
1828 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1829 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1830 $sth->execute([$owner_uid]);
1832 $check_cats = array_merge(
1833 Feeds
::getParentCategories($cat_id, $owner_uid),
1836 $check_cats_str = join(",", $check_cats);
1837 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1839 while ($line = $sth->fetch()) {
1840 $filter_id = $line["id"];
1842 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1844 $sth2 = $pdo->prepare("SELECT
1845 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1846 FROM ttrss_filters2_rules AS r,
1847 ttrss_filter_types AS t
1849 (match_on IS NOT NULL OR
1850 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1851 (feed_id IS NULL OR feed_id = ?))) AND
1852 filter_type = t.id AND filter_id = ?");
1853 $sth2->execute([$feed_id, $filter_id]);
1858 while ($rule_line = $sth2->fetch()) {
1859 # print_r($rule_line);
1861 if ($rule_line["match_on"]) {
1862 $match_on = json_decode($rule_line["match_on"], true);
1864 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1867 $rule["reg_exp"] = $rule_line["reg_exp"];
1868 $rule["type"] = $rule_line["type_name"];
1869 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1871 array_push($rules, $rule);
1872 } else if (!$match_any_rule) {
1873 // this filter contains a rule that doesn't match to this feed/category combination
1874 // thus filter has to be rejected
1883 $rule["reg_exp"] = $rule_line["reg_exp"];
1884 $rule["type"] = $rule_line["type_name"];
1885 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1887 array_push($rules, $rule);
1891 if (count($rules) > 0) {
1892 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1893 FROM ttrss_filters2_actions AS a,
1894 ttrss_filter_actions AS t
1896 action_id = t.id AND filter_id = ?");
1897 $sth2->execute([$filter_id]);
1899 while ($action_line = $sth2->fetch()) {
1900 # print_r($action_line);
1903 $action["type"] = $action_line["type_name"];
1904 $action["param"] = $action_line["action_param"];
1906 array_push($actions, $action);
1911 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1912 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1913 $filter["rules"] = $rules;
1914 $filter["actions"] = $actions;
1916 if (count($rules) > 0 && count($actions) > 0) {
1917 array_push($filters, $filter);
1924 function get_score_pic($score) {
1926 return "score_high.png";
1927 } else if ($score > 0) {
1928 return "score_half_high.png";
1929 } else if ($score < -100) {
1930 return "score_low.png";
1931 } else if ($score < 0) {
1932 return "score_half_low.png";
1934 return "score_neutral.png";
1938 function feed_has_icon($id) {
1939 return is_file(ICONS_DIR
. "/$id.ico") && filesize(ICONS_DIR
. "/$id.ico") > 0;
1942 function init_plugins() {
1943 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1948 function add_feed_category($feed_cat, $parent_cat_id = false) {
1950 if (!$feed_cat) return false;
1952 $feed_cat = mb_substr($feed_cat, 0, 250);
1953 if (!$parent_cat_id) $parent_cat_id = null;
1956 $tr_in_progress = false;
1959 $pdo->beginTransaction();
1960 } catch (Exception
$e) {
1961 $tr_in_progress = true;
1964 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1965 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
1966 AND title = :title AND owner_uid = :uid");
1967 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
1969 if (!$sth->fetch()) {
1971 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1973 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
1975 if (!$tr_in_progress) $pdo->commit();
1986 * Fixes incomplete URLs by prepending "http://".
1987 * Also replaces feed:// with http://, and
1988 * prepends a trailing slash if the url is a domain name only.
1990 * @param string $url Possibly incomplete URL
1992 * @return string Fixed URL.
1994 function fix_url($url) {
1996 // support schema-less urls
1997 if (strpos($url, '//') === 0) {
1998 $url = 'https:' . $url;
2001 if (strpos($url, '://') === false) {
2002 $url = 'http://' . $url;
2003 } else if (substr($url, 0, 5) == 'feed:') {
2004 $url = 'http:' . substr($url, 5);
2007 //prepend slash if the URL has no slash in it
2008 // "http://www.example" -> "http://www.example/"
2009 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2013 //convert IDNA hostname to punycode if possible
2014 if (function_exists("idn_to_ascii")) {
2015 $parts = parse_url($url);
2016 if (mb_detect_encoding($parts['host']) != 'ASCII')
2018 $parts['host'] = idn_to_ascii($parts['host']);
2019 $url = build_url($parts);
2023 if ($url != "http:///")
2029 function validate_feed_url($url) {
2030 $parts = parse_url($url);
2032 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2036 /* function save_email_address($email) {
2037 // FIXME: implement persistent storage of emails
2039 if (!$_SESSION['stored_emails'])
2040 $_SESSION['stored_emails'] = array();
2042 if (!in_array($email, $_SESSION['stored_emails']))
2043 array_push($_SESSION['stored_emails'], $email);
2047 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2049 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2051 $is_cat = bool_to_sql_bool($is_cat);
2055 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2056 WHERE feed_id = ? AND is_cat = ?
2057 AND owner_uid = ?");
2058 $sth->execute([$feed_id, $is_cat, $owner_uid]);
2060 if ($row = $sth->fetch()) {
2061 return $row["access_key"];
2063 $key = uniqid_short();
2065 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2066 (access_key, feed_id, is_cat, owner_uid)
2067 VALUES (?, ?, ?, ?)");
2069 $sth->execute([$key, $feed_id, $is_cat, $owner_uid]);
2075 function get_feeds_from_html($url, $content)
2077 $url = fix_url($url);
2078 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2080 libxml_use_internal_errors(true);
2082 $doc = new DOMDocument();
2083 $doc->loadHTML($content);
2084 $xpath = new DOMXPath($doc);
2085 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2086 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2087 $feedUrls = array();
2088 foreach ($entries as $entry) {
2089 if ($entry->hasAttribute('href')) {
2090 $title = $entry->getAttribute('title');
2092 $title = $entry->getAttribute('type');
2094 $feedUrl = rewrite_relative_url(
2095 $baseUrl, $entry->getAttribute('href')
2097 $feedUrls[$feedUrl] = $title;
2103 function is_html($content) {
2104 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2107 function url_is_html($url, $login = false, $pass = false) {
2108 return is_html(fetch_file_contents($url, false, $login, $pass));
2111 function build_url($parts) {
2112 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2115 function cleanup_url_path($path) {
2116 $path = str_replace("/./", "/", $path);
2117 $path = str_replace("//", "/", $path);
2123 * Converts a (possibly) relative URL to a absolute one.
2125 * @param string $url Base URL (i.e. from where the document is)
2126 * @param string $rel_url Possibly relative URL in the document
2128 * @return string Absolute URL
2130 function rewrite_relative_url($url, $rel_url) {
2131 if (strpos($rel_url, "://") !== false) {
2133 } else if (strpos($rel_url, "//") === 0) {
2134 # protocol-relative URL (rare but they exist)
2136 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2137 # magnet:, feed:, etc
2139 } else if (strpos($rel_url, "/") === 0) {
2140 $parts = parse_url($url);
2141 $parts['path'] = $rel_url;
2142 $parts['path'] = cleanup_url_path($parts['path']);
2144 return build_url($parts);
2147 $parts = parse_url($url);
2148 if (!isset($parts['path'])) {
2149 $parts['path'] = '/';
2151 $dir = $parts['path'];
2152 if (substr($dir, -1) !== '/') {
2153 $dir = dirname($parts['path']);
2154 $dir !== '/' && $dir .= '/';
2156 $parts['path'] = $dir . $rel_url;
2157 $parts['path'] = cleanup_url_path($parts['path']);
2159 return build_url($parts);
2163 function cleanup_tags($days = 14, $limit = 1000) {
2165 $days = (int) $days;
2167 if (DB_TYPE
== "pgsql") {
2168 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2169 } else if (DB_TYPE
== "mysql") {
2170 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2177 while ($limit > 0) {
2180 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2181 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2182 WHERE post_int_id = int_id AND $interval_query AND
2183 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2184 $sth->execute([$limit]);
2188 while ($line = $sth->fetch()) {
2189 array_push($ids, $line['id']);
2192 if (count($ids) > 0) {
2193 $ids = join(",", $ids);
2195 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2196 $tags_deleted = $usth->rowCount();
2201 $limit -= $limit_part;
2204 return $tags_deleted;
2207 function print_user_stylesheet() {
2208 $value = get_pref('USER_STYLESHEET');
2211 print "<style type=\"text/css\">";
2212 print str_replace("<br/>", "\n", $value);
2218 function filter_to_sql($filter, $owner_uid) {
2221 if (DB_TYPE
== "pgsql")
2224 $reg_qpart = "REGEXP";
2226 foreach ($filter["rules"] AS $rule) {
2227 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2228 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2229 $rule['reg_exp']) !== FALSE;
2231 if ($regexp_valid) {
2233 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2235 switch ($rule["type"]) {
2237 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2238 $rule['reg_exp'] . "')";
2241 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2242 $rule['reg_exp'] . "')";
2245 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2246 $rule['reg_exp'] . "') OR LOWER(" .
2247 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2250 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2251 $rule['reg_exp'] . "')";
2254 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2255 $rule['reg_exp'] . "')";
2258 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2259 $rule['reg_exp'] . "')";
2263 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2265 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2266 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2269 if (isset($rule["cat_id"])) {
2271 if ($rule["cat_id"] > 0) {
2272 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2273 array_push($children, $rule["cat_id"]);
2275 $children = join(",", $children);
2277 $cat_qpart = "cat_id IN ($children)";
2279 $cat_qpart = "cat_id IS NULL";
2282 $qpart .= " AND $cat_qpart";
2285 $qpart .= " AND feed_id IS NOT NULL";
2287 array_push($query, "($qpart)");
2292 if (count($query) > 0) {
2293 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2295 $fullquery = "(false)";
2298 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2303 if (!function_exists('gzdecode')) {
2304 function gzdecode($string) { // no support for 2nd argument
2305 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2306 base64_encode($string));
2310 function get_random_bytes($length) {
2311 if (function_exists('openssl_random_pseudo_bytes')) {
2312 return openssl_random_pseudo_bytes($length);
2316 for ($i = 0; $i < $length; $i++
)
2317 $output .= chr(mt_rand(0, 255));
2323 function read_stdin() {
2324 $fp = fopen("php://stdin", "r");
2327 $line = trim(fgets($fp));
2335 function implements_interface($class, $interface) {
2336 return in_array($interface, class_implements($class));
2339 function get_minified_js($files) {
2340 require_once 'lib/jshrink/Minifier.php';
2344 foreach ($files as $js) {
2345 if (!isset($_GET['debug'])) {
2346 $cached_file = CACHE_DIR
. "/js/".basename($js).".js";
2348 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2350 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2352 if ($header && $contents) {
2353 list($htag, $hversion) = explode(":", $header);
2355 if ($htag == "tt-rss" && $hversion == VERSION
) {
2362 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js.js"));
2363 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2367 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2374 function calculate_dep_timestamp() {
2375 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2379 foreach ($files as $file) {
2380 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2386 function T_js_decl($s1, $s2) {
2388 $s1 = preg_replace("/\n/", "", $s1);
2389 $s2 = preg_replace("/\n/", "", $s2);
2391 $s1 = preg_replace("/\"/", "\\\"", $s1);
2392 $s2 = preg_replace("/\"/", "\\\"", $s2);
2394 return "T_messages[\"$s1\"] = \"$s2\";\n";
2398 function init_js_translations() {
2400 print 'var T_messages = new Object();
2403 if (T_messages[msg]) {
2404 return T_messages[msg];
2410 function ngettext(msg1, msg2, n) {
2411 return __((parseInt(n) > 1) ? msg2 : msg1);
2414 $l10n = _get_reader();
2416 for ($i = 0; $i < $l10n->total
; $i++
) {
2417 $orig = $l10n->get_original_string($i);
2418 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2419 $key = explode(chr(0), $orig);
2420 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2421 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2423 $translation = __($orig);
2424 print T_js_decl($orig, $translation);
2429 function get_theme_path($theme) {
2430 $check = "themes/$theme";
2431 if (file_exists($check)) return $check;
2433 $check = "themes.local/$theme";
2434 if (file_exists($check)) return $check;
2437 function theme_valid($theme) {
2438 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2440 if (in_array($theme, $bundled_themes)) return true;
2442 $file = "themes/" . basename($theme);
2444 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2446 if (file_exists($file) && is_readable($file)) {
2447 $fh = fopen($file, "r");
2450 $header = fgets($fh);
2453 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2461 * @SuppressWarnings(unused)
2463 function error_json($code) {
2464 require_once "errors.php";
2466 @$message = $ERRORS[$code];
2468 return json_encode(array("error" =>
2469 array("code" => $code, "message" => $message)));
2473 /*function abs_to_rel_path($dir) {
2474 $tmp = str_replace(dirname(__DIR__), "", $dir);
2476 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2481 function get_upload_error_message($code) {
2484 0 => __('There is no error, the file uploaded with success'),
2485 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2486 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2487 3 => __('The uploaded file was only partially uploaded'),
2488 4 => __('No file was uploaded'),
2489 6 => __('Missing a temporary folder'),
2490 7 => __('Failed to write file to disk.'),
2491 8 => __('A PHP extension stopped the file upload.'),
2494 return $errors[$code];
2497 function base64_img($filename) {
2498 if (file_exists($filename)) {
2499 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2501 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2507 /* this is essentially a wrapper for readfile() which allows plugins to hook
2508 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2510 hook function should return true if request was handled (or at least attempted to)
2512 note that this can be called without user context so the plugin to handle this
2513 should be loaded systemwide in config.php */
2514 function send_local_file($filename) {
2515 if (file_exists($filename)) {
2516 $tmppluginhost = new PluginHost();
2518 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2519 $tmppluginhost->load_data();
2521 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2522 if ($plugin->hook_send_local_file($filename)) return true;
2525 $mimetype = mime_content_type($filename);
2526 header("Content-type: $mimetype");
2528 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2529 header("Last-Modified: $stamp", true);
2531 return readfile($filename);
2537 function check_mysql_tables() {
2540 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2541 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2542 $sth->execute([DB_NAME
]);
2546 while ($line = $sth->fetch()) {
2547 array_push($bad_tables, $line);
2553 function arr_qmarks($arr) {
2554 return str_repeat('?,', count($arr) - 1) . '?';