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 if (DB_TYPE
== "pgsql") {
263 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
265 WHERE ttrss_entries.id = ref_id AND
269 ttrss_entries.date_updated < NOW() - INTERVAL ?");
270 $sth->execute([$feed_id, "$purge_interval days"]);
273 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
274 USING ttrss_user_entries, ttrss_entries
275 WHERE ttrss_entries.id = ref_id AND
279 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL ? DAY)");
280 $sth->execute([$feed_id, $purge_interval]);
284 $rows = $sth->rowCount();
286 CCache
::update($feed_id, $owner_uid);
289 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
293 } // function purge_feed
295 function feed_purge_interval($feed_id) {
299 $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
301 $sth->execute([$feed_id]);
303 if ($row = $sth->fetch()) {
304 $purge_interval = $row["purge_interval"];
305 $owner_uid = $row["owner_uid"];
307 if ($purge_interval == 0) $purge_interval = get_pref(
308 'PURGE_OLD_DAYS', $owner_uid);
310 return $purge_interval;
317 // TODO: multiple-argument way is deprecated, first parameter is a hash now
318 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
319 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
321 global $fetch_last_error;
322 global $fetch_last_error_code;
323 global $fetch_last_error_content;
324 global $fetch_last_content_type;
325 global $fetch_last_modified;
326 global $fetch_curl_used;
328 $fetch_last_error = false;
329 $fetch_last_error_code = -1;
330 $fetch_last_error_content = "";
331 $fetch_last_content_type = "";
332 $fetch_curl_used = false;
333 $fetch_last_modified = "";
335 if (!is_array($options)) {
337 // falling back on compatibility shim
338 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
341 for ($i = 0; $i < func_num_args(); $i++
) {
342 $tmp[$option_names[$i]] = func_get_arg($i);
348 "url" => func_get_arg(0),
349 "type" => @func_get_arg(1),
350 "login" => @func_get_arg(2),
351 "pass" => @func_get_arg(3),
352 "post_query" => @func_get_arg(4),
353 "timeout" => @func_get_arg(5),
354 "timestamp" => @func_get_arg(6),
355 "useragent" => @func_get_arg(7)
359 $url = $options["url"];
360 $type = isset($options["type"]) ?
$options["type"] : false;
361 $login = isset($options["login"]) ?
$options["login"] : false;
362 $pass = isset($options["pass"]) ?
$options["pass"] : false;
363 $post_query = isset($options["post_query"]) ?
$options["post_query"] : false;
364 $timeout = isset($options["timeout"]) ?
$options["timeout"] : false;
365 $last_modified = isset($options["last_modified"]) ?
$options["last_modified"] : "";
366 $useragent = isset($options["useragent"]) ?
$options["useragent"] : false;
367 $followlocation = isset($options["followlocation"]) ?
$options["followlocation"] : true;
369 $url = ltrim($url, ' ');
370 $url = str_replace(' ', '%20', $url);
372 if (strpos($url, "//") === 0)
373 $url = 'http:' . $url;
375 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
377 $fetch_curl_used = true;
379 $ch = curl_init($url);
381 if ($last_modified && !$post_query) {
382 curl_setopt($ch, CURLOPT_HTTPHEADER
,
383 array("If-Modified-Since: $last_modified"));
386 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
387 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
388 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
389 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
390 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
391 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
392 curl_setopt($ch, CURLOPT_HEADER
, true);
393 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
394 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
396 curl_setopt($ch, CURLOPT_ENCODING
, "");
397 //curl_setopt($ch, CURLOPT_REFERER, $url);
399 if (!ini_get("open_basedir")) {
400 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
403 if (defined('_CURL_HTTP_PROXY')) {
404 curl_setopt($ch, CURLOPT_PROXY
, _CURL_HTTP_PROXY
);
408 curl_setopt($ch, CURLOPT_POST
, true);
409 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
413 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
415 $ret = @curl_exec
($ch);
417 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE
);
418 $headers = explode("\r\n", substr($ret, 0, $headers_length));
419 $contents = substr($ret, $headers_length);
421 foreach ($headers as $header) {
422 if (strstr($header, ": ") !== FALSE) {
423 list ($key, $value) = explode(": ", $header);
425 if (strtolower($key) == "last-modified") {
426 $fetch_last_modified = $value;
430 if (substr(strtolower($header), 0, 7) == 'http/1.') {
431 $fetch_last_error_code = (int) substr($header, 9, 3);
432 $fetch_last_error = $header;
436 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
437 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
438 $contents = @curl_exec
($ch);
441 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
442 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
444 $fetch_last_error_code = $http_code;
446 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
448 if (curl_errno($ch) != 0) {
449 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
452 $fetch_last_error_content = $contents;
458 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
468 $fetch_curl_used = false;
470 if ($login && $pass){
471 $url_parts = array();
473 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
475 $pass = urlencode($pass);
477 if ($url_parts[1] && $url_parts[2]) {
478 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
482 // TODO: should this support POST requests or not? idk
484 if (!$post_query && $last_modified) {
485 $context = stream_context_create(array(
488 'ignore_errors' => true,
489 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
490 'protocol_version'=> 1.1,
491 'header' => "If-Modified-Since: $last_modified\r\n")
494 $context = stream_context_create(array(
497 'ignore_errors' => true,
498 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
499 'protocol_version'=> 1.1
503 $old_error = error_get_last();
505 $data = @file_get_contents
($url, false, $context);
507 if (isset($http_response_header) && is_array($http_response_header)) {
508 foreach ($http_response_header as $header) {
509 if (strstr($header, ": ") !== FALSE) {
510 list ($key, $value) = explode(": ", $header);
512 $key = strtolower($key);
514 if ($key == 'content-type') {
515 $fetch_last_content_type = $value;
516 // don't abort here b/c there might be more than one
517 // e.g. if we were being redirected -- last one is the right one
518 } else if ($key == 'last-modified') {
519 $fetch_last_modified = $value;
523 if (substr(strtolower($header), 0, 7) == 'http/1.') {
524 $fetch_last_error_code = (int) substr($header, 9, 3);
525 $fetch_last_error = $header;
530 if ($fetch_last_error_code != 200) {
531 $error = error_get_last();
533 if ($error['message'] != $old_error['message']) {
534 $fetch_last_error .= "; " . $error["message"];
537 $fetch_last_error_content = $data;
547 * Try to determine the favicon URL for a feed.
548 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
549 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
551 * @param string $url A feed or page URL
553 * @return mixed The favicon URL, or false if none was found.
555 function get_favicon_url($url) {
557 $favicon_url = false;
559 if ($html = @fetch_file_contents
($url)) {
561 libxml_use_internal_errors(true);
563 $doc = new DOMDocument();
564 $doc->loadHTML($html);
565 $xpath = new DOMXPath($doc);
567 $base = $xpath->query('/html/head/base[@href]');
568 foreach ($base as $b) {
569 $url = rewrite_relative_url($url, $b->getAttribute("href"));
573 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
574 if (count($entries) > 0) {
575 foreach ($entries as $entry) {
576 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
583 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
586 } // function get_favicon_url
588 function initialize_user_prefs($uid, $profile = false) {
590 $uid = db_escape_string($uid);
592 if (get_schema_version() < 63) $profile_qpart = "";
594 ////db_query("BEGIN");
598 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
600 $profile = $profile ?
$profile : null;
602 $u_sth = $pdo->prepare("SELECT pref_name
603 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
604 profile = :profile OR (:profile IS NULL AND profile IS NULL)");
605 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
607 $active_prefs = array();
609 while ($line = $u_sth->fetch()) {
610 array_push($active_prefs, $line["pref_name"]);
613 while ($line = $sth->fetch()) {
614 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
615 // print "adding " . $line["pref_name"] . "<br>";
617 $line["def_value"] = db_escape_string($line["def_value"]);
618 $line["pref_name"] = db_escape_string($line["pref_name"]);
620 if (get_schema_version() < 63) {
621 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
622 (owner_uid,pref_name,value) VALUES
624 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
627 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
628 (owner_uid,pref_name,value, profile) VALUES
630 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
636 ////db_query("COMMIT");
640 function get_ssl_certificate_id() {
641 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
642 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
643 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
644 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
645 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
647 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
648 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
649 $_SERVER["SSL_CLIENT_V_START"] .
650 $_SERVER["SSL_CLIENT_V_END"] .
651 $_SERVER["SSL_CLIENT_S_DN"]);
656 function authenticate_user($login, $password, $check_only = false) {
658 if (!SINGLE_USER_MODE
) {
661 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
663 $user_id = (int) $plugin->authenticate($login, $password);
666 $_SESSION["auth_module"] = strtolower(get_class($plugin));
671 if ($user_id && !$check_only) {
674 $_SESSION["uid"] = $user_id;
675 $_SESSION["version"] = VERSION_STATIC
;
678 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
680 $sth->execute([$user_id]);
681 $row = $sth->fetch();
683 $_SESSION["name"] = $row["login"];
684 $_SESSION["access_level"] = $row["access_level"];
685 $_SESSION["csrf_token"] = uniqid_short();
687 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
688 $usth->execute([$user_id]);
690 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
691 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
692 $_SESSION["pwd_hash"] = $row["pwd_hash"];
694 $_SESSION["last_version_check"] = time();
696 initialize_user_prefs($_SESSION["uid"]);
705 $_SESSION["uid"] = 1;
706 $_SESSION["name"] = "admin";
707 $_SESSION["access_level"] = 10;
709 $_SESSION["hide_hello"] = true;
710 $_SESSION["hide_logout"] = true;
712 $_SESSION["auth_module"] = false;
714 if (!$_SESSION["csrf_token"]) {
715 $_SESSION["csrf_token"] = uniqid_short();
718 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
720 initialize_user_prefs($_SESSION["uid"]);
726 function make_password($length = 8) {
729 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
733 while ($i < $length) {
734 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
736 if (!strstr($password, $char)) {
744 // this is called after user is created to initialize default feeds, labels
747 // user preferences are checked on every login, not here
749 function initialize_user($uid) {
753 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
754 values (?, 'Tiny Tiny RSS: Forum',
755 'http://tt-rss.org/forum/rss.php')");
756 $sth->execute([$uid]);
759 function logout_user() {
761 if (isset($_COOKIE[session_name()])) {
762 setcookie(session_name(), '', time()-42000, '/');
766 function validate_csrf($csrf_token) {
767 return $csrf_token == $_SESSION['csrf_token'];
770 function load_user_plugins($owner_uid, $pluginhost = false) {
772 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
774 if ($owner_uid && SCHEMA_VERSION
>= 100) {
775 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
777 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
779 if (get_schema_version() > 100) {
780 $pluginhost->load_data();
785 function login_sequence() {
788 if (SINGLE_USER_MODE
) {
790 authenticate_user("admin", null);
792 load_user_plugins($_SESSION["uid"]);
794 if (!validate_session()) $_SESSION["uid"] = false;
796 if (!$_SESSION["uid"]) {
798 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
799 $_SESSION["ref_schema_version"] = get_schema_version(true);
801 authenticate_user(null, null, true);
804 if (!$_SESSION["uid"]) {
806 setcookie(session_name(), '', time()-42000, '/');
813 /* bump login timestamp */
814 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
815 $sth->execute([$_SESSION['uid']]);
817 $_SESSION["last_login_update"] = time();
820 if ($_SESSION["uid"]) {
822 load_user_plugins($_SESSION["uid"]);
826 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
828 (SELECT COUNT(id) FROM ttrss_feeds WHERE
829 ttrss_feeds.id = feed_id) = 0");
831 $sth->execute($_SESSION['uid']);
833 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
835 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
836 ttrss_feed_categories.id = feed_id) = 0");
838 $sth->execute($_SESSION['uid']);
844 function truncate_string($str, $max_len, $suffix = '…') {
845 if (mb_strlen($str, "utf-8") > $max_len) {
846 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
853 function truncate_middle($str, $max_len, $suffix = '…') {
854 if (strlen($str) > $max_len) {
855 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
861 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
864 $source_tz = new DateTimeZone($source_tz);
865 } catch (Exception
$e) {
866 $source_tz = new DateTimeZone('UTC');
870 $dest_tz = new DateTimeZone($dest_tz);
871 } catch (Exception
$e) {
872 $dest_tz = new DateTimeZone('UTC');
875 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
876 return $dt->format('U') +
$dest_tz->getOffset($dt);
879 function make_local_datetime($timestamp, $long, $owner_uid = false,
880 $no_smart_dt = false, $eta_min = false) {
882 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
883 if (!$timestamp) $timestamp = '1970-01-01 0:00';
888 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
890 $timestamp = substr($timestamp, 0, 19);
892 # We store date in UTC internally
893 $dt = new DateTime($timestamp, $utc_tz);
895 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
897 if ($user_tz_string != 'Automatic') {
900 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
901 } catch (Exception
$e) {
905 $tz_offset = $user_tz->getOffset($dt);
907 $tz_offset = (int) -$_SESSION["clientTzOffset"];
910 $user_timestamp = $dt->format('U') +
$tz_offset;
913 return smart_date_time($user_timestamp,
914 $tz_offset, $owner_uid, $eta_min);
917 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
919 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
921 return date($format, $user_timestamp);
925 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
926 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
928 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
929 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
930 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
931 return date("G:i", $timestamp);
932 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
933 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
934 return date($format, $timestamp);
936 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
937 return date($format, $timestamp);
941 function sql_bool_to_bool($s) {
942 if ($s == "t" ||
$s == "1" ||
strtolower($s) == "true") {
949 function bool_to_sql_bool($s) {
957 // Session caching removed due to causing wrong redirects to upgrade
958 // script when get_schema_version() is called on an obsolete session
959 // created on a previous schema version.
960 function get_schema_version($nocache = false) {
961 global $schema_version;
965 if (!$schema_version && !$nocache) {
966 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
967 $version = $row["schema_version"];
968 $schema_version = $version;
971 return $schema_version;
975 function sanity_check() {
976 require_once 'errors.php';
980 $schema_version = get_schema_version(true);
982 if ($schema_version != SCHEMA_VERSION
) {
986 if (db_escape_string("testTEST") != "testTEST") {
990 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
993 function file_is_locked($filename) {
994 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
995 if (function_exists('flock')) {
996 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
998 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1009 return true; // consider the file always locked and skip the test
1016 function make_lockfile($filename) {
1017 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1019 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1020 $stat_h = fstat($fp);
1021 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1023 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1024 if ($stat_h["ino"] != $stat_f["ino"] ||
1025 $stat_h["dev"] != $stat_f["dev"]) {
1031 if (function_exists('posix_getpid')) {
1032 fwrite($fp, posix_getpid() . "\n");
1040 function make_stampfile($filename) {
1041 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1043 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1044 fwrite($fp, time() . "\n");
1045 flock($fp, LOCK_UN
);
1053 function sql_random_function() {
1054 if (DB_TYPE
== "mysql") {
1061 function getFeedUnread($feed, $is_cat = false) {
1062 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1065 function checkbox_to_sql_bool($val) {
1066 return ($val == "on") ?
"true" : "false";
1069 function uniqid_short() {
1070 return uniqid(base_convert(rand(), 10, 36));
1073 function make_init_params() {
1076 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1077 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1078 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1079 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1081 $params[strtolower($param)] = (int) get_pref($param);
1084 $params["icons_url"] = ICONS_URL
;
1085 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1086 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1087 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1088 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1089 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1090 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1092 $theme = get_pref( "USER_CSS_THEME", false, false);
1093 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1095 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1097 $params["php_platform"] = PHP_OS
;
1098 $params["php_version"] = PHP_VERSION
;
1100 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1104 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1105 ttrss_feeds WHERE owner_uid = ?");
1106 $sth->execute([$_SESSION['uid']]);
1107 $row = $sth->fetch();
1109 $max_feed_id = $row["mid"];
1110 $num_feeds = $row["nf"];
1112 $params["max_feed_id"] = (int) $max_feed_id;
1113 $params["num_feeds"] = (int) $num_feeds;
1115 $params["hotkeys"] = get_hotkeys_map();
1117 $params["csrf_token"] = $_SESSION["csrf_token"];
1118 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1120 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1122 $params["icon_alert"] = base64_img("images/alert.png");
1123 $params["icon_information"] = base64_img("images/information.png");
1124 $params["icon_cross"] = base64_img("images/cross.png");
1125 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1127 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1132 function get_hotkeys_info() {
1134 __("Navigation") => array(
1135 "next_feed" => __("Open next feed"),
1136 "prev_feed" => __("Open previous feed"),
1137 "next_article" => __("Open next article"),
1138 "prev_article" => __("Open previous article"),
1139 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1140 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1141 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1142 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1143 "search_dialog" => __("Show search dialog")),
1144 __("Article") => array(
1145 "toggle_mark" => __("Toggle starred"),
1146 "toggle_publ" => __("Toggle published"),
1147 "toggle_unread" => __("Toggle unread"),
1148 "edit_tags" => __("Edit tags"),
1149 "open_in_new_window" => __("Open in new window"),
1150 "catchup_below" => __("Mark below as read"),
1151 "catchup_above" => __("Mark above as read"),
1152 "article_scroll_down" => __("Scroll down"),
1153 "article_scroll_up" => __("Scroll up"),
1154 "select_article_cursor" => __("Select article under cursor"),
1155 "email_article" => __("Email article"),
1156 "close_article" => __("Close/collapse article"),
1157 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1158 "toggle_widescreen" => __("Toggle widescreen mode"),
1159 "toggle_embed_original" => __("Toggle embed original")),
1160 __("Article selection") => array(
1161 "select_all" => __("Select all articles"),
1162 "select_unread" => __("Select unread"),
1163 "select_marked" => __("Select starred"),
1164 "select_published" => __("Select published"),
1165 "select_invert" => __("Invert selection"),
1166 "select_none" => __("Deselect everything")),
1167 __("Feed") => array(
1168 "feed_refresh" => __("Refresh current feed"),
1169 "feed_unhide_read" => __("Un/hide read feeds"),
1170 "feed_subscribe" => __("Subscribe to feed"),
1171 "feed_edit" => __("Edit feed"),
1172 "feed_catchup" => __("Mark as read"),
1173 "feed_reverse" => __("Reverse headlines"),
1174 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1175 "feed_debug_update" => __("Debug feed update"),
1176 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1177 "catchup_all" => __("Mark all feeds as read"),
1178 "cat_toggle_collapse" => __("Un/collapse current category"),
1179 "toggle_combined_mode" => __("Toggle combined mode"),
1180 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1181 __("Go to") => array(
1182 "goto_all" => __("All articles"),
1183 "goto_fresh" => __("Fresh"),
1184 "goto_marked" => __("Starred"),
1185 "goto_published" => __("Published"),
1186 "goto_tagcloud" => __("Tag cloud"),
1187 "goto_prefs" => __("Preferences")),
1188 __("Other") => array(
1189 "create_label" => __("Create label"),
1190 "create_filter" => __("Create filter"),
1191 "collapse_sidebar" => __("Un/collapse sidebar"),
1192 "help_dialog" => __("Show help dialog"))
1195 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1196 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1202 function get_hotkeys_map() {
1204 // "navigation" => array(
1207 "n" => "next_article",
1208 "p" => "prev_article",
1209 "(38)|up" => "prev_article",
1210 "(40)|down" => "next_article",
1211 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1212 // "^(40)|Ctrl-down" => "next_article_noscroll",
1213 "(191)|/" => "search_dialog",
1214 // "article" => array(
1215 "s" => "toggle_mark",
1216 "*s" => "toggle_publ",
1217 "u" => "toggle_unread",
1218 "*t" => "edit_tags",
1219 "o" => "open_in_new_window",
1220 "c p" => "catchup_below",
1221 "c n" => "catchup_above",
1222 "*n" => "article_scroll_down",
1223 "*p" => "article_scroll_up",
1224 "*(38)|Shift+up" => "article_scroll_up",
1225 "*(40)|Shift+down" => "article_scroll_down",
1226 "a *w" => "toggle_widescreen",
1227 "a e" => "toggle_embed_original",
1228 "e" => "email_article",
1229 "a q" => "close_article",
1230 // "article_selection" => array(
1231 "a a" => "select_all",
1232 "a u" => "select_unread",
1233 "a *u" => "select_marked",
1234 "a p" => "select_published",
1235 "a i" => "select_invert",
1236 "a n" => "select_none",
1238 "f r" => "feed_refresh",
1239 "f a" => "feed_unhide_read",
1240 "f s" => "feed_subscribe",
1241 "f e" => "feed_edit",
1242 "f q" => "feed_catchup",
1243 "f x" => "feed_reverse",
1244 "f g" => "feed_toggle_vgroup",
1245 "f *d" => "feed_debug_update",
1246 "f *g" => "feed_debug_viewfeed",
1247 "f *c" => "toggle_combined_mode",
1248 "f c" => "toggle_cdm_expanded",
1249 "*q" => "catchup_all",
1250 "x" => "cat_toggle_collapse",
1252 "g a" => "goto_all",
1253 "g f" => "goto_fresh",
1254 "g s" => "goto_marked",
1255 "g p" => "goto_published",
1256 "g t" => "goto_tagcloud",
1257 "g *p" => "goto_prefs",
1258 // "other" => array(
1259 "(9)|Tab" => "select_article_cursor", // tab
1260 "c l" => "create_label",
1261 "c f" => "create_filter",
1262 "c s" => "collapse_sidebar",
1263 "^(191)|Ctrl+/" => "help_dialog",
1266 if (get_pref('COMBINED_DISPLAY_MODE')) {
1267 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1268 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1271 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1272 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1275 $prefixes = array();
1277 foreach (array_keys($hotkeys) as $hotkey) {
1278 $pair = explode(" ", $hotkey, 2);
1280 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1281 array_push($prefixes, $pair[0]);
1285 return array($prefixes, $hotkeys);
1288 function check_for_update() {
1289 if (defined("GIT_VERSION_TIMESTAMP")) {
1290 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1293 $content = json_decode($content, true);
1295 if ($content && isset($content["changeset"])) {
1296 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1297 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1299 return $content["changeset"]["id"];
1308 function make_runtime_info($disable_update_check = false) {
1313 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1314 ttrss_feeds WHERE owner_uid = ?");
1315 $sth->execute([$_SESSION['uid']]);
1316 $row = $sth->fetch();
1318 $max_feed_id = $row['mid'];
1319 $num_feeds = $row['nf'];
1321 $data["max_feed_id"] = (int) $max_feed_id;
1322 $data["num_feeds"] = (int) $num_feeds;
1324 $data['last_article_id'] = Article
::getLastArticleId();
1325 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1327 $data['dep_ts'] = calculate_dep_timestamp();
1328 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1330 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1332 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1333 $update_result = @check_for_update
();
1335 $data["update_result"] = $update_result;
1337 $_SESSION["last_version_check"] = time();
1340 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1342 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1344 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1346 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1349 $stamp_delta = time() - $stamp;
1351 if ($stamp_delta > 1800) {
1355 $_SESSION["daemon_stamp_check"] = time();
1358 $data['daemon_stamp_ok'] = $stamp_check;
1360 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1362 $data['daemon_stamp'] = $stamp_fmt;
1370 function search_to_sql($search, $search_language) {
1372 $keywords = str_getcsv(trim($search), " ");
1373 $query_keywords = array();
1374 $search_words = array();
1375 $search_query_leftover = array();
1377 if ($search_language)
1378 $search_language = db_escape_string(mb_strtolower($search_language));
1380 $search_language = "english";
1382 foreach ($keywords as $k) {
1383 if (strpos($k, "-") === 0) {
1390 $commandpair = explode(":", mb_strtolower($k), 2);
1392 switch ($commandpair[0]) {
1394 if ($commandpair[1]) {
1395 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
1396 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1398 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1399 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1400 array_push($search_words, $k);
1404 if ($commandpair[1]) {
1405 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
1406 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1408 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1409 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1410 array_push($search_words, $k);
1414 if ($commandpair[1]) {
1415 if ($commandpair[1] == "true")
1416 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1417 else if ($commandpair[1] == "false")
1418 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1420 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
1421 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1423 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1424 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1425 if (!$not) array_push($search_words, $k);
1430 if ($commandpair[1]) {
1431 if ($commandpair[1] == "true")
1432 array_push($query_keywords, "($not (marked = true))");
1434 array_push($query_keywords, "($not (marked = false))");
1436 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1437 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1438 if (!$not) array_push($search_words, $k);
1442 if ($commandpair[1]) {
1443 if ($commandpair[1] == "true")
1444 array_push($query_keywords, "($not (published = true))");
1446 array_push($query_keywords, "($not (published = false))");
1449 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1450 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1451 if (!$not) array_push($search_words, $k);
1455 if ($commandpair[1]) {
1456 if ($commandpair[1] == "true")
1457 array_push($query_keywords, "($not (unread = true))");
1459 array_push($query_keywords, "($not (unread = false))");
1462 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1463 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1464 if (!$not) array_push($search_words, $k);
1468 if (strpos($k, "@") === 0) {
1470 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1471 $orig_ts = strtotime(substr($k, 1));
1472 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1474 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1476 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1479 if (DB_TYPE
== "pgsql") {
1480 $k = mb_strtolower($k);
1481 array_push($search_query_leftover, $not ?
"!$k" : $k);
1483 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1484 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1487 if (!$not) array_push($search_words, $k);
1492 if (count($search_query_leftover) > 0) {
1493 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
1495 if (DB_TYPE
== "pgsql") {
1496 array_push($query_keywords,
1497 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
1502 $search_query_part = implode("AND", $query_keywords);
1504 return array($search_query_part, $search_words);
1507 function iframe_whitelisted($entry) {
1508 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1510 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1513 foreach ($whitelist as $w) {
1514 if ($src == $w ||
$src == "www.$w")
1522 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1523 if (!$owner) $owner = $_SESSION["uid"];
1525 $res = trim($str); if (!$res) return '';
1527 $charset_hack = '<head>
1528 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1531 $res = trim($res); if (!$res) return '';
1533 libxml_use_internal_errors(true);
1535 $doc = new DOMDocument();
1536 $doc->loadHTML($charset_hack . $res);
1537 $xpath = new DOMXPath($doc);
1539 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1541 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1543 foreach ($entries as $entry) {
1545 if ($entry->hasAttribute('href')) {
1546 $entry->setAttribute('href',
1547 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1549 $entry->setAttribute('rel', 'noopener noreferrer');
1552 if ($entry->hasAttribute('src')) {
1553 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1554 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1556 if (file_exists($cached_filename)) {
1558 // this is strictly cosmetic
1559 if ($entry->tagName
== 'img') {
1561 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1563 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1569 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1571 if ($entry->hasAttribute('srcset')) {
1572 $entry->removeAttribute('srcset');
1575 if ($entry->hasAttribute('sizes')) {
1576 $entry->removeAttribute('sizes');
1580 $entry->setAttribute('src', $src);
1583 if ($entry->nodeName
== 'img') {
1585 if ($entry->hasAttribute('src')) {
1586 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1588 if (is_prefix_https() && !$is_https_url) {
1590 if ($entry->hasAttribute('srcset')) {
1591 $entry->removeAttribute('srcset');
1594 if ($entry->hasAttribute('sizes')) {
1595 $entry->removeAttribute('sizes');
1600 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1601 $force_remove_images ||
$_SESSION["bw_limit"]) {
1603 $p = $doc->createElement('p');
1605 $a = $doc->createElement('a');
1606 $a->setAttribute('href', $entry->getAttribute('src'));
1608 $a->appendChild(new DOMText($entry->getAttribute('src')));
1609 $a->setAttribute('target', '_blank');
1610 $a->setAttribute('rel', 'noopener noreferrer');
1612 $p->appendChild($a);
1614 $entry->parentNode
->replaceChild($p, $entry);
1618 if (strtolower($entry->nodeName
) == "a") {
1619 $entry->setAttribute("target", "_blank");
1620 $entry->setAttribute("rel", "noopener noreferrer");
1624 $entries = $xpath->query('//iframe');
1625 foreach ($entries as $entry) {
1626 if (!iframe_whitelisted($entry)) {
1627 $entry->setAttribute('sandbox', 'allow-scripts');
1629 if (is_prefix_https()) {
1630 $entry->setAttribute("src",
1631 str_replace("http://", "https://",
1632 $entry->getAttribute("src")));
1637 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1638 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1639 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1640 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1641 'dt', 'em', 'footer', 'figure', 'figcaption',
1642 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1643 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1644 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1645 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1646 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1647 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1649 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1651 $disallowed_attributes = array('id', 'style', 'class');
1653 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1654 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1655 if (is_array($retval)) {
1657 $allowed_elements = $retval[1];
1658 $disallowed_attributes = $retval[2];
1664 $doc->removeChild($doc->firstChild
); //remove doctype
1665 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1667 if ($highlight_words) {
1668 foreach ($highlight_words as $word) {
1670 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1672 $elements = $xpath->query("//*/text()");
1674 foreach ($elements as $child) {
1676 $fragment = $doc->createDocumentFragment();
1677 $text = $child->textContent
;
1679 while (($pos = mb_stripos($text, $word)) !== false) {
1680 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1681 $word = mb_substr($text, $pos, mb_strlen($word));
1682 $highlight = $doc->createElement('span');
1683 $highlight->appendChild(new DomText($word));
1684 $highlight->setAttribute('class', 'highlight');
1685 $fragment->appendChild($highlight);
1686 $text = mb_substr($text, $pos +
mb_strlen($word));
1689 if (!empty($text)) $fragment->appendChild(new DomText($text));
1691 $child->parentNode
->replaceChild($fragment, $child);
1696 $res = $doc->saveHTML();
1698 /* strip everything outside of <body>...</body> */
1700 $res_frag = array();
1701 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1702 return $res_frag[1];
1708 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1709 $xpath = new DOMXPath($doc);
1710 $entries = $xpath->query('//*');
1712 foreach ($entries as $entry) {
1713 if (!in_array($entry->nodeName
, $allowed_elements)) {
1714 $entry->parentNode
->removeChild($entry);
1717 if ($entry->hasAttributes()) {
1718 $attrs_to_remove = array();
1720 foreach ($entry->attributes
as $attr) {
1722 if (strpos($attr->nodeName
, 'on') === 0) {
1723 array_push($attrs_to_remove, $attr);
1726 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1727 array_push($attrs_to_remove, $attr);
1730 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1731 array_push($attrs_to_remove, $attr);
1735 foreach ($attrs_to_remove as $attr) {
1736 $entry->removeAttributeNode($attr);
1744 function trim_array($array) {
1746 array_walk($tmp, 'trim');
1750 function tag_is_valid($tag) {
1751 if ($tag == '') return false;
1752 if (is_numeric($tag)) return false;
1753 if (mb_strlen($tag) > 250) return false;
1755 if (!$tag) return false;
1760 function render_login_form() {
1761 header('Cache-Control: public');
1763 require_once "login_form.php";
1767 function T_sprintf() {
1768 $args = func_get_args();
1769 return vsprintf(__(array_shift($args)), $args);
1772 function print_checkpoint($n, $s) {
1773 $ts = microtime(true);
1774 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1778 function sanitize_tag($tag) {
1781 $tag = mb_strtolower($tag, 'utf-8');
1783 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1785 if (DB_TYPE
== "mysql") {
1786 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1792 function is_server_https() {
1793 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1796 function is_prefix_https() {
1797 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1800 // this returns SELF_URL_PATH sans ending slash
1801 function get_self_url_prefix() {
1802 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1803 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1805 return SELF_URL_PATH
;
1809 function encrypt_password($pass, $salt = '', $mode2 = false) {
1810 if ($salt && $mode2) {
1811 return "MODE2:" . hash('sha256', $salt . $pass);
1813 return "SHA1X:" . sha1("$salt:$pass");
1815 return "SHA1:" . sha1($pass);
1817 } // function encrypt_password
1819 function load_filters($feed_id, $owner_uid) {
1822 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1825 $null_cat_qpart = "cat_id IS NULL OR";
1827 $null_cat_qpart = "";
1831 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1832 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1833 $sth->execute([$owner_uid]);
1835 $check_cats = array_merge(
1836 Feeds
::getParentCategories($cat_id, $owner_uid),
1839 $check_cats_str = join(",", $check_cats);
1840 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1842 while ($line = $sth->fetch()) {
1843 $filter_id = $line["id"];
1845 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1847 $sth2 = $pdo->prepare("SELECT
1848 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1849 FROM ttrss_filters2_rules AS r,
1850 ttrss_filter_types AS t
1852 (match_on IS NOT NULL OR
1853 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1854 (feed_id IS NULL OR feed_id = ?))) AND
1855 filter_type = t.id AND filter_id = ?");
1856 $sth2->execute([$feed_id, $filter_id]);
1861 while ($rule_line = $sth2->fetch()) {
1862 # print_r($rule_line);
1864 if ($rule_line["match_on"]) {
1865 $match_on = json_decode($rule_line["match_on"], true);
1867 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1870 $rule["reg_exp"] = $rule_line["reg_exp"];
1871 $rule["type"] = $rule_line["type_name"];
1872 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1874 array_push($rules, $rule);
1875 } else if (!$match_any_rule) {
1876 // this filter contains a rule that doesn't match to this feed/category combination
1877 // thus filter has to be rejected
1886 $rule["reg_exp"] = $rule_line["reg_exp"];
1887 $rule["type"] = $rule_line["type_name"];
1888 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1890 array_push($rules, $rule);
1894 if (count($rules) > 0) {
1895 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1896 FROM ttrss_filters2_actions AS a,
1897 ttrss_filter_actions AS t
1899 action_id = t.id AND filter_id = ?");
1900 $sth2->execute([$filter_id]);
1902 while ($action_line = $sth2->fetch()) {
1903 # print_r($action_line);
1906 $action["type"] = $action_line["type_name"];
1907 $action["param"] = $action_line["action_param"];
1909 array_push($actions, $action);
1914 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1915 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1916 $filter["rules"] = $rules;
1917 $filter["actions"] = $actions;
1919 if (count($rules) > 0 && count($actions) > 0) {
1920 array_push($filters, $filter);
1927 function get_score_pic($score) {
1929 return "score_high.png";
1930 } else if ($score > 0) {
1931 return "score_half_high.png";
1932 } else if ($score < -100) {
1933 return "score_low.png";
1934 } else if ($score < 0) {
1935 return "score_half_low.png";
1937 return "score_neutral.png";
1941 function feed_has_icon($id) {
1942 return is_file(ICONS_DIR
. "/$id.ico") && filesize(ICONS_DIR
. "/$id.ico") > 0;
1945 function init_plugins() {
1946 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1951 function add_feed_category($feed_cat, $parent_cat_id = false) {
1953 if (!$feed_cat) return false;
1955 ////db_query("BEGIN");
1957 $feed_cat = mb_substr($feed_cat, 0, 250);
1961 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1962 WHERE (:parent IS NULL AND parent_cat IS NULL OR parent_cat = :parent)
1963 AND title = :cat AND owner_uid = :uid");
1964 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
1966 if ($sth->fetch()) {
1968 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1970 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
1972 //db_query("COMMIT");
1981 * Fixes incomplete URLs by prepending "http://".
1982 * Also replaces feed:// with http://, and
1983 * prepends a trailing slash if the url is a domain name only.
1985 * @param string $url Possibly incomplete URL
1987 * @return string Fixed URL.
1989 function fix_url($url) {
1991 // support schema-less urls
1992 if (strpos($url, '//') === 0) {
1993 $url = 'https:' . $url;
1996 if (strpos($url, '://') === false) {
1997 $url = 'http://' . $url;
1998 } else if (substr($url, 0, 5) == 'feed:') {
1999 $url = 'http:' . substr($url, 5);
2002 //prepend slash if the URL has no slash in it
2003 // "http://www.example" -> "http://www.example/"
2004 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2008 //convert IDNA hostname to punycode if possible
2009 if (function_exists("idn_to_ascii")) {
2010 $parts = parse_url($url);
2011 if (mb_detect_encoding($parts['host']) != 'ASCII')
2013 $parts['host'] = idn_to_ascii($parts['host']);
2014 $url = build_url($parts);
2018 if ($url != "http:///")
2024 function validate_feed_url($url) {
2025 $parts = parse_url($url);
2027 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2031 /* function save_email_address($email) {
2032 // FIXME: implement persistent storage of emails
2034 if (!$_SESSION['stored_emails'])
2035 $_SESSION['stored_emails'] = array();
2037 if (!in_array($email, $_SESSION['stored_emails']))
2038 array_push($_SESSION['stored_emails'], $email);
2042 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2044 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2046 $sql_is_cat = bool_to_sql_bool($is_cat);
2048 $result = db_query("SELECT access_key FROM ttrss_access_keys
2049 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
2050 AND owner_uid = " . $owner_uid);
2052 if (db_num_rows($result) == 1) {
2053 return db_fetch_result($result, 0, "access_key");
2055 $key = db_escape_string(uniqid_short());
2057 $result = db_query("INSERT INTO ttrss_access_keys
2058 (access_key, feed_id, is_cat, owner_uid)
2059 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
2066 function get_feeds_from_html($url, $content)
2068 $url = fix_url($url);
2069 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2071 libxml_use_internal_errors(true);
2073 $doc = new DOMDocument();
2074 $doc->loadHTML($content);
2075 $xpath = new DOMXPath($doc);
2076 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2077 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2078 $feedUrls = array();
2079 foreach ($entries as $entry) {
2080 if ($entry->hasAttribute('href')) {
2081 $title = $entry->getAttribute('title');
2083 $title = $entry->getAttribute('type');
2085 $feedUrl = rewrite_relative_url(
2086 $baseUrl, $entry->getAttribute('href')
2088 $feedUrls[$feedUrl] = $title;
2094 function is_html($content) {
2095 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2098 function url_is_html($url, $login = false, $pass = false) {
2099 return is_html(fetch_file_contents($url, false, $login, $pass));
2102 function build_url($parts) {
2103 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2106 function cleanup_url_path($path) {
2107 $path = str_replace("/./", "/", $path);
2108 $path = str_replace("//", "/", $path);
2114 * Converts a (possibly) relative URL to a absolute one.
2116 * @param string $url Base URL (i.e. from where the document is)
2117 * @param string $rel_url Possibly relative URL in the document
2119 * @return string Absolute URL
2121 function rewrite_relative_url($url, $rel_url) {
2122 if (strpos($rel_url, "://") !== false) {
2124 } else if (strpos($rel_url, "//") === 0) {
2125 # protocol-relative URL (rare but they exist)
2127 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2128 # magnet:, feed:, etc
2130 } else if (strpos($rel_url, "/") === 0) {
2131 $parts = parse_url($url);
2132 $parts['path'] = $rel_url;
2133 $parts['path'] = cleanup_url_path($parts['path']);
2135 return build_url($parts);
2138 $parts = parse_url($url);
2139 if (!isset($parts['path'])) {
2140 $parts['path'] = '/';
2142 $dir = $parts['path'];
2143 if (substr($dir, -1) !== '/') {
2144 $dir = dirname($parts['path']);
2145 $dir !== '/' && $dir .= '/';
2147 $parts['path'] = $dir . $rel_url;
2148 $parts['path'] = cleanup_url_path($parts['path']);
2150 return build_url($parts);
2154 function cleanup_tags($days = 14, $limit = 1000) {
2156 if (DB_TYPE
== "pgsql") {
2157 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2158 } else if (DB_TYPE
== "mysql") {
2159 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2164 while ($limit > 0) {
2167 $query = "SELECT ttrss_tags.id AS id
2168 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2169 WHERE post_int_id = int_id AND $interval_query AND
2170 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2172 $result = db_query($query);
2176 while ($line = db_fetch_assoc($result)) {
2177 array_push($ids, $line['id']);
2180 if (count($ids) > 0) {
2181 $ids = join(",", $ids);
2183 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2184 $tags_deleted +
= db_affected_rows($tmp_result);
2189 $limit -= $limit_part;
2192 return $tags_deleted;
2195 function print_user_stylesheet() {
2196 $value = get_pref('USER_STYLESHEET');
2199 print "<style type=\"text/css\">";
2200 print str_replace("<br/>", "\n", $value);
2206 function filter_to_sql($filter, $owner_uid) {
2209 if (DB_TYPE
== "pgsql")
2212 $reg_qpart = "REGEXP";
2214 foreach ($filter["rules"] AS $rule) {
2215 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2216 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2217 $rule['reg_exp']) !== FALSE;
2219 if ($regexp_valid) {
2221 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2223 switch ($rule["type"]) {
2225 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2226 $rule['reg_exp'] . "')";
2229 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2230 $rule['reg_exp'] . "')";
2233 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2234 $rule['reg_exp'] . "') OR LOWER(" .
2235 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2238 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2239 $rule['reg_exp'] . "')";
2242 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2243 $rule['reg_exp'] . "')";
2246 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2247 $rule['reg_exp'] . "')";
2251 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2253 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2254 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2257 if (isset($rule["cat_id"])) {
2259 if ($rule["cat_id"] > 0) {
2260 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2261 array_push($children, $rule["cat_id"]);
2263 $children = join(",", $children);
2265 $cat_qpart = "cat_id IN ($children)";
2267 $cat_qpart = "cat_id IS NULL";
2270 $qpart .= " AND $cat_qpart";
2273 $qpart .= " AND feed_id IS NOT NULL";
2275 array_push($query, "($qpart)");
2280 if (count($query) > 0) {
2281 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2283 $fullquery = "(false)";
2286 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2291 if (!function_exists('gzdecode')) {
2292 function gzdecode($string) { // no support for 2nd argument
2293 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2294 base64_encode($string));
2298 function get_random_bytes($length) {
2299 if (function_exists('openssl_random_pseudo_bytes')) {
2300 return openssl_random_pseudo_bytes($length);
2304 for ($i = 0; $i < $length; $i++
)
2305 $output .= chr(mt_rand(0, 255));
2311 function read_stdin() {
2312 $fp = fopen("php://stdin", "r");
2315 $line = trim(fgets($fp));
2323 function implements_interface($class, $interface) {
2324 return in_array($interface, class_implements($class));
2327 function get_minified_js($files) {
2328 require_once 'lib/jshrink/Minifier.php';
2332 foreach ($files as $js) {
2333 if (!isset($_GET['debug'])) {
2334 $cached_file = CACHE_DIR
. "/js/".basename($js).".js";
2336 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2338 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2340 if ($header && $contents) {
2341 list($htag, $hversion) = explode(":", $header);
2343 if ($htag == "tt-rss" && $hversion == VERSION
) {
2350 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js.js"));
2351 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2355 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2362 function calculate_dep_timestamp() {
2363 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2367 foreach ($files as $file) {
2368 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2374 function T_js_decl($s1, $s2) {
2376 $s1 = preg_replace("/\n/", "", $s1);
2377 $s2 = preg_replace("/\n/", "", $s2);
2379 $s1 = preg_replace("/\"/", "\\\"", $s1);
2380 $s2 = preg_replace("/\"/", "\\\"", $s2);
2382 return "T_messages[\"$s1\"] = \"$s2\";\n";
2386 function init_js_translations() {
2388 print 'var T_messages = new Object();
2391 if (T_messages[msg]) {
2392 return T_messages[msg];
2398 function ngettext(msg1, msg2, n) {
2399 return __((parseInt(n) > 1) ? msg2 : msg1);
2402 $l10n = _get_reader();
2404 for ($i = 0; $i < $l10n->total
; $i++
) {
2405 $orig = $l10n->get_original_string($i);
2406 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2407 $key = explode(chr(0), $orig);
2408 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2409 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2411 $translation = __($orig);
2412 print T_js_decl($orig, $translation);
2417 function get_theme_path($theme) {
2418 $check = "themes/$theme";
2419 if (file_exists($check)) return $check;
2421 $check = "themes.local/$theme";
2422 if (file_exists($check)) return $check;
2425 function theme_valid($theme) {
2426 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2428 if (in_array($theme, $bundled_themes)) return true;
2430 $file = "themes/" . basename($theme);
2432 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2434 if (file_exists($file) && is_readable($file)) {
2435 $fh = fopen($file, "r");
2438 $header = fgets($fh);
2441 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2449 * @SuppressWarnings(unused)
2451 function error_json($code) {
2452 require_once "errors.php";
2454 @$message = $ERRORS[$code];
2456 return json_encode(array("error" =>
2457 array("code" => $code, "message" => $message)));
2461 /*function abs_to_rel_path($dir) {
2462 $tmp = str_replace(dirname(__DIR__), "", $dir);
2464 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2469 function get_upload_error_message($code) {
2472 0 => __('There is no error, the file uploaded with success'),
2473 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2474 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2475 3 => __('The uploaded file was only partially uploaded'),
2476 4 => __('No file was uploaded'),
2477 6 => __('Missing a temporary folder'),
2478 7 => __('Failed to write file to disk.'),
2479 8 => __('A PHP extension stopped the file upload.'),
2482 return $errors[$code];
2485 function base64_img($filename) {
2486 if (file_exists($filename)) {
2487 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2489 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2495 /* this is essentially a wrapper for readfile() which allows plugins to hook
2496 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2498 hook function should return true if request was handled (or at least attempted to)
2500 note that this can be called without user context so the plugin to handle this
2501 should be loaded systemwide in config.php */
2502 function send_local_file($filename) {
2503 if (file_exists($filename)) {
2504 $tmppluginhost = new PluginHost();
2506 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2507 $tmppluginhost->load_data();
2509 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2510 if ($plugin->hook_send_local_file($filename)) return true;
2513 $mimetype = mime_content_type($filename);
2514 header("Content-type: $mimetype");
2516 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2517 header("Last-Modified: $stamp", true);
2519 return readfile($filename);
2525 function check_mysql_tables() {
2526 $schema = db_escape_string(DB_NAME
);
2528 $result = db_query("SELECT engine, table_name FROM information_schema.tables WHERE
2529 table_schema = '$schema' AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2533 while ($line = db_fetch_assoc($result)) {
2534 array_push($bad_tables, $line);
2540 function arr_qmarks($arr) {
2541 return str_repeat('?,', count($arr) - 1) . '?';