2 define('EXPECTED_CONFIG_VERSION', 26);
3 define('SCHEMA_VERSION', 134);
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_effective_url = false;
15 $fetch_curl_used = false;
16 $suppress_debugging = false;
18 libxml_disable_entity_loader(true);
20 // separate test because this is included before sanity checks
21 if (function_exists("mb_internal_encoding")) mb_internal_encoding("UTF-8");
23 date_default_timezone_set('UTC');
24 if (defined('E_DEPRECATED')) {
25 error_reporting(E_ALL
& ~E_NOTICE
& ~E_DEPRECATED
);
27 error_reporting(E_ALL
& ~E_NOTICE
);
30 require_once 'config.php';
33 * Define a constant if not already defined
35 function define_default($name, $value) {
36 defined($name) or define($name, $value);
39 /* Some tunables you can override in config.php using define(): */
41 define_default('FEED_FETCH_TIMEOUT', 45);
42 // How may seconds to wait for response when requesting feed from a site
43 define_default('FEED_FETCH_NO_CACHE_TIMEOUT', 15);
44 // How may seconds to wait for response when requesting feed from a
45 // site when that feed wasn't cached before
46 define_default('FILE_FETCH_TIMEOUT', 45);
47 // Default timeout when fetching files from remote sites
48 define_default('FILE_FETCH_CONNECT_TIMEOUT', 15);
49 // How many seconds to wait for initial response from website when
50 // fetching files from remote sites
51 define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
52 // stop updating feeds if users haven't logged in for X days
53 define_default('DAEMON_FEED_LIMIT', 500);
54 // feed limit for one update batch
55 define_default('DAEMON_SLEEP_INTERVAL', 120);
56 // default sleep interval between feed updates (sec)
57 define_default('MIN_CACHE_FILE_SIZE', 1024);
58 // do not cache files smaller than that (bytes)
59 define_default('MAX_CACHE_FILE_SIZE', 64*1024*1024);
60 // do not cache files larger than that (bytes)
61 define_default('MAX_DOWNLOAD_FILE_SIZE', 16*1024*1024);
62 // do not download general files larger than that (bytes)
63 define_default('CACHE_MAX_DAYS', 7);
64 // max age in days for various automatically cached (temporary) files
65 define_default('MAX_CONDITIONAL_INTERVAL', 3600*12);
66 // max interval between forced unconditional updates for servers
67 // not complying with http if-modified-since (seconds)
69 /* tunables end here */
71 if (DB_TYPE
== "pgsql") {
72 define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
74 define('SUBSTRING_FOR_DATE', 'SUBSTRING');
78 * Return available translations names.
81 * @return array A array of available translations.
83 function get_translations() {
85 "auto" => "Detect automatically",
86 "ar_SA" => "العربيّة (Arabic)",
87 "bg_BG" => "Bulgarian",
92 "el_GR" => "Ελληνικά",
93 "es_ES" => "Español (España)",
96 "fr_FR" => "Français",
97 "hu_HU" => "Magyar (Hungarian)",
98 "it_IT" => "Italiano",
99 "ja_JP" => "日本語 (Japanese)",
100 "lv_LV" => "Latviešu",
101 "nb_NO" => "Norwegian bokmål",
104 "ru_RU" => "Русский",
105 "pt_BR" => "Portuguese/Brazil",
106 "pt_PT" => "Portuguese/Portugal",
107 "zh_CN" => "Simplified Chinese",
108 "zh_TW" => "Traditional Chinese",
109 "sv_SE" => "Svenska",
111 "tr_TR" => "Türkçe");
116 require_once "lib/accept-to-gettext.php";
117 require_once "lib/gettext/gettext.inc";
119 function startup_gettext() {
121 # Get locale from Accept-Language header
122 $lang = al2gt(array_keys(get_translations()), "text/html");
124 if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
125 $lang = _TRANSLATION_OVERRIDE_DEFAULT
;
128 if ($_SESSION["uid"] && get_schema_version() >= 120) {
129 $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
131 if ($pref_lang && $pref_lang != 'auto') {
137 if (defined('LC_MESSAGES')) {
138 _setlocale(LC_MESSAGES
, $lang);
139 } else if (defined('LC_ALL')) {
140 _setlocale(LC_ALL
, $lang);
143 _bindtextdomain("messages", "locale");
145 _textdomain("messages");
146 _bind_textdomain_codeset("messages", "UTF-8");
150 require_once 'db-prefs.php';
151 require_once 'version.php';
152 require_once 'controls.php';
154 define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION
. ' (http://tt-rss.org/)');
155 ini_set('user_agent', SELF_USER_AGENT
);
157 $schema_version = false;
159 function _debug_suppress($suppress) {
160 global $suppress_debugging;
162 $suppress_debugging = $suppress;
166 * Print a timestamped debug message.
168 * @param string $msg The debug message.
171 function _debug($msg, $show = true) {
172 global $suppress_debugging;
174 //echo "[$suppress_debugging] $msg $show\n";
176 if ($suppress_debugging) return false;
178 $ts = strftime("%H:%M:%S", time());
179 if (function_exists('posix_getpid')) {
180 $ts = "$ts/" . posix_getpid();
183 if ($show && !(defined('QUIET') && QUIET
)) {
184 print "[$ts] $msg\n";
187 if (defined('LOGFILE')) {
188 $fp = fopen(LOGFILE
, 'a+');
193 if (function_exists("flock")) {
196 // try to lock logfile for writing
197 while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB
)) {
208 fputs($fp, "[$ts] $msg\n");
210 if (function_exists("flock")) {
221 * Purge a feed old posts.
223 * @param mixed $link A database connection.
224 * @param mixed $feed_id The id of the purged feed.
225 * @param mixed $purge_interval Olderness of purged posts.
226 * @param boolean $debug Set to True to enable the debug. False by default.
230 function purge_feed($feed_id, $purge_interval, $debug = false) {
232 if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
236 $sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
237 $sth->execute([$feed_id]);
241 if ($row = $sth->fetch()) {
242 $owner_uid = $row["owner_uid"];
245 if ($purge_interval == -1 ||
!$purge_interval) {
247 CCache
::update($feed_id, $owner_uid);
252 if (!$owner_uid) return;
254 if (FORCE_ARTICLE_PURGE
== 0) {
255 $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
258 $purge_unread = true;
259 $purge_interval = FORCE_ARTICLE_PURGE
;
263 $query_limit = " unread = false AND ";
267 $purge_interval = (int) $purge_interval;
269 if (DB_TYPE
== "pgsql") {
270 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
272 WHERE ttrss_entries.id = ref_id AND
276 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
277 $sth->execute([$feed_id]);
280 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
281 USING ttrss_user_entries, ttrss_entries
282 WHERE ttrss_entries.id = ref_id AND
286 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
287 $sth->execute([$feed_id]);
291 $rows = $sth->rowCount();
293 CCache
::update($feed_id, $owner_uid);
296 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
300 } // function purge_feed
302 function feed_purge_interval($feed_id) {
306 $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
308 $sth->execute([$feed_id]);
310 if ($row = $sth->fetch()) {
311 $purge_interval = $row["purge_interval"];
312 $owner_uid = $row["owner_uid"];
314 if ($purge_interval == 0) $purge_interval = get_pref(
315 'PURGE_OLD_DAYS', $owner_uid);
317 return $purge_interval;
324 // TODO: max_size currently only works for CURL transfers
325 // TODO: multiple-argument way is deprecated, first parameter is a hash now
326 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
327 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
329 global $fetch_last_error;
330 global $fetch_last_error_code;
331 global $fetch_last_error_content;
332 global $fetch_last_content_type;
333 global $fetch_last_modified;
334 global $fetch_effective_url;
335 global $fetch_curl_used;
337 $fetch_last_error = false;
338 $fetch_last_error_code = -1;
339 $fetch_last_error_content = "";
340 $fetch_last_content_type = "";
341 $fetch_curl_used = false;
342 $fetch_last_modified = "";
343 $fetch_effective_url = "";
345 if (!is_array($options)) {
347 // falling back on compatibility shim
348 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
351 for ($i = 0; $i < func_num_args(); $i++
) {
352 $tmp[$option_names[$i]] = func_get_arg($i);
358 "url" => func_get_arg(0),
359 "type" => @func_get_arg(1),
360 "login" => @func_get_arg(2),
361 "pass" => @func_get_arg(3),
362 "post_query" => @func_get_arg(4),
363 "timeout" => @func_get_arg(5),
364 "timestamp" => @func_get_arg(6),
365 "useragent" => @func_get_arg(7)
369 $url = $options["url"];
370 $type = isset($options["type"]) ?
$options["type"] : false;
371 $login = isset($options["login"]) ?
$options["login"] : false;
372 $pass = isset($options["pass"]) ?
$options["pass"] : false;
373 $post_query = isset($options["post_query"]) ?
$options["post_query"] : false;
374 $timeout = isset($options["timeout"]) ?
$options["timeout"] : false;
375 $last_modified = isset($options["last_modified"]) ?
$options["last_modified"] : "";
376 $useragent = isset($options["useragent"]) ?
$options["useragent"] : false;
377 $followlocation = isset($options["followlocation"]) ?
$options["followlocation"] : true;
378 $max_size = isset($options["max_size"]) ?
$options["max_size"] : MAX_DOWNLOAD_FILE_SIZE
; // in bytes
379 $http_accept = isset($options["http_accept"]) ?
$options["http_accept"] : false;
381 $url = ltrim($url, ' ');
382 $url = str_replace(' ', '%20', $url);
384 if (strpos($url, "//") === 0)
385 $url = 'http:' . $url;
387 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
389 $fetch_curl_used = true;
391 $ch = curl_init($url);
393 $curl_http_headers = [];
395 if ($last_modified && !$post_query)
396 array_push($curl_http_headers, "If-Modified-Since: $last_modified");
399 array_push($curl_http_headers, "Accept: " . $http_accept);
401 if (count($curl_http_headers) > 0)
402 curl_setopt($ch, CURLOPT_HTTPHEADER
, $curl_http_headers);
404 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
405 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
406 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
407 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
408 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
409 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
410 curl_setopt($ch, CURLOPT_HEADER
, true);
411 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
412 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
414 curl_setopt($ch, CURLOPT_ENCODING
, "");
415 //curl_setopt($ch, CURLOPT_REFERER, $url);
418 curl_setopt($ch, CURLOPT_NOPROGRESS
, false);
419 curl_setopt($ch, CURLOPT_BUFFERSIZE
, 16384); // needed to get 5 arguments in progress function?
421 // holy shit closures in php
422 // download & upload are *expected* sizes respectively, could be zero
423 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION
, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use( &$max_size) {
424 //_debug("[curl progressfunction] $downloaded $max_size");
426 return ($downloaded > $max_size) ?
1 : 0; // if max size is set, abort when exceeding it
431 if (!ini_get("open_basedir")) {
432 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
435 if (defined('_HTTP_PROXY')) {
436 curl_setopt($ch, CURLOPT_PROXY
, _HTTP_PROXY
);
440 curl_setopt($ch, CURLOPT_POST
, true);
441 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
445 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
447 $ret = @curl_exec
($ch);
449 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE
);
450 $headers = explode("\r\n", substr($ret, 0, $headers_length));
451 $contents = substr($ret, $headers_length);
453 foreach ($headers as $header) {
454 if (strstr($header, ": ") !== FALSE) {
455 list ($key, $value) = explode(": ", $header);
457 if (strtolower($key) == "last-modified") {
458 $fetch_last_modified = $value;
462 if (substr(strtolower($header), 0, 7) == 'http/1.') {
463 $fetch_last_error_code = (int) substr($header, 9, 3);
464 $fetch_last_error = $header;
468 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
469 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
470 $contents = @curl_exec
($ch);
473 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
474 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
476 $fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL
);
478 $fetch_last_error_code = $http_code;
480 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
482 if (curl_errno($ch) != 0) {
483 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
486 $fetch_last_error_content = $contents;
492 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
502 $fetch_curl_used = false;
504 if ($login && $pass){
505 $url_parts = array();
507 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
509 $pass = urlencode($pass);
511 if ($url_parts[1] && $url_parts[2]) {
512 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
516 // TODO: should this support POST requests or not? idk
518 $context_options = array(
524 'ignore_errors' => true,
525 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
526 'protocol_version'=> 1.1)
529 if (!$post_query && $last_modified)
530 array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
533 array_push($context_options['http']['header'], "Accept: $http_accept");
535 if (defined('_HTTP_PROXY')) {
536 $context_options['http']['request_fulluri'] = true;
537 $context_options['http']['proxy'] = _HTTP_PROXY
;
540 $context = stream_context_create($context_options);
542 $old_error = error_get_last();
544 $fetch_effective_url = $url;
546 $data = @file_get_contents
($url, false, $context);
548 if (isset($http_response_header) && is_array($http_response_header)) {
549 foreach ($http_response_header as $header) {
550 if (strstr($header, ": ") !== FALSE) {
551 list ($key, $value) = explode(": ", $header);
553 $key = strtolower($key);
555 if ($key == 'content-type') {
556 $fetch_last_content_type = $value;
557 // don't abort here b/c there might be more than one
558 // e.g. if we were being redirected -- last one is the right one
559 } else if ($key == 'last-modified') {
560 $fetch_last_modified = $value;
561 } else if ($key == 'location') {
562 $fetch_effective_url = $value;
566 if (substr(strtolower($header), 0, 7) == 'http/1.') {
567 $fetch_last_error_code = (int) substr($header, 9, 3);
568 $fetch_last_error = $header;
573 if ($fetch_last_error_code != 200) {
574 $error = error_get_last();
576 if ($error['message'] != $old_error['message']) {
577 $fetch_last_error .= "; " . $error["message"];
580 $fetch_last_error_content = $data;
590 * Try to determine the favicon URL for a feed.
591 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
592 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
594 * @param string $url A feed or page URL
596 * @return mixed The favicon URL, or false if none was found.
598 function get_favicon_url($url) {
600 $favicon_url = false;
602 if ($html = @fetch_file_contents
($url)) {
604 libxml_use_internal_errors(true);
606 $doc = new DOMDocument();
607 $doc->loadHTML($html);
608 $xpath = new DOMXPath($doc);
610 $base = $xpath->query('/html/head/base[@href]');
611 foreach ($base as $b) {
612 $url = rewrite_relative_url($url, $b->getAttribute("href"));
616 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
617 if (count($entries) > 0) {
618 foreach ($entries as $entry) {
619 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
626 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
629 } // function get_favicon_url
631 function initialize_user_prefs($uid, $profile = false) {
633 if (get_schema_version() < 63) $profile_qpart = "";
636 $in_nested_tr = false;
639 $pdo->beginTransaction();
640 } catch (Exception
$e) {
641 $in_nested_tr = true;
644 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
646 $profile = $profile ?
$profile : null;
648 $u_sth = $pdo->prepare("SELECT pref_name
649 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
650 (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
651 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
653 $active_prefs = array();
655 while ($line = $u_sth->fetch()) {
656 array_push($active_prefs, $line["pref_name"]);
659 while ($line = $sth->fetch()) {
660 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
661 // print "adding " . $line["pref_name"] . "<br>";
663 if (get_schema_version() < 63) {
664 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
665 (owner_uid,pref_name,value) VALUES
667 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
670 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
671 (owner_uid,pref_name,value, profile) VALUES
673 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
679 if (!$in_nested_tr) $pdo->commit();
683 function get_ssl_certificate_id() {
684 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
685 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
686 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
687 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
688 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
690 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
691 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
692 $_SERVER["SSL_CLIENT_V_START"] .
693 $_SERVER["SSL_CLIENT_V_END"] .
694 $_SERVER["SSL_CLIENT_S_DN"]);
699 function authenticate_user($login, $password, $check_only = false) {
701 if (!SINGLE_USER_MODE
) {
704 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
706 $user_id = (int) $plugin->authenticate($login, $password);
709 $_SESSION["auth_module"] = strtolower(get_class($plugin));
714 if ($user_id && !$check_only) {
716 if (session_status() != PHP_SESSION_NONE
) {
722 session_regenerate_id(true);
724 $_SESSION["uid"] = $user_id;
725 $_SESSION["version"] = VERSION_STATIC
;
728 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
730 $sth->execute([$user_id]);
731 $row = $sth->fetch();
733 $_SESSION["name"] = $row["login"];
734 $_SESSION["access_level"] = $row["access_level"];
735 $_SESSION["csrf_token"] = uniqid_short();
737 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
738 $usth->execute([$user_id]);
740 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
741 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
742 $_SESSION["pwd_hash"] = $row["pwd_hash"];
744 $_SESSION["last_version_check"] = time();
746 initialize_user_prefs($_SESSION["uid"]);
755 $_SESSION["uid"] = 1;
756 $_SESSION["name"] = "admin";
757 $_SESSION["access_level"] = 10;
759 $_SESSION["hide_hello"] = true;
760 $_SESSION["hide_logout"] = true;
762 $_SESSION["auth_module"] = false;
764 if (!$_SESSION["csrf_token"]) {
765 $_SESSION["csrf_token"] = uniqid_short();
768 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
770 initialize_user_prefs($_SESSION["uid"]);
776 // this is used for user http parameters unless HTML code is actually needed
777 function clean($param) {
778 if (is_array($param)) {
779 return array_map("strip_tags", $param);
780 } else if (is_string($param)) {
781 return strip_tags($param);
787 function make_password($length = 8) {
790 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
794 while ($i < $length) {
795 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
797 if (!strstr($password, $char)) {
805 // this is called after user is created to initialize default feeds, labels
808 // user preferences are checked on every login, not here
810 function initialize_user($uid) {
814 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
815 values (?, 'Tiny Tiny RSS: Forum',
816 'http://tt-rss.org/forum/rss.php')");
817 $sth->execute([$uid]);
820 function logout_user() {
822 if (isset($_COOKIE[session_name()])) {
823 setcookie(session_name(), '', time()-42000, '/');
828 function validate_csrf($csrf_token) {
829 return $csrf_token == $_SESSION['csrf_token'];
832 function load_user_plugins($owner_uid, $pluginhost = false) {
834 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
836 if ($owner_uid && SCHEMA_VERSION
>= 100) {
837 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
839 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
841 if (get_schema_version() > 100) {
842 $pluginhost->load_data();
847 function login_sequence() {
850 if (SINGLE_USER_MODE
) {
852 authenticate_user("admin", null);
854 load_user_plugins($_SESSION["uid"]);
856 if (!validate_session()) $_SESSION["uid"] = false;
858 if (!$_SESSION["uid"]) {
860 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
861 $_SESSION["ref_schema_version"] = get_schema_version(true);
863 authenticate_user(null, null, true);
866 if (!$_SESSION["uid"]) {
874 /* bump login timestamp */
875 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
876 $sth->execute([$_SESSION['uid']]);
878 $_SESSION["last_login_update"] = time();
881 if ($_SESSION["uid"]) {
883 load_user_plugins($_SESSION["uid"]);
887 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
889 (SELECT COUNT(id) FROM ttrss_feeds WHERE
890 ttrss_feeds.id = feed_id) = 0");
892 $sth->execute([$_SESSION['uid']]);
894 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
896 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
897 ttrss_feed_categories.id = feed_id) = 0");
899 $sth->execute([$_SESSION['uid']]);
905 function truncate_string($str, $max_len, $suffix = '…') {
906 if (mb_strlen($str, "utf-8") > $max_len) {
907 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
914 function truncate_middle($str, $max_len, $suffix = '…') {
915 if (strlen($str) > $max_len) {
916 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
922 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
925 $source_tz = new DateTimeZone($source_tz);
926 } catch (Exception
$e) {
927 $source_tz = new DateTimeZone('UTC');
931 $dest_tz = new DateTimeZone($dest_tz);
932 } catch (Exception
$e) {
933 $dest_tz = new DateTimeZone('UTC');
936 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
937 return $dt->format('U') +
$dest_tz->getOffset($dt);
940 function make_local_datetime($timestamp, $long, $owner_uid = false,
941 $no_smart_dt = false, $eta_min = false) {
943 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
944 if (!$timestamp) $timestamp = '1970-01-01 0:00';
949 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
951 $timestamp = substr($timestamp, 0, 19);
953 # We store date in UTC internally
954 $dt = new DateTime($timestamp, $utc_tz);
956 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
958 if ($user_tz_string != 'Automatic') {
961 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
962 } catch (Exception
$e) {
966 $tz_offset = $user_tz->getOffset($dt);
968 $tz_offset = (int) -$_SESSION["clientTzOffset"];
971 $user_timestamp = $dt->format('U') +
$tz_offset;
974 return smart_date_time($user_timestamp,
975 $tz_offset, $owner_uid, $eta_min);
978 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
980 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
982 return date($format, $user_timestamp);
986 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
987 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
989 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
990 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
991 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
992 return date("G:i", $timestamp);
993 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
994 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
995 return date($format, $timestamp);
997 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
998 return date($format, $timestamp);
1002 function sql_bool_to_bool($s) {
1003 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
1006 function bool_to_sql_bool($s) {
1010 // Session caching removed due to causing wrong redirects to upgrade
1011 // script when get_schema_version() is called on an obsolete session
1012 // created on a previous schema version.
1013 function get_schema_version($nocache = false) {
1014 global $schema_version;
1018 if (!$schema_version && !$nocache) {
1019 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
1020 $version = $row["schema_version"];
1021 $schema_version = $version;
1024 return $schema_version;
1028 function sanity_check() {
1029 require_once 'errors.php';
1033 $schema_version = get_schema_version(true);
1035 if ($schema_version != SCHEMA_VERSION
) {
1039 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1042 function file_is_locked($filename) {
1043 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
1044 if (function_exists('flock')) {
1045 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1047 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1048 flock($fp, LOCK_UN
);
1058 return true; // consider the file always locked and skip the test
1065 function make_lockfile($filename) {
1066 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1068 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1069 $stat_h = fstat($fp);
1070 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1072 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1073 if ($stat_h["ino"] != $stat_f["ino"] ||
1074 $stat_h["dev"] != $stat_f["dev"]) {
1080 if (function_exists('posix_getpid')) {
1081 fwrite($fp, posix_getpid() . "\n");
1089 function make_stampfile($filename) {
1090 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1092 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1093 fwrite($fp, time() . "\n");
1094 flock($fp, LOCK_UN
);
1102 function sql_random_function() {
1103 if (DB_TYPE
== "mysql") {
1110 function getFeedUnread($feed, $is_cat = false) {
1111 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1114 function checkbox_to_sql_bool($val) {
1115 return ($val == "on") ?
1 : 0;
1118 function uniqid_short() {
1119 return uniqid(base_convert(rand(), 10, 36));
1122 function make_init_params() {
1125 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1126 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1127 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1128 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1130 $params[strtolower($param)] = (int) get_pref($param);
1133 $params["icons_url"] = ICONS_URL
;
1134 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1135 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1136 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1137 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1138 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1139 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1140 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1142 $theme = get_pref( "USER_CSS_THEME", false, false);
1143 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1145 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1147 $params["php_platform"] = PHP_OS
;
1148 $params["php_version"] = PHP_VERSION
;
1150 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1154 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1155 ttrss_feeds WHERE owner_uid = ?");
1156 $sth->execute([$_SESSION['uid']]);
1157 $row = $sth->fetch();
1159 $max_feed_id = $row["mid"];
1160 $num_feeds = $row["nf"];
1162 $params["max_feed_id"] = (int) $max_feed_id;
1163 $params["num_feeds"] = (int) $num_feeds;
1165 $params["hotkeys"] = get_hotkeys_map();
1167 $params["csrf_token"] = $_SESSION["csrf_token"];
1168 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1170 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1172 $params["icon_alert"] = base64_img("images/alert.png");
1173 $params["icon_information"] = base64_img("images/information.png");
1174 $params["icon_cross"] = base64_img("images/cross.png");
1175 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1177 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1182 function get_hotkeys_info() {
1184 __("Navigation") => array(
1185 "next_feed" => __("Open next feed"),
1186 "prev_feed" => __("Open previous feed"),
1187 "next_article" => __("Open next article"),
1188 "prev_article" => __("Open previous article"),
1189 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1190 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1191 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1192 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1193 "search_dialog" => __("Show search dialog")),
1194 __("Article") => array(
1195 "toggle_mark" => __("Toggle starred"),
1196 "toggle_publ" => __("Toggle published"),
1197 "toggle_unread" => __("Toggle unread"),
1198 "edit_tags" => __("Edit tags"),
1199 "open_in_new_window" => __("Open in new window"),
1200 "catchup_below" => __("Mark below as read"),
1201 "catchup_above" => __("Mark above as read"),
1202 "article_scroll_down" => __("Scroll down"),
1203 "article_scroll_up" => __("Scroll up"),
1204 "select_article_cursor" => __("Select article under cursor"),
1205 "email_article" => __("Email article"),
1206 "close_article" => __("Close/collapse article"),
1207 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1208 "toggle_widescreen" => __("Toggle widescreen mode"),
1209 "toggle_embed_original" => __("Toggle embed original")),
1210 __("Article selection") => array(
1211 "select_all" => __("Select all articles"),
1212 "select_unread" => __("Select unread"),
1213 "select_marked" => __("Select starred"),
1214 "select_published" => __("Select published"),
1215 "select_invert" => __("Invert selection"),
1216 "select_none" => __("Deselect everything")),
1217 __("Feed") => array(
1218 "feed_refresh" => __("Refresh current feed"),
1219 "feed_unhide_read" => __("Un/hide read feeds"),
1220 "feed_subscribe" => __("Subscribe to feed"),
1221 "feed_edit" => __("Edit feed"),
1222 "feed_catchup" => __("Mark as read"),
1223 "feed_reverse" => __("Reverse headlines"),
1224 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1225 "feed_debug_update" => __("Debug feed update"),
1226 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1227 "catchup_all" => __("Mark all feeds as read"),
1228 "cat_toggle_collapse" => __("Un/collapse current category"),
1229 "toggle_combined_mode" => __("Toggle combined mode"),
1230 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1231 __("Go to") => array(
1232 "goto_all" => __("All articles"),
1233 "goto_fresh" => __("Fresh"),
1234 "goto_marked" => __("Starred"),
1235 "goto_published" => __("Published"),
1236 "goto_tagcloud" => __("Tag cloud"),
1237 "goto_prefs" => __("Preferences")),
1238 __("Other") => array(
1239 "create_label" => __("Create label"),
1240 "create_filter" => __("Create filter"),
1241 "collapse_sidebar" => __("Un/collapse sidebar"),
1242 "help_dialog" => __("Show help dialog"))
1245 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1246 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1252 function get_hotkeys_map() {
1254 // "navigation" => array(
1257 "n" => "next_article",
1258 "p" => "prev_article",
1259 "(38)|up" => "prev_article",
1260 "(40)|down" => "next_article",
1261 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1262 // "^(40)|Ctrl-down" => "next_article_noscroll",
1263 "(191)|/" => "search_dialog",
1264 // "article" => array(
1265 "s" => "toggle_mark",
1266 "*s" => "toggle_publ",
1267 "u" => "toggle_unread",
1268 "*t" => "edit_tags",
1269 "o" => "open_in_new_window",
1270 "c p" => "catchup_below",
1271 "c n" => "catchup_above",
1272 "*n" => "article_scroll_down",
1273 "*p" => "article_scroll_up",
1274 "*(38)|Shift+up" => "article_scroll_up",
1275 "*(40)|Shift+down" => "article_scroll_down",
1276 "a *w" => "toggle_widescreen",
1277 "a e" => "toggle_embed_original",
1278 "e" => "email_article",
1279 "a q" => "close_article",
1280 // "article_selection" => array(
1281 "a a" => "select_all",
1282 "a u" => "select_unread",
1283 "a *u" => "select_marked",
1284 "a p" => "select_published",
1285 "a i" => "select_invert",
1286 "a n" => "select_none",
1288 "f r" => "feed_refresh",
1289 "f a" => "feed_unhide_read",
1290 "f s" => "feed_subscribe",
1291 "f e" => "feed_edit",
1292 "f q" => "feed_catchup",
1293 "f x" => "feed_reverse",
1294 "f g" => "feed_toggle_vgroup",
1295 "f *d" => "feed_debug_update",
1296 "f *g" => "feed_debug_viewfeed",
1297 "f *c" => "toggle_combined_mode",
1298 "f c" => "toggle_cdm_expanded",
1299 "*q" => "catchup_all",
1300 "x" => "cat_toggle_collapse",
1302 "g a" => "goto_all",
1303 "g f" => "goto_fresh",
1304 "g s" => "goto_marked",
1305 "g p" => "goto_published",
1306 "g t" => "goto_tagcloud",
1307 "g *p" => "goto_prefs",
1308 // "other" => array(
1309 "(9)|Tab" => "select_article_cursor", // tab
1310 "c l" => "create_label",
1311 "c f" => "create_filter",
1312 "c s" => "collapse_sidebar",
1313 "^(191)|Ctrl+/" => "help_dialog",
1316 if (get_pref('COMBINED_DISPLAY_MODE')) {
1317 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1318 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1321 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1322 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1325 $prefixes = array();
1327 foreach (array_keys($hotkeys) as $hotkey) {
1328 $pair = explode(" ", $hotkey, 2);
1330 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1331 array_push($prefixes, $pair[0]);
1335 return array($prefixes, $hotkeys);
1338 function check_for_update() {
1339 if (defined("GIT_VERSION_TIMESTAMP")) {
1340 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1343 $content = json_decode($content, true);
1345 if ($content && isset($content["changeset"])) {
1346 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1347 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1349 return $content["changeset"]["id"];
1358 function make_runtime_info($disable_update_check = false) {
1363 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1364 ttrss_feeds WHERE owner_uid = ?");
1365 $sth->execute([$_SESSION['uid']]);
1366 $row = $sth->fetch();
1368 $max_feed_id = $row['mid'];
1369 $num_feeds = $row['nf'];
1371 $data["max_feed_id"] = (int) $max_feed_id;
1372 $data["num_feeds"] = (int) $num_feeds;
1374 $data['last_article_id'] = Article
::getLastArticleId();
1375 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1377 $data['dep_ts'] = calculate_dep_timestamp();
1378 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1380 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1382 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1383 $update_result = @check_for_update
();
1385 $data["update_result"] = $update_result;
1387 $_SESSION["last_version_check"] = time();
1390 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1392 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1394 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1396 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1399 $stamp_delta = time() - $stamp;
1401 if ($stamp_delta > 1800) {
1405 $_SESSION["daemon_stamp_check"] = time();
1408 $data['daemon_stamp_ok'] = $stamp_check;
1410 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1412 $data['daemon_stamp'] = $stamp_fmt;
1420 function search_to_sql($search, $search_language) {
1422 $keywords = str_getcsv(trim($search), " ");
1423 $query_keywords = array();
1424 $search_words = array();
1425 $search_query_leftover = array();
1429 if ($search_language)
1430 $search_language = $pdo->quote(mb_strtolower($search_language));
1432 $search_language = $pdo->quote("english");
1434 foreach ($keywords as $k) {
1435 if (strpos($k, "-") === 0) {
1442 $commandpair = explode(":", mb_strtolower($k), 2);
1444 switch ($commandpair[0]) {
1446 if ($commandpair[1]) {
1447 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1448 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1450 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1451 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1452 array_push($search_words, $k);
1456 if ($commandpair[1]) {
1457 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1458 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1460 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1461 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1462 array_push($search_words, $k);
1466 if ($commandpair[1]) {
1467 if ($commandpair[1] == "true")
1468 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1469 else if ($commandpair[1] == "false")
1470 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1472 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1473 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1475 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1476 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1477 if (!$not) array_push($search_words, $k);
1482 if ($commandpair[1]) {
1483 if ($commandpair[1] == "true")
1484 array_push($query_keywords, "($not (marked = true))");
1486 array_push($query_keywords, "($not (marked = false))");
1488 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1489 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1490 if (!$not) array_push($search_words, $k);
1494 if ($commandpair[1]) {
1495 if ($commandpair[1] == "true")
1496 array_push($query_keywords, "($not (published = true))");
1498 array_push($query_keywords, "($not (published = false))");
1501 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1502 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1503 if (!$not) array_push($search_words, $k);
1507 if ($commandpair[1]) {
1508 if ($commandpair[1] == "true")
1509 array_push($query_keywords, "($not (unread = true))");
1511 array_push($query_keywords, "($not (unread = false))");
1514 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1515 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1516 if (!$not) array_push($search_words, $k);
1520 if (strpos($k, "@") === 0) {
1522 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1523 $orig_ts = strtotime(substr($k, 1));
1524 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1526 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1528 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1531 if (DB_TYPE
== "pgsql") {
1532 $k = mb_strtolower($k);
1533 array_push($search_query_leftover, $not ?
"!$k" : $k);
1535 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1536 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1539 if (!$not) array_push($search_words, $k);
1544 if (count($search_query_leftover) > 0) {
1545 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1547 if (DB_TYPE
== "pgsql") {
1548 array_push($query_keywords,
1549 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1554 $search_query_part = implode("AND", $query_keywords);
1556 return array($search_query_part, $search_words);
1559 function iframe_whitelisted($entry) {
1560 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1562 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1565 foreach ($whitelist as $w) {
1566 if ($src == $w ||
$src == "www.$w")
1574 // check for locally cached (media) URLs and rewrite to local versions
1575 // this is called separately after sanitize() and plugin render article hooks to allow
1576 // plugins work on original source URLs used before caching
1578 function rewrite_cached_urls($str) {
1579 $charset_hack = '<head>
1580 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1583 $res = trim($str); if (!$res) return '';
1585 $doc = new DOMDocument();
1586 $doc->loadHTML($charset_hack . $res);
1587 $xpath = new DOMXPath($doc);
1589 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
1591 $need_saving = false;
1593 foreach ($entries as $entry) {
1595 if ($entry->hasAttribute('src') ||
$entry->hasAttribute('poster')) {
1597 // should be already absolutized because this is called after sanitize()
1598 $src = $entry->hasAttribute('poster') ?
$entry->getAttribute('poster') : $entry->getAttribute('src');
1599 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1601 if (file_exists($cached_filename)) {
1603 // this is strictly cosmetic
1604 if ($entry->tagName
== 'img') {
1606 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1608 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1614 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1616 if ($entry->hasAttribute('poster'))
1617 $entry->setAttribute('poster', $src);
1619 $entry->setAttribute('src', $src);
1621 $need_saving = true;
1627 $doc->removeChild($doc->firstChild
); //remove doctype
1628 $res = $doc->saveHTML();
1634 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1635 if (!$owner) $owner = $_SESSION["uid"];
1637 $res = trim($str); if (!$res) return '';
1639 $charset_hack = '<head>
1640 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1643 $res = trim($res); if (!$res) return '';
1645 libxml_use_internal_errors(true);
1647 $doc = new DOMDocument();
1648 $doc->loadHTML($charset_hack . $res);
1649 $xpath = new DOMXPath($doc);
1651 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1653 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1655 foreach ($entries as $entry) {
1657 if ($entry->hasAttribute('href')) {
1658 $entry->setAttribute('href',
1659 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1661 $entry->setAttribute('rel', 'noopener noreferrer');
1664 if ($entry->hasAttribute('src')) {
1665 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1667 // cache stuff has gone to rewrite_cached_urls()
1669 $entry->setAttribute('src', $src);
1672 if ($entry->nodeName
== 'img') {
1673 $entry->setAttribute('referrerpolicy', 'no-referrer');
1675 $entry->removeAttribute('width');
1676 $entry->removeAttribute('height');
1678 if ($entry->hasAttribute('src')) {
1679 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1681 if (is_prefix_https() && !$is_https_url) {
1683 if ($entry->hasAttribute('srcset')) {
1684 $entry->removeAttribute('srcset');
1687 if ($entry->hasAttribute('sizes')) {
1688 $entry->removeAttribute('sizes');
1694 if ($entry->hasAttribute('src') &&
1695 ($owner && get_pref("STRIP_IMAGES", $owner)) ||
$force_remove_images ||
$_SESSION["bw_limit"]) {
1697 $p = $doc->createElement('p');
1699 $a = $doc->createElement('a');
1700 $a->setAttribute('href', $entry->getAttribute('src'));
1702 $a->appendChild(new DOMText($entry->getAttribute('src')));
1703 $a->setAttribute('target', '_blank');
1704 $a->setAttribute('rel', 'noopener noreferrer');
1706 $p->appendChild($a);
1708 if ($entry->nodeName
== 'source') {
1710 if ($entry->parentNode
&& $entry->parentNode
->parentNode
)
1711 $entry->parentNode
->parentNode
->replaceChild($p, $entry->parentNode
);
1713 } else if ($entry->nodeName
== 'img') {
1715 if ($entry->parentNode
)
1716 $entry->parentNode
->replaceChild($p, $entry);
1721 if (strtolower($entry->nodeName
) == "a") {
1722 $entry->setAttribute("target", "_blank");
1723 $entry->setAttribute("rel", "noopener noreferrer");
1727 $entries = $xpath->query('//iframe');
1728 foreach ($entries as $entry) {
1729 if (!iframe_whitelisted($entry)) {
1730 $entry->setAttribute('sandbox', 'allow-scripts');
1732 if (is_prefix_https()) {
1733 $entry->setAttribute("src",
1734 str_replace("http://", "https://",
1735 $entry->getAttribute("src")));
1740 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1741 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1742 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1743 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1744 'dt', 'em', 'footer', 'figure', 'figcaption',
1745 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1746 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1747 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1748 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1749 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1750 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1752 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1754 $disallowed_attributes = array('id', 'style', 'class');
1756 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1757 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1758 if (is_array($retval)) {
1760 $allowed_elements = $retval[1];
1761 $disallowed_attributes = $retval[2];
1767 $doc->removeChild($doc->firstChild
); //remove doctype
1768 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1770 if ($highlight_words) {
1771 foreach ($highlight_words as $word) {
1773 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1775 $elements = $xpath->query("//*/text()");
1777 foreach ($elements as $child) {
1779 $fragment = $doc->createDocumentFragment();
1780 $text = $child->textContent
;
1782 while (($pos = mb_stripos($text, $word)) !== false) {
1783 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1784 $word = mb_substr($text, $pos, mb_strlen($word));
1785 $highlight = $doc->createElement('span');
1786 $highlight->appendChild(new DomText($word));
1787 $highlight->setAttribute('class', 'highlight');
1788 $fragment->appendChild($highlight);
1789 $text = mb_substr($text, $pos +
mb_strlen($word));
1792 if (!empty($text)) $fragment->appendChild(new DomText($text));
1794 $child->parentNode
->replaceChild($fragment, $child);
1799 $res = $doc->saveHTML();
1801 /* strip everything outside of <body>...</body> */
1803 $res_frag = array();
1804 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1805 return $res_frag[1];
1811 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1812 $xpath = new DOMXPath($doc);
1813 $entries = $xpath->query('//*');
1815 foreach ($entries as $entry) {
1816 if (!in_array($entry->nodeName
, $allowed_elements)) {
1817 $entry->parentNode
->removeChild($entry);
1820 if ($entry->hasAttributes()) {
1821 $attrs_to_remove = array();
1823 foreach ($entry->attributes
as $attr) {
1825 if (strpos($attr->nodeName
, 'on') === 0) {
1826 array_push($attrs_to_remove, $attr);
1829 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1830 array_push($attrs_to_remove, $attr);
1833 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1834 array_push($attrs_to_remove, $attr);
1838 foreach ($attrs_to_remove as $attr) {
1839 $entry->removeAttributeNode($attr);
1847 function trim_array($array) {
1849 array_walk($tmp, 'trim');
1853 function tag_is_valid($tag) {
1854 if (!$tag ||
is_numeric($tag) ||
mb_strlen($tag) > 250)
1860 function render_login_form() {
1861 header('Cache-Control: public');
1863 require_once "login_form.php";
1867 function T_sprintf() {
1868 $args = func_get_args();
1869 return vsprintf(__(array_shift($args)), $args);
1872 function print_checkpoint($n, $s) {
1873 $ts = microtime(true);
1874 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1878 function sanitize_tag($tag) {
1881 $tag = mb_strtolower($tag, 'utf-8');
1883 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1885 if (DB_TYPE
== "mysql") {
1886 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1892 function is_server_https() {
1893 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1896 function is_prefix_https() {
1897 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1900 // this returns SELF_URL_PATH sans ending slash
1901 function get_self_url_prefix() {
1902 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1903 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1905 return SELF_URL_PATH
;
1909 function encrypt_password($pass, $salt = '', $mode2 = false) {
1910 if ($salt && $mode2) {
1911 return "MODE2:" . hash('sha256', $salt . $pass);
1913 return "SHA1X:" . sha1("$salt:$pass");
1915 return "SHA1:" . sha1($pass);
1917 } // function encrypt_password
1919 function load_filters($feed_id, $owner_uid) {
1922 $feed_id = (int) $feed_id;
1923 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1926 $null_cat_qpart = "cat_id IS NULL OR";
1928 $null_cat_qpart = "";
1932 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1933 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1934 $sth->execute([$owner_uid]);
1936 $check_cats = array_merge(
1937 Feeds
::getParentCategories($cat_id, $owner_uid),
1940 $check_cats_str = join(",", $check_cats);
1941 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1943 while ($line = $sth->fetch()) {
1944 $filter_id = $line["id"];
1946 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1948 $sth2 = $pdo->prepare("SELECT
1949 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1950 FROM ttrss_filters2_rules AS r,
1951 ttrss_filter_types AS t
1953 (match_on IS NOT NULL OR
1954 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1955 (feed_id IS NULL OR feed_id = ?))) AND
1956 filter_type = t.id AND filter_id = ?");
1957 $sth2->execute([$feed_id, $filter_id]);
1962 while ($rule_line = $sth2->fetch()) {
1963 # print_r($rule_line);
1965 if ($rule_line["match_on"]) {
1966 $match_on = json_decode($rule_line["match_on"], true);
1968 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1971 $rule["reg_exp"] = $rule_line["reg_exp"];
1972 $rule["type"] = $rule_line["type_name"];
1973 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1975 array_push($rules, $rule);
1976 } else if (!$match_any_rule) {
1977 // this filter contains a rule that doesn't match to this feed/category combination
1978 // thus filter has to be rejected
1987 $rule["reg_exp"] = $rule_line["reg_exp"];
1988 $rule["type"] = $rule_line["type_name"];
1989 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1991 array_push($rules, $rule);
1995 if (count($rules) > 0) {
1996 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1997 FROM ttrss_filters2_actions AS a,
1998 ttrss_filter_actions AS t
2000 action_id = t.id AND filter_id = ?");
2001 $sth2->execute([$filter_id]);
2003 while ($action_line = $sth2->fetch()) {
2004 # print_r($action_line);
2007 $action["type"] = $action_line["type_name"];
2008 $action["param"] = $action_line["action_param"];
2010 array_push($actions, $action);
2015 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
2016 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
2017 $filter["rules"] = $rules;
2018 $filter["actions"] = $actions;
2020 if (count($rules) > 0 && count($actions) > 0) {
2021 array_push($filters, $filter);
2028 function get_score_pic($score) {
2030 return "score_high.png";
2031 } else if ($score > 0) {
2032 return "score_half_high.png";
2033 } else if ($score < -100) {
2034 return "score_low.png";
2035 } else if ($score < 0) {
2036 return "score_half_low.png";
2038 return "score_neutral.png";
2042 function init_plugins() {
2043 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
2048 function add_feed_category($feed_cat, $parent_cat_id = false) {
2050 if (!$feed_cat) return false;
2052 $feed_cat = mb_substr($feed_cat, 0, 250);
2053 if (!$parent_cat_id) $parent_cat_id = null;
2056 $tr_in_progress = false;
2059 $pdo->beginTransaction();
2060 } catch (Exception
$e) {
2061 $tr_in_progress = true;
2064 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2065 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2066 AND title = :title AND owner_uid = :uid");
2067 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2069 if (!$sth->fetch()) {
2071 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2073 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2075 if (!$tr_in_progress) $pdo->commit();
2086 * Fixes incomplete URLs by prepending "http://".
2087 * Also replaces feed:// with http://, and
2088 * prepends a trailing slash if the url is a domain name only.
2090 * @param string $url Possibly incomplete URL
2092 * @return string Fixed URL.
2094 function fix_url($url) {
2096 // support schema-less urls
2097 if (strpos($url, '//') === 0) {
2098 $url = 'https:' . $url;
2101 if (strpos($url, '://') === false) {
2102 $url = 'http://' . $url;
2103 } else if (substr($url, 0, 5) == 'feed:') {
2104 $url = 'http:' . substr($url, 5);
2107 //prepend slash if the URL has no slash in it
2108 // "http://www.example" -> "http://www.example/"
2109 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2113 //convert IDNA hostname to punycode if possible
2114 if (function_exists("idn_to_ascii")) {
2115 $parts = parse_url($url);
2116 if (mb_detect_encoding($parts['host']) != 'ASCII')
2118 $parts['host'] = idn_to_ascii($parts['host']);
2119 $url = build_url($parts);
2123 if ($url != "http:///")
2129 function validate_feed_url($url) {
2130 $parts = parse_url($url);
2132 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2136 /* function save_email_address($email) {
2137 // FIXME: implement persistent storage of emails
2139 if (!$_SESSION['stored_emails'])
2140 $_SESSION['stored_emails'] = array();
2142 if (!in_array($email, $_SESSION['stored_emails']))
2143 array_push($_SESSION['stored_emails'], $email);
2147 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2149 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2151 $is_cat = bool_to_sql_bool($is_cat);
2155 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2156 WHERE feed_id = ? AND is_cat = ?
2157 AND owner_uid = ?");
2158 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2160 if ($row = $sth->fetch()) {
2161 return $row["access_key"];
2163 $key = uniqid_short();
2165 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2166 (access_key, feed_id, is_cat, owner_uid)
2167 VALUES (?, ?, ?, ?)");
2169 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2175 function get_feeds_from_html($url, $content)
2177 $url = fix_url($url);
2178 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2180 libxml_use_internal_errors(true);
2182 $doc = new DOMDocument();
2183 $doc->loadHTML($content);
2184 $xpath = new DOMXPath($doc);
2185 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2186 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2187 $feedUrls = array();
2188 foreach ($entries as $entry) {
2189 if ($entry->hasAttribute('href')) {
2190 $title = $entry->getAttribute('title');
2192 $title = $entry->getAttribute('type');
2194 $feedUrl = rewrite_relative_url(
2195 $baseUrl, $entry->getAttribute('href')
2197 $feedUrls[$feedUrl] = $title;
2203 function is_html($content) {
2204 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2207 function url_is_html($url, $login = false, $pass = false) {
2208 return is_html(fetch_file_contents($url, false, $login, $pass));
2211 function build_url($parts) {
2212 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2215 function cleanup_url_path($path) {
2216 $path = str_replace("/./", "/", $path);
2217 $path = str_replace("//", "/", $path);
2223 * Converts a (possibly) relative URL to a absolute one.
2225 * @param string $url Base URL (i.e. from where the document is)
2226 * @param string $rel_url Possibly relative URL in the document
2228 * @return string Absolute URL
2230 function rewrite_relative_url($url, $rel_url) {
2231 if (strpos($rel_url, "://") !== false) {
2233 } else if (strpos($rel_url, "//") === 0) {
2234 # protocol-relative URL (rare but they exist)
2236 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2237 # magnet:, feed:, etc
2239 } else if (strpos($rel_url, "/") === 0) {
2240 $parts = parse_url($url);
2241 $parts['path'] = $rel_url;
2242 $parts['path'] = cleanup_url_path($parts['path']);
2244 return build_url($parts);
2247 $parts = parse_url($url);
2248 if (!isset($parts['path'])) {
2249 $parts['path'] = '/';
2251 $dir = $parts['path'];
2252 if (substr($dir, -1) !== '/') {
2253 $dir = dirname($parts['path']);
2254 $dir !== '/' && $dir .= '/';
2256 $parts['path'] = $dir . $rel_url;
2257 $parts['path'] = cleanup_url_path($parts['path']);
2259 return build_url($parts);
2263 function cleanup_tags($days = 14, $limit = 1000) {
2265 $days = (int) $days;
2267 if (DB_TYPE
== "pgsql") {
2268 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2269 } else if (DB_TYPE
== "mysql") {
2270 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2277 while ($limit > 0) {
2280 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2281 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2282 WHERE post_int_id = int_id AND $interval_query AND
2283 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2284 $sth->execute([$limit]);
2288 while ($line = $sth->fetch()) {
2289 array_push($ids, $line['id']);
2292 if (count($ids) > 0) {
2293 $ids = join(",", $ids);
2295 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2296 $tags_deleted = $usth->rowCount();
2301 $limit -= $limit_part;
2304 return $tags_deleted;
2307 function print_user_stylesheet() {
2308 $value = get_pref('USER_STYLESHEET');
2311 print "<style type=\"text/css\">";
2312 print str_replace("<br/>", "\n", $value);
2318 function filter_to_sql($filter, $owner_uid) {
2323 if (DB_TYPE
== "pgsql")
2326 $reg_qpart = "REGEXP";
2328 foreach ($filter["rules"] AS $rule) {
2329 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2330 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2331 $rule['reg_exp']) !== FALSE;
2333 if ($regexp_valid) {
2335 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2337 switch ($rule["type"]) {
2339 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2340 $rule['reg_exp'] . "')";
2343 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2344 $rule['reg_exp'] . "')";
2347 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2348 $rule['reg_exp'] . "') OR LOWER(" .
2349 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2352 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2353 $rule['reg_exp'] . "')";
2356 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2357 $rule['reg_exp'] . "')";
2360 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2361 $rule['reg_exp'] . "')";
2365 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2367 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2368 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2371 if (isset($rule["cat_id"])) {
2373 if ($rule["cat_id"] > 0) {
2374 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2375 array_push($children, $rule["cat_id"]);
2376 $children = array_map("intval", $children);
2378 $children = join(",", $children);
2380 $cat_qpart = "cat_id IN ($children)";
2382 $cat_qpart = "cat_id IS NULL";
2385 $qpart .= " AND $cat_qpart";
2388 $qpart .= " AND feed_id IS NOT NULL";
2390 array_push($query, "($qpart)");
2395 if (count($query) > 0) {
2396 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2398 $fullquery = "(false)";
2401 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2406 if (!function_exists('gzdecode')) {
2407 function gzdecode($string) { // no support for 2nd argument
2408 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2409 base64_encode($string));
2413 function get_random_bytes($length) {
2414 if (function_exists('openssl_random_pseudo_bytes')) {
2415 return openssl_random_pseudo_bytes($length);
2419 for ($i = 0; $i < $length; $i++
)
2420 $output .= chr(mt_rand(0, 255));
2426 function read_stdin() {
2427 $fp = fopen("php://stdin", "r");
2430 $line = trim(fgets($fp));
2438 function implements_interface($class, $interface) {
2439 return in_array($interface, class_implements($class));
2442 function get_minified_js($files) {
2446 foreach ($files as $js) {
2447 if (!isset($_GET['debug'])) {
2448 $cached_file = CACHE_DIR
. "/js/".basename($js);
2450 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2452 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2454 if ($header && $contents) {
2455 list($htag, $hversion) = explode(":", $header);
2457 if ($htag == "tt-rss" && $hversion == VERSION
) {
2464 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2465 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2469 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2476 function calculate_dep_timestamp() {
2477 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2481 foreach ($files as $file) {
2482 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2488 function T_js_decl($s1, $s2) {
2490 $s1 = preg_replace("/\n/", "", $s1);
2491 $s2 = preg_replace("/\n/", "", $s2);
2493 $s1 = preg_replace("/\"/", "\\\"", $s1);
2494 $s2 = preg_replace("/\"/", "\\\"", $s2);
2496 return "T_messages[\"$s1\"] = \"$s2\";\n";
2500 function init_js_translations() {
2502 print 'var T_messages = new Object();
2505 if (T_messages[msg]) {
2506 return T_messages[msg];
2512 function ngettext(msg1, msg2, n) {
2513 return __((parseInt(n) > 1) ? msg2 : msg1);
2516 $l10n = _get_reader();
2518 for ($i = 0; $i < $l10n->total
; $i++
) {
2519 $orig = $l10n->get_original_string($i);
2520 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2521 $key = explode(chr(0), $orig);
2522 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2523 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2525 $translation = __($orig);
2526 print T_js_decl($orig, $translation);
2531 function get_theme_path($theme) {
2532 if ($theme == "default.php")
2533 return "css/default.css";
2535 $check = "themes/$theme";
2536 if (file_exists($check)) return $check;
2538 $check = "themes.local/$theme";
2539 if (file_exists($check)) return $check;
2542 function theme_valid($theme) {
2543 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2545 if (in_array($theme, $bundled_themes)) return true;
2547 $file = "themes/" . basename($theme);
2549 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2551 if (file_exists($file) && is_readable($file)) {
2552 $fh = fopen($file, "r");
2555 $header = fgets($fh);
2558 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2566 * @SuppressWarnings(unused)
2568 function error_json($code) {
2569 require_once "errors.php";
2571 @$message = $ERRORS[$code];
2573 return json_encode(array("error" =>
2574 array("code" => $code, "message" => $message)));
2578 /*function abs_to_rel_path($dir) {
2579 $tmp = str_replace(dirname(__DIR__), "", $dir);
2581 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2586 function get_upload_error_message($code) {
2589 0 => __('There is no error, the file uploaded with success'),
2590 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2591 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2592 3 => __('The uploaded file was only partially uploaded'),
2593 4 => __('No file was uploaded'),
2594 6 => __('Missing a temporary folder'),
2595 7 => __('Failed to write file to disk.'),
2596 8 => __('A PHP extension stopped the file upload.'),
2599 return $errors[$code];
2602 function base64_img($filename) {
2603 if (file_exists($filename)) {
2604 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2606 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2612 /* this is essentially a wrapper for readfile() which allows plugins to hook
2613 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2615 hook function should return true if request was handled (or at least attempted to)
2617 note that this can be called without user context so the plugin to handle this
2618 should be loaded systemwide in config.php */
2619 function send_local_file($filename) {
2620 if (file_exists($filename)) {
2622 if (is_writable($filename)) touch($filename);
2624 $tmppluginhost = new PluginHost();
2626 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2627 $tmppluginhost->load_data();
2629 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2630 if ($plugin->hook_send_local_file($filename)) return true;
2633 $mimetype = mime_content_type($filename);
2635 // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2636 // video files are detected as octet-stream by mime_content_type()
2638 if ($mimetype == "application/octet-stream")
2639 $mimetype = "video/mp4";
2641 header("Content-type: $mimetype");
2643 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2644 header("Last-Modified: $stamp", true);
2646 return readfile($filename);
2652 function check_mysql_tables() {
2655 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2656 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2657 $sth->execute([DB_NAME
]);
2661 while ($line = $sth->fetch()) {
2662 array_push($bad_tables, $line);
2668 function validate_field($string, $allowed, $default = "") {
2669 if (in_array($string, $allowed))
2675 function arr_qmarks($arr) {
2676 return str_repeat('?,', count($arr) - 1) . '?';