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") ?
1 : 0;
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["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1081 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1083 $theme = get_pref( "USER_CSS_THEME", false, false);
1084 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1086 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1088 $params["php_platform"] = PHP_OS
;
1089 $params["php_version"] = PHP_VERSION
;
1091 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1095 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1096 ttrss_feeds WHERE owner_uid = ?");
1097 $sth->execute([$_SESSION['uid']]);
1098 $row = $sth->fetch();
1100 $max_feed_id = $row["mid"];
1101 $num_feeds = $row["nf"];
1103 $params["max_feed_id"] = (int) $max_feed_id;
1104 $params["num_feeds"] = (int) $num_feeds;
1106 $params["hotkeys"] = get_hotkeys_map();
1108 $params["csrf_token"] = $_SESSION["csrf_token"];
1109 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1111 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1113 $params["icon_alert"] = base64_img("images/alert.png");
1114 $params["icon_information"] = base64_img("images/information.png");
1115 $params["icon_cross"] = base64_img("images/cross.png");
1116 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1118 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1123 function get_hotkeys_info() {
1125 __("Navigation") => array(
1126 "next_feed" => __("Open next feed"),
1127 "prev_feed" => __("Open previous feed"),
1128 "next_article" => __("Open next article"),
1129 "prev_article" => __("Open previous article"),
1130 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1131 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1132 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1133 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1134 "search_dialog" => __("Show search dialog")),
1135 __("Article") => array(
1136 "toggle_mark" => __("Toggle starred"),
1137 "toggle_publ" => __("Toggle published"),
1138 "toggle_unread" => __("Toggle unread"),
1139 "edit_tags" => __("Edit tags"),
1140 "open_in_new_window" => __("Open in new window"),
1141 "catchup_below" => __("Mark below as read"),
1142 "catchup_above" => __("Mark above as read"),
1143 "article_scroll_down" => __("Scroll down"),
1144 "article_scroll_up" => __("Scroll up"),
1145 "select_article_cursor" => __("Select article under cursor"),
1146 "email_article" => __("Email article"),
1147 "close_article" => __("Close/collapse article"),
1148 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1149 "toggle_widescreen" => __("Toggle widescreen mode"),
1150 "toggle_embed_original" => __("Toggle embed original")),
1151 __("Article selection") => array(
1152 "select_all" => __("Select all articles"),
1153 "select_unread" => __("Select unread"),
1154 "select_marked" => __("Select starred"),
1155 "select_published" => __("Select published"),
1156 "select_invert" => __("Invert selection"),
1157 "select_none" => __("Deselect everything")),
1158 __("Feed") => array(
1159 "feed_refresh" => __("Refresh current feed"),
1160 "feed_unhide_read" => __("Un/hide read feeds"),
1161 "feed_subscribe" => __("Subscribe to feed"),
1162 "feed_edit" => __("Edit feed"),
1163 "feed_catchup" => __("Mark as read"),
1164 "feed_reverse" => __("Reverse headlines"),
1165 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1166 "feed_debug_update" => __("Debug feed update"),
1167 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1168 "catchup_all" => __("Mark all feeds as read"),
1169 "cat_toggle_collapse" => __("Un/collapse current category"),
1170 "toggle_combined_mode" => __("Toggle combined mode"),
1171 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1172 __("Go to") => array(
1173 "goto_all" => __("All articles"),
1174 "goto_fresh" => __("Fresh"),
1175 "goto_marked" => __("Starred"),
1176 "goto_published" => __("Published"),
1177 "goto_tagcloud" => __("Tag cloud"),
1178 "goto_prefs" => __("Preferences")),
1179 __("Other") => array(
1180 "create_label" => __("Create label"),
1181 "create_filter" => __("Create filter"),
1182 "collapse_sidebar" => __("Un/collapse sidebar"),
1183 "help_dialog" => __("Show help dialog"))
1186 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1187 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1193 function get_hotkeys_map() {
1195 // "navigation" => array(
1198 "n" => "next_article",
1199 "p" => "prev_article",
1200 "(38)|up" => "prev_article",
1201 "(40)|down" => "next_article",
1202 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1203 // "^(40)|Ctrl-down" => "next_article_noscroll",
1204 "(191)|/" => "search_dialog",
1205 // "article" => array(
1206 "s" => "toggle_mark",
1207 "*s" => "toggle_publ",
1208 "u" => "toggle_unread",
1209 "*t" => "edit_tags",
1210 "o" => "open_in_new_window",
1211 "c p" => "catchup_below",
1212 "c n" => "catchup_above",
1213 "*n" => "article_scroll_down",
1214 "*p" => "article_scroll_up",
1215 "*(38)|Shift+up" => "article_scroll_up",
1216 "*(40)|Shift+down" => "article_scroll_down",
1217 "a *w" => "toggle_widescreen",
1218 "a e" => "toggle_embed_original",
1219 "e" => "email_article",
1220 "a q" => "close_article",
1221 // "article_selection" => array(
1222 "a a" => "select_all",
1223 "a u" => "select_unread",
1224 "a *u" => "select_marked",
1225 "a p" => "select_published",
1226 "a i" => "select_invert",
1227 "a n" => "select_none",
1229 "f r" => "feed_refresh",
1230 "f a" => "feed_unhide_read",
1231 "f s" => "feed_subscribe",
1232 "f e" => "feed_edit",
1233 "f q" => "feed_catchup",
1234 "f x" => "feed_reverse",
1235 "f g" => "feed_toggle_vgroup",
1236 "f *d" => "feed_debug_update",
1237 "f *g" => "feed_debug_viewfeed",
1238 "f *c" => "toggle_combined_mode",
1239 "f c" => "toggle_cdm_expanded",
1240 "*q" => "catchup_all",
1241 "x" => "cat_toggle_collapse",
1243 "g a" => "goto_all",
1244 "g f" => "goto_fresh",
1245 "g s" => "goto_marked",
1246 "g p" => "goto_published",
1247 "g t" => "goto_tagcloud",
1248 "g *p" => "goto_prefs",
1249 // "other" => array(
1250 "(9)|Tab" => "select_article_cursor", // tab
1251 "c l" => "create_label",
1252 "c f" => "create_filter",
1253 "c s" => "collapse_sidebar",
1254 "^(191)|Ctrl+/" => "help_dialog",
1257 if (get_pref('COMBINED_DISPLAY_MODE')) {
1258 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1259 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1262 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1263 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1266 $prefixes = array();
1268 foreach (array_keys($hotkeys) as $hotkey) {
1269 $pair = explode(" ", $hotkey, 2);
1271 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1272 array_push($prefixes, $pair[0]);
1276 return array($prefixes, $hotkeys);
1279 function check_for_update() {
1280 if (defined("GIT_VERSION_TIMESTAMP")) {
1281 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1284 $content = json_decode($content, true);
1286 if ($content && isset($content["changeset"])) {
1287 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1288 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1290 return $content["changeset"]["id"];
1299 function make_runtime_info($disable_update_check = false) {
1304 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1305 ttrss_feeds WHERE owner_uid = ?");
1306 $sth->execute([$_SESSION['uid']]);
1307 $row = $sth->fetch();
1309 $max_feed_id = $row['mid'];
1310 $num_feeds = $row['nf'];
1312 $data["max_feed_id"] = (int) $max_feed_id;
1313 $data["num_feeds"] = (int) $num_feeds;
1315 $data['last_article_id'] = Article
::getLastArticleId();
1316 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1318 $data['dep_ts'] = calculate_dep_timestamp();
1319 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1321 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1323 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1324 $update_result = @check_for_update
();
1326 $data["update_result"] = $update_result;
1328 $_SESSION["last_version_check"] = time();
1331 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1333 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1335 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1337 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1340 $stamp_delta = time() - $stamp;
1342 if ($stamp_delta > 1800) {
1346 $_SESSION["daemon_stamp_check"] = time();
1349 $data['daemon_stamp_ok'] = $stamp_check;
1351 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1353 $data['daemon_stamp'] = $stamp_fmt;
1361 function search_to_sql($search, $search_language) {
1363 $keywords = str_getcsv(trim($search), " ");
1364 $query_keywords = array();
1365 $search_words = array();
1366 $search_query_leftover = array();
1370 if ($search_language)
1371 $search_language = $pdo->quote(mb_strtolower($search_language));
1373 $search_language = "english";
1375 foreach ($keywords as $k) {
1376 if (strpos($k, "-") === 0) {
1383 $commandpair = explode(":", mb_strtolower($k), 2);
1385 switch ($commandpair[0]) {
1387 if ($commandpair[1]) {
1388 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1389 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1391 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1392 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1393 array_push($search_words, $k);
1397 if ($commandpair[1]) {
1398 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1399 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1401 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1402 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1403 array_push($search_words, $k);
1407 if ($commandpair[1]) {
1408 if ($commandpair[1] == "true")
1409 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1410 else if ($commandpair[1] == "false")
1411 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1413 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1414 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1416 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1417 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1418 if (!$not) array_push($search_words, $k);
1423 if ($commandpair[1]) {
1424 if ($commandpair[1] == "true")
1425 array_push($query_keywords, "($not (marked = true))");
1427 array_push($query_keywords, "($not (marked = false))");
1429 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1430 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1431 if (!$not) array_push($search_words, $k);
1435 if ($commandpair[1]) {
1436 if ($commandpair[1] == "true")
1437 array_push($query_keywords, "($not (published = true))");
1439 array_push($query_keywords, "($not (published = false))");
1442 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1443 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1444 if (!$not) array_push($search_words, $k);
1448 if ($commandpair[1]) {
1449 if ($commandpair[1] == "true")
1450 array_push($query_keywords, "($not (unread = true))");
1452 array_push($query_keywords, "($not (unread = false))");
1455 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1456 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1457 if (!$not) array_push($search_words, $k);
1461 if (strpos($k, "@") === 0) {
1463 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1464 $orig_ts = strtotime(substr($k, 1));
1465 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1467 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1469 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1472 if (DB_TYPE
== "pgsql") {
1473 $k = mb_strtolower($k);
1474 array_push($search_query_leftover, $not ?
"!$k" : $k);
1476 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1477 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1480 if (!$not) array_push($search_words, $k);
1485 if (count($search_query_leftover) > 0) {
1486 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1488 if (DB_TYPE
== "pgsql") {
1489 array_push($query_keywords,
1490 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1495 $search_query_part = implode("AND", $query_keywords);
1497 return array($search_query_part, $search_words);
1500 function iframe_whitelisted($entry) {
1501 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1503 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1506 foreach ($whitelist as $w) {
1507 if ($src == $w ||
$src == "www.$w")
1515 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1516 if (!$owner) $owner = $_SESSION["uid"];
1518 $res = trim($str); if (!$res) return '';
1520 $charset_hack = '<head>
1521 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1524 $res = trim($res); if (!$res) return '';
1526 libxml_use_internal_errors(true);
1528 $doc = new DOMDocument();
1529 $doc->loadHTML($charset_hack . $res);
1530 $xpath = new DOMXPath($doc);
1532 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1534 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1536 foreach ($entries as $entry) {
1538 if ($entry->hasAttribute('href')) {
1539 $entry->setAttribute('href',
1540 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1542 $entry->setAttribute('rel', 'noopener noreferrer');
1545 if ($entry->hasAttribute('src')) {
1546 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1547 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1549 if (file_exists($cached_filename)) {
1551 // this is strictly cosmetic
1552 if ($entry->tagName
== 'img') {
1554 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1556 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1562 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1564 if ($entry->hasAttribute('srcset')) {
1565 $entry->removeAttribute('srcset');
1568 if ($entry->hasAttribute('sizes')) {
1569 $entry->removeAttribute('sizes');
1573 $entry->setAttribute('src', $src);
1576 if ($entry->nodeName
== 'img') {
1578 if ($entry->hasAttribute('src')) {
1579 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1581 if (is_prefix_https() && !$is_https_url) {
1583 if ($entry->hasAttribute('srcset')) {
1584 $entry->removeAttribute('srcset');
1587 if ($entry->hasAttribute('sizes')) {
1588 $entry->removeAttribute('sizes');
1593 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1594 $force_remove_images ||
$_SESSION["bw_limit"]) {
1596 $p = $doc->createElement('p');
1598 $a = $doc->createElement('a');
1599 $a->setAttribute('href', $entry->getAttribute('src'));
1601 $a->appendChild(new DOMText($entry->getAttribute('src')));
1602 $a->setAttribute('target', '_blank');
1603 $a->setAttribute('rel', 'noopener noreferrer');
1605 $p->appendChild($a);
1607 $entry->parentNode
->replaceChild($p, $entry);
1611 if (strtolower($entry->nodeName
) == "a") {
1612 $entry->setAttribute("target", "_blank");
1613 $entry->setAttribute("rel", "noopener noreferrer");
1617 $entries = $xpath->query('//iframe');
1618 foreach ($entries as $entry) {
1619 if (!iframe_whitelisted($entry)) {
1620 $entry->setAttribute('sandbox', 'allow-scripts');
1622 if (is_prefix_https()) {
1623 $entry->setAttribute("src",
1624 str_replace("http://", "https://",
1625 $entry->getAttribute("src")));
1630 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1631 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1632 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1633 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1634 'dt', 'em', 'footer', 'figure', 'figcaption',
1635 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1636 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1637 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1638 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1639 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1640 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1642 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1644 $disallowed_attributes = array('id', 'style', 'class');
1646 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1647 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1648 if (is_array($retval)) {
1650 $allowed_elements = $retval[1];
1651 $disallowed_attributes = $retval[2];
1657 $doc->removeChild($doc->firstChild
); //remove doctype
1658 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1660 if ($highlight_words) {
1661 foreach ($highlight_words as $word) {
1663 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1665 $elements = $xpath->query("//*/text()");
1667 foreach ($elements as $child) {
1669 $fragment = $doc->createDocumentFragment();
1670 $text = $child->textContent
;
1672 while (($pos = mb_stripos($text, $word)) !== false) {
1673 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1674 $word = mb_substr($text, $pos, mb_strlen($word));
1675 $highlight = $doc->createElement('span');
1676 $highlight->appendChild(new DomText($word));
1677 $highlight->setAttribute('class', 'highlight');
1678 $fragment->appendChild($highlight);
1679 $text = mb_substr($text, $pos +
mb_strlen($word));
1682 if (!empty($text)) $fragment->appendChild(new DomText($text));
1684 $child->parentNode
->replaceChild($fragment, $child);
1689 $res = $doc->saveHTML();
1691 /* strip everything outside of <body>...</body> */
1693 $res_frag = array();
1694 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1695 return $res_frag[1];
1701 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1702 $xpath = new DOMXPath($doc);
1703 $entries = $xpath->query('//*');
1705 foreach ($entries as $entry) {
1706 if (!in_array($entry->nodeName
, $allowed_elements)) {
1707 $entry->parentNode
->removeChild($entry);
1710 if ($entry->hasAttributes()) {
1711 $attrs_to_remove = array();
1713 foreach ($entry->attributes
as $attr) {
1715 if (strpos($attr->nodeName
, 'on') === 0) {
1716 array_push($attrs_to_remove, $attr);
1719 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1720 array_push($attrs_to_remove, $attr);
1723 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1724 array_push($attrs_to_remove, $attr);
1728 foreach ($attrs_to_remove as $attr) {
1729 $entry->removeAttributeNode($attr);
1737 function trim_array($array) {
1739 array_walk($tmp, 'trim');
1743 function tag_is_valid($tag) {
1744 if ($tag == '') return false;
1745 if (is_numeric($tag)) return false;
1746 if (mb_strlen($tag) > 250) return false;
1748 if (!$tag) return false;
1753 function render_login_form() {
1754 header('Cache-Control: public');
1756 require_once "login_form.php";
1760 function T_sprintf() {
1761 $args = func_get_args();
1762 return vsprintf(__(array_shift($args)), $args);
1765 function print_checkpoint($n, $s) {
1766 $ts = microtime(true);
1767 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1771 function sanitize_tag($tag) {
1774 $tag = mb_strtolower($tag, 'utf-8');
1776 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1778 if (DB_TYPE
== "mysql") {
1779 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1785 function is_server_https() {
1786 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1789 function is_prefix_https() {
1790 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1793 // this returns SELF_URL_PATH sans ending slash
1794 function get_self_url_prefix() {
1795 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1796 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1798 return SELF_URL_PATH
;
1802 function encrypt_password($pass, $salt = '', $mode2 = false) {
1803 if ($salt && $mode2) {
1804 return "MODE2:" . hash('sha256', $salt . $pass);
1806 return "SHA1X:" . sha1("$salt:$pass");
1808 return "SHA1:" . sha1($pass);
1810 } // function encrypt_password
1812 function load_filters($feed_id, $owner_uid) {
1815 $feed_id = (int) $feed_id;
1816 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1819 $null_cat_qpart = "cat_id IS NULL OR";
1821 $null_cat_qpart = "";
1825 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1826 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1827 $sth->execute([$owner_uid]);
1829 $check_cats = array_merge(
1830 Feeds
::getParentCategories($cat_id, $owner_uid),
1833 $check_cats_str = join(",", $check_cats);
1834 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1836 while ($line = $sth->fetch()) {
1837 $filter_id = $line["id"];
1839 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1841 $sth2 = $pdo->prepare("SELECT
1842 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1843 FROM ttrss_filters2_rules AS r,
1844 ttrss_filter_types AS t
1846 (match_on IS NOT NULL OR
1847 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1848 (feed_id IS NULL OR feed_id = ?))) AND
1849 filter_type = t.id AND filter_id = ?");
1850 $sth2->execute([$feed_id, $filter_id]);
1855 while ($rule_line = $sth2->fetch()) {
1856 # print_r($rule_line);
1858 if ($rule_line["match_on"]) {
1859 $match_on = json_decode($rule_line["match_on"], true);
1861 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1864 $rule["reg_exp"] = $rule_line["reg_exp"];
1865 $rule["type"] = $rule_line["type_name"];
1866 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1868 array_push($rules, $rule);
1869 } else if (!$match_any_rule) {
1870 // this filter contains a rule that doesn't match to this feed/category combination
1871 // thus filter has to be rejected
1880 $rule["reg_exp"] = $rule_line["reg_exp"];
1881 $rule["type"] = $rule_line["type_name"];
1882 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1884 array_push($rules, $rule);
1888 if (count($rules) > 0) {
1889 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1890 FROM ttrss_filters2_actions AS a,
1891 ttrss_filter_actions AS t
1893 action_id = t.id AND filter_id = ?");
1894 $sth2->execute([$filter_id]);
1896 while ($action_line = $sth2->fetch()) {
1897 # print_r($action_line);
1900 $action["type"] = $action_line["type_name"];
1901 $action["param"] = $action_line["action_param"];
1903 array_push($actions, $action);
1908 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1909 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1910 $filter["rules"] = $rules;
1911 $filter["actions"] = $actions;
1913 if (count($rules) > 0 && count($actions) > 0) {
1914 array_push($filters, $filter);
1921 function get_score_pic($score) {
1923 return "score_high.png";
1924 } else if ($score > 0) {
1925 return "score_half_high.png";
1926 } else if ($score < -100) {
1927 return "score_low.png";
1928 } else if ($score < 0) {
1929 return "score_half_low.png";
1931 return "score_neutral.png";
1935 function init_plugins() {
1936 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1941 function add_feed_category($feed_cat, $parent_cat_id = false) {
1943 if (!$feed_cat) return false;
1945 $feed_cat = mb_substr($feed_cat, 0, 250);
1946 if (!$parent_cat_id) $parent_cat_id = null;
1949 $tr_in_progress = false;
1952 $pdo->beginTransaction();
1953 } catch (Exception
$e) {
1954 $tr_in_progress = true;
1957 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1958 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
1959 AND title = :title AND owner_uid = :uid");
1960 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
1962 if (!$sth->fetch()) {
1964 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1966 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
1968 if (!$tr_in_progress) $pdo->commit();
1979 * Fixes incomplete URLs by prepending "http://".
1980 * Also replaces feed:// with http://, and
1981 * prepends a trailing slash if the url is a domain name only.
1983 * @param string $url Possibly incomplete URL
1985 * @return string Fixed URL.
1987 function fix_url($url) {
1989 // support schema-less urls
1990 if (strpos($url, '//') === 0) {
1991 $url = 'https:' . $url;
1994 if (strpos($url, '://') === false) {
1995 $url = 'http://' . $url;
1996 } else if (substr($url, 0, 5) == 'feed:') {
1997 $url = 'http:' . substr($url, 5);
2000 //prepend slash if the URL has no slash in it
2001 // "http://www.example" -> "http://www.example/"
2002 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2006 //convert IDNA hostname to punycode if possible
2007 if (function_exists("idn_to_ascii")) {
2008 $parts = parse_url($url);
2009 if (mb_detect_encoding($parts['host']) != 'ASCII')
2011 $parts['host'] = idn_to_ascii($parts['host']);
2012 $url = build_url($parts);
2016 if ($url != "http:///")
2022 function validate_feed_url($url) {
2023 $parts = parse_url($url);
2025 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2029 /* function save_email_address($email) {
2030 // FIXME: implement persistent storage of emails
2032 if (!$_SESSION['stored_emails'])
2033 $_SESSION['stored_emails'] = array();
2035 if (!in_array($email, $_SESSION['stored_emails']))
2036 array_push($_SESSION['stored_emails'], $email);
2040 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2042 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2044 $is_cat = bool_to_sql_bool($is_cat);
2048 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2049 WHERE feed_id = ? AND is_cat = ?
2050 AND owner_uid = ?");
2051 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2053 if ($row = $sth->fetch()) {
2054 return $row["access_key"];
2056 $key = uniqid_short();
2058 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2059 (access_key, feed_id, is_cat, owner_uid)
2060 VALUES (?, ?, ?, ?)");
2062 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2068 function get_feeds_from_html($url, $content)
2070 $url = fix_url($url);
2071 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2073 libxml_use_internal_errors(true);
2075 $doc = new DOMDocument();
2076 $doc->loadHTML($content);
2077 $xpath = new DOMXPath($doc);
2078 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2079 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2080 $feedUrls = array();
2081 foreach ($entries as $entry) {
2082 if ($entry->hasAttribute('href')) {
2083 $title = $entry->getAttribute('title');
2085 $title = $entry->getAttribute('type');
2087 $feedUrl = rewrite_relative_url(
2088 $baseUrl, $entry->getAttribute('href')
2090 $feedUrls[$feedUrl] = $title;
2096 function is_html($content) {
2097 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2100 function url_is_html($url, $login = false, $pass = false) {
2101 return is_html(fetch_file_contents($url, false, $login, $pass));
2104 function build_url($parts) {
2105 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2108 function cleanup_url_path($path) {
2109 $path = str_replace("/./", "/", $path);
2110 $path = str_replace("//", "/", $path);
2116 * Converts a (possibly) relative URL to a absolute one.
2118 * @param string $url Base URL (i.e. from where the document is)
2119 * @param string $rel_url Possibly relative URL in the document
2121 * @return string Absolute URL
2123 function rewrite_relative_url($url, $rel_url) {
2124 if (strpos($rel_url, "://") !== false) {
2126 } else if (strpos($rel_url, "//") === 0) {
2127 # protocol-relative URL (rare but they exist)
2129 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2130 # magnet:, feed:, etc
2132 } else if (strpos($rel_url, "/") === 0) {
2133 $parts = parse_url($url);
2134 $parts['path'] = $rel_url;
2135 $parts['path'] = cleanup_url_path($parts['path']);
2137 return build_url($parts);
2140 $parts = parse_url($url);
2141 if (!isset($parts['path'])) {
2142 $parts['path'] = '/';
2144 $dir = $parts['path'];
2145 if (substr($dir, -1) !== '/') {
2146 $dir = dirname($parts['path']);
2147 $dir !== '/' && $dir .= '/';
2149 $parts['path'] = $dir . $rel_url;
2150 $parts['path'] = cleanup_url_path($parts['path']);
2152 return build_url($parts);
2156 function cleanup_tags($days = 14, $limit = 1000) {
2158 $days = (int) $days;
2160 if (DB_TYPE
== "pgsql") {
2161 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2162 } else if (DB_TYPE
== "mysql") {
2163 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2170 while ($limit > 0) {
2173 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2174 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2175 WHERE post_int_id = int_id AND $interval_query AND
2176 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2177 $sth->execute([$limit]);
2181 while ($line = $sth->fetch()) {
2182 array_push($ids, $line['id']);
2185 if (count($ids) > 0) {
2186 $ids = join(",", $ids);
2188 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2189 $tags_deleted = $usth->rowCount();
2194 $limit -= $limit_part;
2197 return $tags_deleted;
2200 function print_user_stylesheet() {
2201 $value = get_pref('USER_STYLESHEET');
2204 print "<style type=\"text/css\">";
2205 print str_replace("<br/>", "\n", $value);
2211 function filter_to_sql($filter, $owner_uid) {
2216 if (DB_TYPE
== "pgsql")
2219 $reg_qpart = "REGEXP";
2221 foreach ($filter["rules"] AS $rule) {
2222 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2223 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2224 $rule['reg_exp']) !== FALSE;
2226 if ($regexp_valid) {
2228 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2230 switch ($rule["type"]) {
2232 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2233 $rule['reg_exp'] . "')";
2236 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2237 $rule['reg_exp'] . "')";
2240 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2241 $rule['reg_exp'] . "') OR LOWER(" .
2242 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2245 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2246 $rule['reg_exp'] . "')";
2249 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2250 $rule['reg_exp'] . "')";
2253 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2254 $rule['reg_exp'] . "')";
2258 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2260 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2261 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2264 if (isset($rule["cat_id"])) {
2266 if ($rule["cat_id"] > 0) {
2267 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2268 array_push($children, $rule["cat_id"]);
2270 $children = join(",", $children);
2272 $cat_qpart = "cat_id IN ($children)";
2274 $cat_qpart = "cat_id IS NULL";
2277 $qpart .= " AND $cat_qpart";
2280 $qpart .= " AND feed_id IS NOT NULL";
2282 array_push($query, "($qpart)");
2287 if (count($query) > 0) {
2288 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2290 $fullquery = "(false)";
2293 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2298 if (!function_exists('gzdecode')) {
2299 function gzdecode($string) { // no support for 2nd argument
2300 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2301 base64_encode($string));
2305 function get_random_bytes($length) {
2306 if (function_exists('openssl_random_pseudo_bytes')) {
2307 return openssl_random_pseudo_bytes($length);
2311 for ($i = 0; $i < $length; $i++
)
2312 $output .= chr(mt_rand(0, 255));
2318 function read_stdin() {
2319 $fp = fopen("php://stdin", "r");
2322 $line = trim(fgets($fp));
2330 function implements_interface($class, $interface) {
2331 return in_array($interface, class_implements($class));
2334 function get_minified_js($files) {
2335 require_once 'lib/jshrink/Minifier.php';
2339 foreach ($files as $js) {
2340 if (!isset($_GET['debug'])) {
2341 $cached_file = CACHE_DIR
. "/js/".basename($js).".js";
2343 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2345 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2347 if ($header && $contents) {
2348 list($htag, $hversion) = explode(":", $header);
2350 if ($htag == "tt-rss" && $hversion == VERSION
) {
2357 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js.js"));
2358 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2362 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2369 function calculate_dep_timestamp() {
2370 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2374 foreach ($files as $file) {
2375 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2381 function T_js_decl($s1, $s2) {
2383 $s1 = preg_replace("/\n/", "", $s1);
2384 $s2 = preg_replace("/\n/", "", $s2);
2386 $s1 = preg_replace("/\"/", "\\\"", $s1);
2387 $s2 = preg_replace("/\"/", "\\\"", $s2);
2389 return "T_messages[\"$s1\"] = \"$s2\";\n";
2393 function init_js_translations() {
2395 print 'var T_messages = new Object();
2398 if (T_messages[msg]) {
2399 return T_messages[msg];
2405 function ngettext(msg1, msg2, n) {
2406 return __((parseInt(n) > 1) ? msg2 : msg1);
2409 $l10n = _get_reader();
2411 for ($i = 0; $i < $l10n->total
; $i++
) {
2412 $orig = $l10n->get_original_string($i);
2413 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2414 $key = explode(chr(0), $orig);
2415 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2416 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2418 $translation = __($orig);
2419 print T_js_decl($orig, $translation);
2424 function get_theme_path($theme) {
2425 if ($theme == "default.php")
2426 return "css/default.css";
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 validate_field($string, $allowed, $default = "") {
2552 if (in_array($string, $allowed))
2558 function arr_qmarks($arr) {
2559 return str_repeat('?,', count($arr) - 1) . '?';