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 = ? AND profile = ?");
604 $u_sth->execute([$uid, $profile]);
606 $active_prefs = array();
608 while ($line = $u_sth->fetch()) {
609 array_push($active_prefs, $line["pref_name"]);
612 while ($line = $sth->fetch()) {
613 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
614 // print "adding " . $line["pref_name"] . "<br>";
616 $line["def_value"] = db_escape_string($line["def_value"]);
617 $line["pref_name"] = db_escape_string($line["pref_name"]);
619 if (get_schema_version() < 63) {
620 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
621 (owner_uid,pref_name,value) VALUES
623 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
626 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
627 (owner_uid,pref_name,value, profile) VALUES
629 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
635 ////db_query("COMMIT");
639 function get_ssl_certificate_id() {
640 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
641 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
642 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
643 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
644 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
646 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
647 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
648 $_SERVER["SSL_CLIENT_V_START"] .
649 $_SERVER["SSL_CLIENT_V_END"] .
650 $_SERVER["SSL_CLIENT_S_DN"]);
655 function authenticate_user($login, $password, $check_only = false) {
657 if (!SINGLE_USER_MODE
) {
660 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
662 $user_id = (int) $plugin->authenticate($login, $password);
665 $_SESSION["auth_module"] = strtolower(get_class($plugin));
670 if ($user_id && !$check_only) {
673 $_SESSION["uid"] = $user_id;
674 $_SESSION["version"] = VERSION_STATIC
;
677 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
679 $sth->execute([$user_id]);
680 $row = $sth->fetch();
682 $_SESSION["name"] = $row["login"];
683 $_SESSION["access_level"] = $row["access_level"];
684 $_SESSION["csrf_token"] = uniqid_short();
686 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
687 $usth->execute([$user_id]);
689 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
690 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
691 $_SESSION["pwd_hash"] = $row["pwd_hash"];
693 $_SESSION["last_version_check"] = time();
695 initialize_user_prefs($_SESSION["uid"]);
704 $_SESSION["uid"] = 1;
705 $_SESSION["name"] = "admin";
706 $_SESSION["access_level"] = 10;
708 $_SESSION["hide_hello"] = true;
709 $_SESSION["hide_logout"] = true;
711 $_SESSION["auth_module"] = false;
713 if (!$_SESSION["csrf_token"]) {
714 $_SESSION["csrf_token"] = uniqid_short();
717 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
719 initialize_user_prefs($_SESSION["uid"]);
725 function make_password($length = 8) {
728 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
732 while ($i < $length) {
733 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
735 if (!strstr($password, $char)) {
743 // this is called after user is created to initialize default feeds, labels
746 // user preferences are checked on every login, not here
748 function initialize_user($uid) {
752 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
753 values (?, 'Tiny Tiny RSS: Forum',
754 'http://tt-rss.org/forum/rss.php')");
755 $sth->execute([$uid]);
758 function logout_user() {
760 if (isset($_COOKIE[session_name()])) {
761 setcookie(session_name(), '', time()-42000, '/');
765 function validate_csrf($csrf_token) {
766 return $csrf_token == $_SESSION['csrf_token'];
769 function load_user_plugins($owner_uid, $pluginhost = false) {
771 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
773 if ($owner_uid && SCHEMA_VERSION
>= 100) {
774 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
776 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
778 if (get_schema_version() > 100) {
779 $pluginhost->load_data();
784 function login_sequence() {
787 if (SINGLE_USER_MODE
) {
789 authenticate_user("admin", null);
791 load_user_plugins($_SESSION["uid"]);
793 if (!validate_session()) $_SESSION["uid"] = false;
795 if (!$_SESSION["uid"]) {
797 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
798 $_SESSION["ref_schema_version"] = get_schema_version(true);
800 authenticate_user(null, null, true);
803 if (!$_SESSION["uid"]) {
805 setcookie(session_name(), '', time()-42000, '/');
812 /* bump login timestamp */
813 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
814 $sth->execute([$_SESSION['uid']]);
816 $_SESSION["last_login_update"] = time();
819 if ($_SESSION["uid"]) {
821 load_user_plugins($_SESSION["uid"]);
825 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
827 (SELECT COUNT(id) FROM ttrss_feeds WHERE
828 ttrss_feeds.id = feed_id) = 0");
830 $sth->execute($_SESSION['uid']);
832 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
834 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
835 ttrss_feed_categories.id = feed_id) = 0");
837 $sth->execute($_SESSION['uid']);
843 function truncate_string($str, $max_len, $suffix = '…') {
844 if (mb_strlen($str, "utf-8") > $max_len) {
845 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
852 function truncate_middle($str, $max_len, $suffix = '…') {
853 if (strlen($str) > $max_len) {
854 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
860 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
863 $source_tz = new DateTimeZone($source_tz);
864 } catch (Exception
$e) {
865 $source_tz = new DateTimeZone('UTC');
869 $dest_tz = new DateTimeZone($dest_tz);
870 } catch (Exception
$e) {
871 $dest_tz = new DateTimeZone('UTC');
874 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
875 return $dt->format('U') +
$dest_tz->getOffset($dt);
878 function make_local_datetime($timestamp, $long, $owner_uid = false,
879 $no_smart_dt = false, $eta_min = false) {
881 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
882 if (!$timestamp) $timestamp = '1970-01-01 0:00';
887 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
889 $timestamp = substr($timestamp, 0, 19);
891 # We store date in UTC internally
892 $dt = new DateTime($timestamp, $utc_tz);
894 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
896 if ($user_tz_string != 'Automatic') {
899 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
900 } catch (Exception
$e) {
904 $tz_offset = $user_tz->getOffset($dt);
906 $tz_offset = (int) -$_SESSION["clientTzOffset"];
909 $user_timestamp = $dt->format('U') +
$tz_offset;
912 return smart_date_time($user_timestamp,
913 $tz_offset, $owner_uid, $eta_min);
916 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
918 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
920 return date($format, $user_timestamp);
924 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
925 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
927 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
928 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
929 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
930 return date("G:i", $timestamp);
931 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
932 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
933 return date($format, $timestamp);
935 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
936 return date($format, $timestamp);
940 function sql_bool_to_bool($s) {
941 if ($s == "t" ||
$s == "1" ||
strtolower($s) == "true") {
948 function bool_to_sql_bool($s) {
956 // Session caching removed due to causing wrong redirects to upgrade
957 // script when get_schema_version() is called on an obsolete session
958 // created on a previous schema version.
959 function get_schema_version($nocache = false) {
960 global $schema_version;
964 if (!$schema_version && !$nocache) {
965 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
966 $version = $row["schema_version"];
967 $schema_version = $version;
970 return $schema_version;
974 function sanity_check() {
975 require_once 'errors.php';
979 $schema_version = get_schema_version(true);
981 if ($schema_version != SCHEMA_VERSION
) {
985 if (db_escape_string("testTEST") != "testTEST") {
989 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
992 function file_is_locked($filename) {
993 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
994 if (function_exists('flock')) {
995 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
997 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1008 return true; // consider the file always locked and skip the test
1015 function make_lockfile($filename) {
1016 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1018 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1019 $stat_h = fstat($fp);
1020 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1022 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1023 if ($stat_h["ino"] != $stat_f["ino"] ||
1024 $stat_h["dev"] != $stat_f["dev"]) {
1030 if (function_exists('posix_getpid')) {
1031 fwrite($fp, posix_getpid() . "\n");
1039 function make_stampfile($filename) {
1040 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1042 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1043 fwrite($fp, time() . "\n");
1044 flock($fp, LOCK_UN
);
1052 function sql_random_function() {
1053 if (DB_TYPE
== "mysql") {
1060 function getFeedUnread($feed, $is_cat = false) {
1061 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1064 function checkbox_to_sql_bool($val) {
1065 return ($val == "on") ?
"true" : "false";
1068 function uniqid_short() {
1069 return uniqid(base_convert(rand(), 10, 36));
1072 function make_init_params() {
1075 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1076 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1077 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1078 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1080 $params[strtolower($param)] = (int) get_pref($param);
1083 $params["icons_url"] = ICONS_URL
;
1084 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1085 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1086 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1087 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1088 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1089 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1091 $theme = get_pref( "USER_CSS_THEME", false, false);
1092 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1094 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1096 $params["php_platform"] = PHP_OS
;
1097 $params["php_version"] = PHP_VERSION
;
1099 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1103 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1104 ttrss_feeds WHERE owner_uid = ?");
1105 $sth->execute([$_SESSION['uid']]);
1106 $row = $sth->fetch();
1108 $max_feed_id = $row["mid"];
1109 $num_feeds = $row["nf"];
1111 $params["max_feed_id"] = (int) $max_feed_id;
1112 $params["num_feeds"] = (int) $num_feeds;
1114 $params["hotkeys"] = get_hotkeys_map();
1116 $params["csrf_token"] = $_SESSION["csrf_token"];
1117 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1119 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1121 $params["icon_alert"] = base64_img("images/alert.png");
1122 $params["icon_information"] = base64_img("images/information.png");
1123 $params["icon_cross"] = base64_img("images/cross.png");
1124 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1126 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1131 function get_hotkeys_info() {
1133 __("Navigation") => array(
1134 "next_feed" => __("Open next feed"),
1135 "prev_feed" => __("Open previous feed"),
1136 "next_article" => __("Open next article"),
1137 "prev_article" => __("Open previous article"),
1138 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1139 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1140 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1141 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1142 "search_dialog" => __("Show search dialog")),
1143 __("Article") => array(
1144 "toggle_mark" => __("Toggle starred"),
1145 "toggle_publ" => __("Toggle published"),
1146 "toggle_unread" => __("Toggle unread"),
1147 "edit_tags" => __("Edit tags"),
1148 "open_in_new_window" => __("Open in new window"),
1149 "catchup_below" => __("Mark below as read"),
1150 "catchup_above" => __("Mark above as read"),
1151 "article_scroll_down" => __("Scroll down"),
1152 "article_scroll_up" => __("Scroll up"),
1153 "select_article_cursor" => __("Select article under cursor"),
1154 "email_article" => __("Email article"),
1155 "close_article" => __("Close/collapse article"),
1156 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1157 "toggle_widescreen" => __("Toggle widescreen mode"),
1158 "toggle_embed_original" => __("Toggle embed original")),
1159 __("Article selection") => array(
1160 "select_all" => __("Select all articles"),
1161 "select_unread" => __("Select unread"),
1162 "select_marked" => __("Select starred"),
1163 "select_published" => __("Select published"),
1164 "select_invert" => __("Invert selection"),
1165 "select_none" => __("Deselect everything")),
1166 __("Feed") => array(
1167 "feed_refresh" => __("Refresh current feed"),
1168 "feed_unhide_read" => __("Un/hide read feeds"),
1169 "feed_subscribe" => __("Subscribe to feed"),
1170 "feed_edit" => __("Edit feed"),
1171 "feed_catchup" => __("Mark as read"),
1172 "feed_reverse" => __("Reverse headlines"),
1173 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1174 "feed_debug_update" => __("Debug feed update"),
1175 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1176 "catchup_all" => __("Mark all feeds as read"),
1177 "cat_toggle_collapse" => __("Un/collapse current category"),
1178 "toggle_combined_mode" => __("Toggle combined mode"),
1179 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1180 __("Go to") => array(
1181 "goto_all" => __("All articles"),
1182 "goto_fresh" => __("Fresh"),
1183 "goto_marked" => __("Starred"),
1184 "goto_published" => __("Published"),
1185 "goto_tagcloud" => __("Tag cloud"),
1186 "goto_prefs" => __("Preferences")),
1187 __("Other") => array(
1188 "create_label" => __("Create label"),
1189 "create_filter" => __("Create filter"),
1190 "collapse_sidebar" => __("Un/collapse sidebar"),
1191 "help_dialog" => __("Show help dialog"))
1194 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1195 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1201 function get_hotkeys_map() {
1203 // "navigation" => array(
1206 "n" => "next_article",
1207 "p" => "prev_article",
1208 "(38)|up" => "prev_article",
1209 "(40)|down" => "next_article",
1210 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1211 // "^(40)|Ctrl-down" => "next_article_noscroll",
1212 "(191)|/" => "search_dialog",
1213 // "article" => array(
1214 "s" => "toggle_mark",
1215 "*s" => "toggle_publ",
1216 "u" => "toggle_unread",
1217 "*t" => "edit_tags",
1218 "o" => "open_in_new_window",
1219 "c p" => "catchup_below",
1220 "c n" => "catchup_above",
1221 "*n" => "article_scroll_down",
1222 "*p" => "article_scroll_up",
1223 "*(38)|Shift+up" => "article_scroll_up",
1224 "*(40)|Shift+down" => "article_scroll_down",
1225 "a *w" => "toggle_widescreen",
1226 "a e" => "toggle_embed_original",
1227 "e" => "email_article",
1228 "a q" => "close_article",
1229 // "article_selection" => array(
1230 "a a" => "select_all",
1231 "a u" => "select_unread",
1232 "a *u" => "select_marked",
1233 "a p" => "select_published",
1234 "a i" => "select_invert",
1235 "a n" => "select_none",
1237 "f r" => "feed_refresh",
1238 "f a" => "feed_unhide_read",
1239 "f s" => "feed_subscribe",
1240 "f e" => "feed_edit",
1241 "f q" => "feed_catchup",
1242 "f x" => "feed_reverse",
1243 "f g" => "feed_toggle_vgroup",
1244 "f *d" => "feed_debug_update",
1245 "f *g" => "feed_debug_viewfeed",
1246 "f *c" => "toggle_combined_mode",
1247 "f c" => "toggle_cdm_expanded",
1248 "*q" => "catchup_all",
1249 "x" => "cat_toggle_collapse",
1251 "g a" => "goto_all",
1252 "g f" => "goto_fresh",
1253 "g s" => "goto_marked",
1254 "g p" => "goto_published",
1255 "g t" => "goto_tagcloud",
1256 "g *p" => "goto_prefs",
1257 // "other" => array(
1258 "(9)|Tab" => "select_article_cursor", // tab
1259 "c l" => "create_label",
1260 "c f" => "create_filter",
1261 "c s" => "collapse_sidebar",
1262 "^(191)|Ctrl+/" => "help_dialog",
1265 if (get_pref('COMBINED_DISPLAY_MODE')) {
1266 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1267 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1270 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1271 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1274 $prefixes = array();
1276 foreach (array_keys($hotkeys) as $hotkey) {
1277 $pair = explode(" ", $hotkey, 2);
1279 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1280 array_push($prefixes, $pair[0]);
1284 return array($prefixes, $hotkeys);
1287 function check_for_update() {
1288 if (defined("GIT_VERSION_TIMESTAMP")) {
1289 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1292 $content = json_decode($content, true);
1294 if ($content && isset($content["changeset"])) {
1295 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1296 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1298 return $content["changeset"]["id"];
1307 function make_runtime_info($disable_update_check = false) {
1312 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1313 ttrss_feeds WHERE owner_uid = ?");
1314 $sth->execute([$_SESSION['uid']]);
1315 $row = $sth->fetch();
1317 $max_feed_id = $row['mid'];
1318 $num_feeds = $row['nf'];
1320 $data["max_feed_id"] = (int) $max_feed_id;
1321 $data["num_feeds"] = (int) $num_feeds;
1323 $data['last_article_id'] = Article
::getLastArticleId();
1324 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1326 $data['dep_ts'] = calculate_dep_timestamp();
1327 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1329 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1331 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1332 $update_result = @check_for_update
();
1334 $data["update_result"] = $update_result;
1336 $_SESSION["last_version_check"] = time();
1339 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1341 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1343 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1345 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1348 $stamp_delta = time() - $stamp;
1350 if ($stamp_delta > 1800) {
1354 $_SESSION["daemon_stamp_check"] = time();
1357 $data['daemon_stamp_ok'] = $stamp_check;
1359 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1361 $data['daemon_stamp'] = $stamp_fmt;
1369 function search_to_sql($search, $search_language) {
1371 $keywords = str_getcsv(trim($search), " ");
1372 $query_keywords = array();
1373 $search_words = array();
1374 $search_query_leftover = array();
1376 if ($search_language)
1377 $search_language = db_escape_string(mb_strtolower($search_language));
1379 $search_language = "english";
1381 foreach ($keywords as $k) {
1382 if (strpos($k, "-") === 0) {
1389 $commandpair = explode(":", mb_strtolower($k), 2);
1391 switch ($commandpair[0]) {
1393 if ($commandpair[1]) {
1394 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
1395 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1397 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1398 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1399 array_push($search_words, $k);
1403 if ($commandpair[1]) {
1404 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
1405 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1407 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1408 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1409 array_push($search_words, $k);
1413 if ($commandpair[1]) {
1414 if ($commandpair[1] == "true")
1415 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1416 else if ($commandpair[1] == "false")
1417 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1419 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
1420 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1422 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1423 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1424 if (!$not) array_push($search_words, $k);
1429 if ($commandpair[1]) {
1430 if ($commandpair[1] == "true")
1431 array_push($query_keywords, "($not (marked = true))");
1433 array_push($query_keywords, "($not (marked = false))");
1435 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1436 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1437 if (!$not) array_push($search_words, $k);
1441 if ($commandpair[1]) {
1442 if ($commandpair[1] == "true")
1443 array_push($query_keywords, "($not (published = true))");
1445 array_push($query_keywords, "($not (published = false))");
1448 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1449 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1450 if (!$not) array_push($search_words, $k);
1454 if ($commandpair[1]) {
1455 if ($commandpair[1] == "true")
1456 array_push($query_keywords, "($not (unread = true))");
1458 array_push($query_keywords, "($not (unread = false))");
1461 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1462 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1463 if (!$not) array_push($search_words, $k);
1467 if (strpos($k, "@") === 0) {
1469 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1470 $orig_ts = strtotime(substr($k, 1));
1471 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1473 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1475 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1478 if (DB_TYPE
== "pgsql") {
1479 $k = mb_strtolower($k);
1480 array_push($search_query_leftover, $not ?
"!$k" : $k);
1482 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1483 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1486 if (!$not) array_push($search_words, $k);
1491 if (count($search_query_leftover) > 0) {
1492 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
1494 if (DB_TYPE
== "pgsql") {
1495 array_push($query_keywords,
1496 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
1501 $search_query_part = implode("AND", $query_keywords);
1503 return array($search_query_part, $search_words);
1506 function iframe_whitelisted($entry) {
1507 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1509 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1512 foreach ($whitelist as $w) {
1513 if ($src == $w ||
$src == "www.$w")
1521 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1522 if (!$owner) $owner = $_SESSION["uid"];
1524 $res = trim($str); if (!$res) return '';
1526 $charset_hack = '<head>
1527 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1530 $res = trim($res); if (!$res) return '';
1532 libxml_use_internal_errors(true);
1534 $doc = new DOMDocument();
1535 $doc->loadHTML($charset_hack . $res);
1536 $xpath = new DOMXPath($doc);
1538 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1540 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1542 foreach ($entries as $entry) {
1544 if ($entry->hasAttribute('href')) {
1545 $entry->setAttribute('href',
1546 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1548 $entry->setAttribute('rel', 'noopener noreferrer');
1551 if ($entry->hasAttribute('src')) {
1552 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1553 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1555 if (file_exists($cached_filename)) {
1557 // this is strictly cosmetic
1558 if ($entry->tagName
== 'img') {
1560 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1562 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1568 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1570 if ($entry->hasAttribute('srcset')) {
1571 $entry->removeAttribute('srcset');
1574 if ($entry->hasAttribute('sizes')) {
1575 $entry->removeAttribute('sizes');
1579 $entry->setAttribute('src', $src);
1582 if ($entry->nodeName
== 'img') {
1584 if ($entry->hasAttribute('src')) {
1585 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1587 if (is_prefix_https() && !$is_https_url) {
1589 if ($entry->hasAttribute('srcset')) {
1590 $entry->removeAttribute('srcset');
1593 if ($entry->hasAttribute('sizes')) {
1594 $entry->removeAttribute('sizes');
1599 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1600 $force_remove_images ||
$_SESSION["bw_limit"]) {
1602 $p = $doc->createElement('p');
1604 $a = $doc->createElement('a');
1605 $a->setAttribute('href', $entry->getAttribute('src'));
1607 $a->appendChild(new DOMText($entry->getAttribute('src')));
1608 $a->setAttribute('target', '_blank');
1609 $a->setAttribute('rel', 'noopener noreferrer');
1611 $p->appendChild($a);
1613 $entry->parentNode
->replaceChild($p, $entry);
1617 if (strtolower($entry->nodeName
) == "a") {
1618 $entry->setAttribute("target", "_blank");
1619 $entry->setAttribute("rel", "noopener noreferrer");
1623 $entries = $xpath->query('//iframe');
1624 foreach ($entries as $entry) {
1625 if (!iframe_whitelisted($entry)) {
1626 $entry->setAttribute('sandbox', 'allow-scripts');
1628 if (is_prefix_https()) {
1629 $entry->setAttribute("src",
1630 str_replace("http://", "https://",
1631 $entry->getAttribute("src")));
1636 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1637 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1638 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1639 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1640 'dt', 'em', 'footer', 'figure', 'figcaption',
1641 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1642 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1643 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1644 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1645 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1646 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1648 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1650 $disallowed_attributes = array('id', 'style', 'class');
1652 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1653 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1654 if (is_array($retval)) {
1656 $allowed_elements = $retval[1];
1657 $disallowed_attributes = $retval[2];
1663 $doc->removeChild($doc->firstChild
); //remove doctype
1664 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1666 if ($highlight_words) {
1667 foreach ($highlight_words as $word) {
1669 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1671 $elements = $xpath->query("//*/text()");
1673 foreach ($elements as $child) {
1675 $fragment = $doc->createDocumentFragment();
1676 $text = $child->textContent
;
1678 while (($pos = mb_stripos($text, $word)) !== false) {
1679 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1680 $word = mb_substr($text, $pos, mb_strlen($word));
1681 $highlight = $doc->createElement('span');
1682 $highlight->appendChild(new DomText($word));
1683 $highlight->setAttribute('class', 'highlight');
1684 $fragment->appendChild($highlight);
1685 $text = mb_substr($text, $pos +
mb_strlen($word));
1688 if (!empty($text)) $fragment->appendChild(new DomText($text));
1690 $child->parentNode
->replaceChild($fragment, $child);
1695 $res = $doc->saveHTML();
1697 /* strip everything outside of <body>...</body> */
1699 $res_frag = array();
1700 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1701 return $res_frag[1];
1707 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1708 $xpath = new DOMXPath($doc);
1709 $entries = $xpath->query('//*');
1711 foreach ($entries as $entry) {
1712 if (!in_array($entry->nodeName
, $allowed_elements)) {
1713 $entry->parentNode
->removeChild($entry);
1716 if ($entry->hasAttributes()) {
1717 $attrs_to_remove = array();
1719 foreach ($entry->attributes
as $attr) {
1721 if (strpos($attr->nodeName
, 'on') === 0) {
1722 array_push($attrs_to_remove, $attr);
1725 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1726 array_push($attrs_to_remove, $attr);
1729 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1730 array_push($attrs_to_remove, $attr);
1734 foreach ($attrs_to_remove as $attr) {
1735 $entry->removeAttributeNode($attr);
1743 function trim_array($array) {
1745 array_walk($tmp, 'trim');
1749 function tag_is_valid($tag) {
1750 if ($tag == '') return false;
1751 if (is_numeric($tag)) return false;
1752 if (mb_strlen($tag) > 250) return false;
1754 if (!$tag) return false;
1759 function render_login_form() {
1760 header('Cache-Control: public');
1762 require_once "login_form.php";
1766 function T_sprintf() {
1767 $args = func_get_args();
1768 return vsprintf(__(array_shift($args)), $args);
1771 function print_checkpoint($n, $s) {
1772 $ts = microtime(true);
1773 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1777 function sanitize_tag($tag) {
1780 $tag = mb_strtolower($tag, 'utf-8');
1782 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1784 if (DB_TYPE
== "mysql") {
1785 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1791 function is_server_https() {
1792 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1795 function is_prefix_https() {
1796 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1799 // this returns SELF_URL_PATH sans ending slash
1800 function get_self_url_prefix() {
1801 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1802 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1804 return SELF_URL_PATH
;
1808 function encrypt_password($pass, $salt = '', $mode2 = false) {
1809 if ($salt && $mode2) {
1810 return "MODE2:" . hash('sha256', $salt . $pass);
1812 return "SHA1X:" . sha1("$salt:$pass");
1814 return "SHA1:" . sha1($pass);
1816 } // function encrypt_password
1818 function load_filters($feed_id, $owner_uid) {
1821 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1824 $null_cat_qpart = "cat_id IS NULL OR";
1826 $null_cat_qpart = "";
1830 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1831 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1832 $sth->execute([$owner_uid]);
1834 $check_cats = array_merge(
1835 Feeds
::getParentCategories($cat_id, $owner_uid),
1838 $check_cats_str = join(",", $check_cats);
1839 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1841 while ($line = $sth->fetch()) {
1842 $filter_id = $line["id"];
1844 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1846 $sth2 = $pdo->prepare("SELECT
1847 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1848 FROM ttrss_filters2_rules AS r,
1849 ttrss_filter_types AS t
1851 (match_on IS NOT NULL OR
1852 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1853 (feed_id IS NULL OR feed_id = ?))) AND
1854 filter_type = t.id AND filter_id = ?");
1855 $sth2->execute([$feed_id, $filter_id]);
1860 while ($rule_line = $sth2->fetch()) {
1861 # print_r($rule_line);
1863 if ($rule_line["match_on"]) {
1864 $match_on = json_decode($rule_line["match_on"], true);
1866 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1869 $rule["reg_exp"] = $rule_line["reg_exp"];
1870 $rule["type"] = $rule_line["type_name"];
1871 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1873 array_push($rules, $rule);
1874 } else if (!$match_any_rule) {
1875 // this filter contains a rule that doesn't match to this feed/category combination
1876 // thus filter has to be rejected
1885 $rule["reg_exp"] = $rule_line["reg_exp"];
1886 $rule["type"] = $rule_line["type_name"];
1887 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1889 array_push($rules, $rule);
1893 if (count($rules) > 0) {
1894 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1895 FROM ttrss_filters2_actions AS a,
1896 ttrss_filter_actions AS t
1898 action_id = t.id AND filter_id = ?");
1899 $sth2->execute([$filter_id]);
1901 while ($action_line = $sth2->fetch()) {
1902 # print_r($action_line);
1905 $action["type"] = $action_line["type_name"];
1906 $action["param"] = $action_line["action_param"];
1908 array_push($actions, $action);
1913 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1914 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1915 $filter["rules"] = $rules;
1916 $filter["actions"] = $actions;
1918 if (count($rules) > 0 && count($actions) > 0) {
1919 array_push($filters, $filter);
1926 function get_score_pic($score) {
1928 return "score_high.png";
1929 } else if ($score > 0) {
1930 return "score_half_high.png";
1931 } else if ($score < -100) {
1932 return "score_low.png";
1933 } else if ($score < 0) {
1934 return "score_half_low.png";
1936 return "score_neutral.png";
1940 function feed_has_icon($id) {
1941 return is_file(ICONS_DIR
. "/$id.ico") && filesize(ICONS_DIR
. "/$id.ico") > 0;
1944 function init_plugins() {
1945 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1950 function add_feed_category($feed_cat, $parent_cat_id = false) {
1952 if (!$feed_cat) return false;
1954 ////db_query("BEGIN");
1956 if ($parent_cat_id) {
1957 $parent_qpart = "parent_cat = '$parent_cat_id'";
1958 $parent_insert = "'$parent_cat_id'";
1960 $parent_qpart = "parent_cat IS NULL";
1961 $parent_insert = "NULL";
1964 $feed_cat = mb_substr($feed_cat, 0, 250);
1968 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1969 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ?");
1971 if (db_num_rows($result) == 0) {
1974 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1975 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
1986 * Fixes incomplete URLs by prepending "http://".
1987 * Also replaces feed:// with http://, and
1988 * prepends a trailing slash if the url is a domain name only.
1990 * @param string $url Possibly incomplete URL
1992 * @return string Fixed URL.
1994 function fix_url($url) {
1996 // support schema-less urls
1997 if (strpos($url, '//') === 0) {
1998 $url = 'https:' . $url;
2001 if (strpos($url, '://') === false) {
2002 $url = 'http://' . $url;
2003 } else if (substr($url, 0, 5) == 'feed:') {
2004 $url = 'http:' . substr($url, 5);
2007 //prepend slash if the URL has no slash in it
2008 // "http://www.example" -> "http://www.example/"
2009 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2013 //convert IDNA hostname to punycode if possible
2014 if (function_exists("idn_to_ascii")) {
2015 $parts = parse_url($url);
2016 if (mb_detect_encoding($parts['host']) != 'ASCII')
2018 $parts['host'] = idn_to_ascii($parts['host']);
2019 $url = build_url($parts);
2023 if ($url != "http:///")
2029 function validate_feed_url($url) {
2030 $parts = parse_url($url);
2032 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2036 /* function save_email_address($email) {
2037 // FIXME: implement persistent storage of emails
2039 if (!$_SESSION['stored_emails'])
2040 $_SESSION['stored_emails'] = array();
2042 if (!in_array($email, $_SESSION['stored_emails']))
2043 array_push($_SESSION['stored_emails'], $email);
2047 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2049 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2051 $sql_is_cat = bool_to_sql_bool($is_cat);
2053 $result = db_query("SELECT access_key FROM ttrss_access_keys
2054 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
2055 AND owner_uid = " . $owner_uid);
2057 if (db_num_rows($result) == 1) {
2058 return db_fetch_result($result, 0, "access_key");
2060 $key = db_escape_string(uniqid_short());
2062 $result = db_query("INSERT INTO ttrss_access_keys
2063 (access_key, feed_id, is_cat, owner_uid)
2064 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
2071 function get_feeds_from_html($url, $content)
2073 $url = fix_url($url);
2074 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2076 libxml_use_internal_errors(true);
2078 $doc = new DOMDocument();
2079 $doc->loadHTML($content);
2080 $xpath = new DOMXPath($doc);
2081 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2082 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2083 $feedUrls = array();
2084 foreach ($entries as $entry) {
2085 if ($entry->hasAttribute('href')) {
2086 $title = $entry->getAttribute('title');
2088 $title = $entry->getAttribute('type');
2090 $feedUrl = rewrite_relative_url(
2091 $baseUrl, $entry->getAttribute('href')
2093 $feedUrls[$feedUrl] = $title;
2099 function is_html($content) {
2100 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2103 function url_is_html($url, $login = false, $pass = false) {
2104 return is_html(fetch_file_contents($url, false, $login, $pass));
2107 function build_url($parts) {
2108 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2111 function cleanup_url_path($path) {
2112 $path = str_replace("/./", "/", $path);
2113 $path = str_replace("//", "/", $path);
2119 * Converts a (possibly) relative URL to a absolute one.
2121 * @param string $url Base URL (i.e. from where the document is)
2122 * @param string $rel_url Possibly relative URL in the document
2124 * @return string Absolute URL
2126 function rewrite_relative_url($url, $rel_url) {
2127 if (strpos($rel_url, "://") !== false) {
2129 } else if (strpos($rel_url, "//") === 0) {
2130 # protocol-relative URL (rare but they exist)
2132 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2133 # magnet:, feed:, etc
2135 } else if (strpos($rel_url, "/") === 0) {
2136 $parts = parse_url($url);
2137 $parts['path'] = $rel_url;
2138 $parts['path'] = cleanup_url_path($parts['path']);
2140 return build_url($parts);
2143 $parts = parse_url($url);
2144 if (!isset($parts['path'])) {
2145 $parts['path'] = '/';
2147 $dir = $parts['path'];
2148 if (substr($dir, -1) !== '/') {
2149 $dir = dirname($parts['path']);
2150 $dir !== '/' && $dir .= '/';
2152 $parts['path'] = $dir . $rel_url;
2153 $parts['path'] = cleanup_url_path($parts['path']);
2155 return build_url($parts);
2159 function cleanup_tags($days = 14, $limit = 1000) {
2161 if (DB_TYPE
== "pgsql") {
2162 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2163 } else if (DB_TYPE
== "mysql") {
2164 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2169 while ($limit > 0) {
2172 $query = "SELECT ttrss_tags.id AS id
2173 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2174 WHERE post_int_id = int_id AND $interval_query AND
2175 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2177 $result = db_query($query);
2181 while ($line = db_fetch_assoc($result)) {
2182 array_push($ids, $line['id']);
2185 if (count($ids) > 0) {
2186 $ids = join(",", $ids);
2188 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2189 $tags_deleted +
= db_affected_rows($tmp_result);
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) {
2214 if (DB_TYPE
== "pgsql")
2217 $reg_qpart = "REGEXP";
2219 foreach ($filter["rules"] AS $rule) {
2220 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2221 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2222 $rule['reg_exp']) !== FALSE;
2224 if ($regexp_valid) {
2226 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2228 switch ($rule["type"]) {
2230 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2231 $rule['reg_exp'] . "')";
2234 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2235 $rule['reg_exp'] . "')";
2238 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2239 $rule['reg_exp'] . "') OR LOWER(" .
2240 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2243 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2244 $rule['reg_exp'] . "')";
2247 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2248 $rule['reg_exp'] . "')";
2251 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2252 $rule['reg_exp'] . "')";
2256 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2258 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2259 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2262 if (isset($rule["cat_id"])) {
2264 if ($rule["cat_id"] > 0) {
2265 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2266 array_push($children, $rule["cat_id"]);
2268 $children = join(",", $children);
2270 $cat_qpart = "cat_id IN ($children)";
2272 $cat_qpart = "cat_id IS NULL";
2275 $qpart .= " AND $cat_qpart";
2278 $qpart .= " AND feed_id IS NOT NULL";
2280 array_push($query, "($qpart)");
2285 if (count($query) > 0) {
2286 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2288 $fullquery = "(false)";
2291 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2296 if (!function_exists('gzdecode')) {
2297 function gzdecode($string) { // no support for 2nd argument
2298 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2299 base64_encode($string));
2303 function get_random_bytes($length) {
2304 if (function_exists('openssl_random_pseudo_bytes')) {
2305 return openssl_random_pseudo_bytes($length);
2309 for ($i = 0; $i < $length; $i++
)
2310 $output .= chr(mt_rand(0, 255));
2316 function read_stdin() {
2317 $fp = fopen("php://stdin", "r");
2320 $line = trim(fgets($fp));
2328 function implements_interface($class, $interface) {
2329 return in_array($interface, class_implements($class));
2332 function get_minified_js($files) {
2333 require_once 'lib/jshrink/Minifier.php';
2337 foreach ($files as $js) {
2338 if (!isset($_GET['debug'])) {
2339 $cached_file = CACHE_DIR
. "/js/".basename($js).".js";
2341 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2343 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2345 if ($header && $contents) {
2346 list($htag, $hversion) = explode(":", $header);
2348 if ($htag == "tt-rss" && $hversion == VERSION
) {
2355 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js.js"));
2356 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2360 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2367 function calculate_dep_timestamp() {
2368 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2372 foreach ($files as $file) {
2373 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2379 function T_js_decl($s1, $s2) {
2381 $s1 = preg_replace("/\n/", "", $s1);
2382 $s2 = preg_replace("/\n/", "", $s2);
2384 $s1 = preg_replace("/\"/", "\\\"", $s1);
2385 $s2 = preg_replace("/\"/", "\\\"", $s2);
2387 return "T_messages[\"$s1\"] = \"$s2\";\n";
2391 function init_js_translations() {
2393 print 'var T_messages = new Object();
2396 if (T_messages[msg]) {
2397 return T_messages[msg];
2403 function ngettext(msg1, msg2, n) {
2404 return __((parseInt(n) > 1) ? msg2 : msg1);
2407 $l10n = _get_reader();
2409 for ($i = 0; $i < $l10n->total
; $i++
) {
2410 $orig = $l10n->get_original_string($i);
2411 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2412 $key = explode(chr(0), $orig);
2413 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2414 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2416 $translation = __($orig);
2417 print T_js_decl($orig, $translation);
2422 function get_theme_path($theme) {
2423 $check = "themes/$theme";
2424 if (file_exists($check)) return $check;
2426 $check = "themes.local/$theme";
2427 if (file_exists($check)) return $check;
2430 function theme_valid($theme) {
2431 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2433 if (in_array($theme, $bundled_themes)) return true;
2435 $file = "themes/" . basename($theme);
2437 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2439 if (file_exists($file) && is_readable($file)) {
2440 $fh = fopen($file, "r");
2443 $header = fgets($fh);
2446 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2454 * @SuppressWarnings(unused)
2456 function error_json($code) {
2457 require_once "errors.php";
2459 @$message = $ERRORS[$code];
2461 return json_encode(array("error" =>
2462 array("code" => $code, "message" => $message)));
2466 /*function abs_to_rel_path($dir) {
2467 $tmp = str_replace(dirname(__DIR__), "", $dir);
2469 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2474 function get_upload_error_message($code) {
2477 0 => __('There is no error, the file uploaded with success'),
2478 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2479 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2480 3 => __('The uploaded file was only partially uploaded'),
2481 4 => __('No file was uploaded'),
2482 6 => __('Missing a temporary folder'),
2483 7 => __('Failed to write file to disk.'),
2484 8 => __('A PHP extension stopped the file upload.'),
2487 return $errors[$code];
2490 function base64_img($filename) {
2491 if (file_exists($filename)) {
2492 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2494 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2500 /* this is essentially a wrapper for readfile() which allows plugins to hook
2501 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2503 hook function should return true if request was handled (or at least attempted to)
2505 note that this can be called without user context so the plugin to handle this
2506 should be loaded systemwide in config.php */
2507 function send_local_file($filename) {
2508 if (file_exists($filename)) {
2509 $tmppluginhost = new PluginHost();
2511 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2512 $tmppluginhost->load_data();
2514 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2515 if ($plugin->hook_send_local_file($filename)) return true;
2518 $mimetype = mime_content_type($filename);
2519 header("Content-type: $mimetype");
2521 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2522 header("Last-Modified: $stamp", true);
2524 return readfile($filename);
2530 function check_mysql_tables() {
2531 $schema = db_escape_string(DB_NAME
);
2533 $result = db_query("SELECT engine, table_name FROM information_schema.tables WHERE
2534 table_schema = '$schema' AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2538 while ($line = db_fetch_assoc($result)) {
2539 array_push($bad_tables, $line);