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
) {
703 $auth_module = false;
705 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
707 $user_id = (int) $plugin->authenticate($login, $password);
710 $auth_module = strtolower(get_class($plugin));
715 if ($user_id && !$check_only) {
718 session_regenerate_id(true);
720 $_SESSION["uid"] = $user_id;
721 $_SESSION["version"] = VERSION_STATIC
;
722 $_SESSION["auth_module"] = $auth_module;
725 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
727 $sth->execute([$user_id]);
728 $row = $sth->fetch();
730 $_SESSION["name"] = $row["login"];
731 $_SESSION["access_level"] = $row["access_level"];
732 $_SESSION["csrf_token"] = uniqid_short();
734 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
735 $usth->execute([$user_id]);
737 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
738 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
739 $_SESSION["pwd_hash"] = $row["pwd_hash"];
741 $_SESSION["last_version_check"] = time();
743 initialize_user_prefs($_SESSION["uid"]);
752 $_SESSION["uid"] = 1;
753 $_SESSION["name"] = "admin";
754 $_SESSION["access_level"] = 10;
756 $_SESSION["hide_hello"] = true;
757 $_SESSION["hide_logout"] = true;
759 $_SESSION["auth_module"] = false;
761 if (!$_SESSION["csrf_token"]) {
762 $_SESSION["csrf_token"] = uniqid_short();
765 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
767 initialize_user_prefs($_SESSION["uid"]);
773 // this is used for user http parameters unless HTML code is actually needed
774 function clean($param) {
775 if (is_array($param)) {
776 return array_map("strip_tags", $param);
777 } else if (is_string($param)) {
778 return strip_tags($param);
784 function make_password($length = 8) {
787 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
791 while ($i < $length) {
792 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
794 if (!strstr($password, $char)) {
802 // this is called after user is created to initialize default feeds, labels
805 // user preferences are checked on every login, not here
807 function initialize_user($uid) {
811 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
812 values (?, 'Tiny Tiny RSS: Forum',
813 'http://tt-rss.org/forum/rss.php')");
814 $sth->execute([$uid]);
817 function logout_user() {
819 if (isset($_COOKIE[session_name()])) {
820 setcookie(session_name(), '', time()-42000, '/');
825 function validate_csrf($csrf_token) {
826 return $csrf_token == $_SESSION['csrf_token'];
829 function load_user_plugins($owner_uid, $pluginhost = false) {
831 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
833 if ($owner_uid && SCHEMA_VERSION
>= 100) {
834 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
836 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
838 if (get_schema_version() > 100) {
839 $pluginhost->load_data();
844 function login_sequence() {
847 if (SINGLE_USER_MODE
) {
849 authenticate_user("admin", null);
851 load_user_plugins($_SESSION["uid"]);
853 if (!validate_session()) $_SESSION["uid"] = false;
855 if (!$_SESSION["uid"]) {
857 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
858 $_SESSION["ref_schema_version"] = get_schema_version(true);
860 authenticate_user(null, null, true);
863 if (!$_SESSION["uid"]) {
871 /* bump login timestamp */
872 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
873 $sth->execute([$_SESSION['uid']]);
875 $_SESSION["last_login_update"] = time();
878 if ($_SESSION["uid"]) {
880 load_user_plugins($_SESSION["uid"]);
884 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
886 (SELECT COUNT(id) FROM ttrss_feeds WHERE
887 ttrss_feeds.id = feed_id) = 0");
889 $sth->execute([$_SESSION['uid']]);
891 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
893 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
894 ttrss_feed_categories.id = feed_id) = 0");
896 $sth->execute([$_SESSION['uid']]);
902 function truncate_string($str, $max_len, $suffix = '…') {
903 if (mb_strlen($str, "utf-8") > $max_len) {
904 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
911 function truncate_middle($str, $max_len, $suffix = '…') {
912 if (strlen($str) > $max_len) {
913 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
919 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
922 $source_tz = new DateTimeZone($source_tz);
923 } catch (Exception
$e) {
924 $source_tz = new DateTimeZone('UTC');
928 $dest_tz = new DateTimeZone($dest_tz);
929 } catch (Exception
$e) {
930 $dest_tz = new DateTimeZone('UTC');
933 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
934 return $dt->format('U') +
$dest_tz->getOffset($dt);
937 function make_local_datetime($timestamp, $long, $owner_uid = false,
938 $no_smart_dt = false, $eta_min = false) {
940 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
941 if (!$timestamp) $timestamp = '1970-01-01 0:00';
946 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
948 $timestamp = substr($timestamp, 0, 19);
950 # We store date in UTC internally
951 $dt = new DateTime($timestamp, $utc_tz);
953 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
955 if ($user_tz_string != 'Automatic') {
958 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
959 } catch (Exception
$e) {
963 $tz_offset = $user_tz->getOffset($dt);
965 $tz_offset = (int) -$_SESSION["clientTzOffset"];
968 $user_timestamp = $dt->format('U') +
$tz_offset;
971 return smart_date_time($user_timestamp,
972 $tz_offset, $owner_uid, $eta_min);
975 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
977 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
979 return date($format, $user_timestamp);
983 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
984 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
986 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
987 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
988 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
989 return date("G:i", $timestamp);
990 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
991 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
992 return date($format, $timestamp);
994 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
995 return date($format, $timestamp);
999 function sql_bool_to_bool($s) {
1000 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
1003 function bool_to_sql_bool($s) {
1007 // Session caching removed due to causing wrong redirects to upgrade
1008 // script when get_schema_version() is called on an obsolete session
1009 // created on a previous schema version.
1010 function get_schema_version($nocache = false) {
1011 global $schema_version;
1015 if (!$schema_version && !$nocache) {
1016 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
1017 $version = $row["schema_version"];
1018 $schema_version = $version;
1021 return $schema_version;
1025 function sanity_check() {
1026 require_once 'errors.php';
1030 $schema_version = get_schema_version(true);
1032 if ($schema_version != SCHEMA_VERSION
) {
1036 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1039 function file_is_locked($filename) {
1040 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
1041 if (function_exists('flock')) {
1042 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1044 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1045 flock($fp, LOCK_UN
);
1055 return true; // consider the file always locked and skip the test
1062 function make_lockfile($filename) {
1063 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1065 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1066 $stat_h = fstat($fp);
1067 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1069 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1070 if ($stat_h["ino"] != $stat_f["ino"] ||
1071 $stat_h["dev"] != $stat_f["dev"]) {
1077 if (function_exists('posix_getpid')) {
1078 fwrite($fp, posix_getpid() . "\n");
1086 function make_stampfile($filename) {
1087 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1089 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1090 fwrite($fp, time() . "\n");
1091 flock($fp, LOCK_UN
);
1099 function sql_random_function() {
1100 if (DB_TYPE
== "mysql") {
1107 function getFeedUnread($feed, $is_cat = false) {
1108 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1111 function checkbox_to_sql_bool($val) {
1112 return ($val == "on") ?
1 : 0;
1115 function uniqid_short() {
1116 return uniqid(base_convert(rand(), 10, 36));
1119 function make_init_params() {
1122 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1123 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1124 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1125 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1127 $params[strtolower($param)] = (int) get_pref($param);
1130 $params["icons_url"] = ICONS_URL
;
1131 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1132 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1133 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1134 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1135 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1136 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1137 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1139 $theme = get_pref( "USER_CSS_THEME", false, false);
1140 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1142 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1144 $params["php_platform"] = PHP_OS
;
1145 $params["php_version"] = PHP_VERSION
;
1147 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1151 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1152 ttrss_feeds WHERE owner_uid = ?");
1153 $sth->execute([$_SESSION['uid']]);
1154 $row = $sth->fetch();
1156 $max_feed_id = $row["mid"];
1157 $num_feeds = $row["nf"];
1159 $params["max_feed_id"] = (int) $max_feed_id;
1160 $params["num_feeds"] = (int) $num_feeds;
1162 $params["hotkeys"] = get_hotkeys_map();
1164 $params["csrf_token"] = $_SESSION["csrf_token"];
1165 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1167 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1169 $params["icon_alert"] = base64_img("images/alert.png");
1170 $params["icon_information"] = base64_img("images/information.png");
1171 $params["icon_cross"] = base64_img("images/cross.png");
1172 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1174 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1179 function get_hotkeys_info() {
1181 __("Navigation") => array(
1182 "next_feed" => __("Open next feed"),
1183 "prev_feed" => __("Open previous feed"),
1184 "next_article" => __("Open next article"),
1185 "prev_article" => __("Open previous article"),
1186 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1187 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1188 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1189 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1190 "search_dialog" => __("Show search dialog")),
1191 __("Article") => array(
1192 "toggle_mark" => __("Toggle starred"),
1193 "toggle_publ" => __("Toggle published"),
1194 "toggle_unread" => __("Toggle unread"),
1195 "edit_tags" => __("Edit tags"),
1196 "open_in_new_window" => __("Open in new window"),
1197 "catchup_below" => __("Mark below as read"),
1198 "catchup_above" => __("Mark above as read"),
1199 "article_scroll_down" => __("Scroll down"),
1200 "article_scroll_up" => __("Scroll up"),
1201 "select_article_cursor" => __("Select article under cursor"),
1202 "email_article" => __("Email article"),
1203 "close_article" => __("Close/collapse article"),
1204 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1205 "toggle_widescreen" => __("Toggle widescreen mode"),
1206 "toggle_embed_original" => __("Toggle embed original")),
1207 __("Article selection") => array(
1208 "select_all" => __("Select all articles"),
1209 "select_unread" => __("Select unread"),
1210 "select_marked" => __("Select starred"),
1211 "select_published" => __("Select published"),
1212 "select_invert" => __("Invert selection"),
1213 "select_none" => __("Deselect everything")),
1214 __("Feed") => array(
1215 "feed_refresh" => __("Refresh current feed"),
1216 "feed_unhide_read" => __("Un/hide read feeds"),
1217 "feed_subscribe" => __("Subscribe to feed"),
1218 "feed_edit" => __("Edit feed"),
1219 "feed_catchup" => __("Mark as read"),
1220 "feed_reverse" => __("Reverse headlines"),
1221 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1222 "feed_debug_update" => __("Debug feed update"),
1223 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1224 "catchup_all" => __("Mark all feeds as read"),
1225 "cat_toggle_collapse" => __("Un/collapse current category"),
1226 "toggle_combined_mode" => __("Toggle combined mode"),
1227 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1228 __("Go to") => array(
1229 "goto_all" => __("All articles"),
1230 "goto_fresh" => __("Fresh"),
1231 "goto_marked" => __("Starred"),
1232 "goto_published" => __("Published"),
1233 "goto_tagcloud" => __("Tag cloud"),
1234 "goto_prefs" => __("Preferences")),
1235 __("Other") => array(
1236 "create_label" => __("Create label"),
1237 "create_filter" => __("Create filter"),
1238 "collapse_sidebar" => __("Un/collapse sidebar"),
1239 "help_dialog" => __("Show help dialog"))
1242 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1243 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1249 function get_hotkeys_map() {
1251 // "navigation" => array(
1254 "n" => "next_article",
1255 "p" => "prev_article",
1256 "(38)|up" => "prev_article",
1257 "(40)|down" => "next_article",
1258 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1259 // "^(40)|Ctrl-down" => "next_article_noscroll",
1260 "(191)|/" => "search_dialog",
1261 // "article" => array(
1262 "s" => "toggle_mark",
1263 "*s" => "toggle_publ",
1264 "u" => "toggle_unread",
1265 "*t" => "edit_tags",
1266 "o" => "open_in_new_window",
1267 "c p" => "catchup_below",
1268 "c n" => "catchup_above",
1269 "*n" => "article_scroll_down",
1270 "*p" => "article_scroll_up",
1271 "*(38)|Shift+up" => "article_scroll_up",
1272 "*(40)|Shift+down" => "article_scroll_down",
1273 "a *w" => "toggle_widescreen",
1274 "a e" => "toggle_embed_original",
1275 "e" => "email_article",
1276 "a q" => "close_article",
1277 // "article_selection" => array(
1278 "a a" => "select_all",
1279 "a u" => "select_unread",
1280 "a *u" => "select_marked",
1281 "a p" => "select_published",
1282 "a i" => "select_invert",
1283 "a n" => "select_none",
1285 "f r" => "feed_refresh",
1286 "f a" => "feed_unhide_read",
1287 "f s" => "feed_subscribe",
1288 "f e" => "feed_edit",
1289 "f q" => "feed_catchup",
1290 "f x" => "feed_reverse",
1291 "f g" => "feed_toggle_vgroup",
1292 "f *d" => "feed_debug_update",
1293 "f *g" => "feed_debug_viewfeed",
1294 "f *c" => "toggle_combined_mode",
1295 "f c" => "toggle_cdm_expanded",
1296 "*q" => "catchup_all",
1297 "x" => "cat_toggle_collapse",
1299 "g a" => "goto_all",
1300 "g f" => "goto_fresh",
1301 "g s" => "goto_marked",
1302 "g p" => "goto_published",
1303 "g t" => "goto_tagcloud",
1304 "g *p" => "goto_prefs",
1305 // "other" => array(
1306 "(9)|Tab" => "select_article_cursor", // tab
1307 "c l" => "create_label",
1308 "c f" => "create_filter",
1309 "c s" => "collapse_sidebar",
1310 "^(191)|Ctrl+/" => "help_dialog",
1313 if (get_pref('COMBINED_DISPLAY_MODE')) {
1314 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1315 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1318 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1319 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1322 $prefixes = array();
1324 foreach (array_keys($hotkeys) as $hotkey) {
1325 $pair = explode(" ", $hotkey, 2);
1327 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1328 array_push($prefixes, $pair[0]);
1332 return array($prefixes, $hotkeys);
1335 function check_for_update() {
1336 if (defined("GIT_VERSION_TIMESTAMP")) {
1337 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1340 $content = json_decode($content, true);
1342 if ($content && isset($content["changeset"])) {
1343 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1344 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1346 return $content["changeset"]["id"];
1355 function make_runtime_info($disable_update_check = false) {
1360 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1361 ttrss_feeds WHERE owner_uid = ?");
1362 $sth->execute([$_SESSION['uid']]);
1363 $row = $sth->fetch();
1365 $max_feed_id = $row['mid'];
1366 $num_feeds = $row['nf'];
1368 $data["max_feed_id"] = (int) $max_feed_id;
1369 $data["num_feeds"] = (int) $num_feeds;
1371 $data['last_article_id'] = Article
::getLastArticleId();
1372 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1374 $data['dep_ts'] = calculate_dep_timestamp();
1375 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1377 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1379 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1380 $update_result = @check_for_update
();
1382 $data["update_result"] = $update_result;
1384 $_SESSION["last_version_check"] = time();
1387 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1389 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1391 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1393 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1396 $stamp_delta = time() - $stamp;
1398 if ($stamp_delta > 1800) {
1402 $_SESSION["daemon_stamp_check"] = time();
1405 $data['daemon_stamp_ok'] = $stamp_check;
1407 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1409 $data['daemon_stamp'] = $stamp_fmt;
1417 function search_to_sql($search, $search_language) {
1419 $keywords = str_getcsv(trim($search), " ");
1420 $query_keywords = array();
1421 $search_words = array();
1422 $search_query_leftover = array();
1426 if ($search_language)
1427 $search_language = $pdo->quote(mb_strtolower($search_language));
1429 $search_language = $pdo->quote("english");
1431 foreach ($keywords as $k) {
1432 if (strpos($k, "-") === 0) {
1439 $commandpair = explode(":", mb_strtolower($k), 2);
1441 switch ($commandpair[0]) {
1443 if ($commandpair[1]) {
1444 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1445 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1447 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1448 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1449 array_push($search_words, $k);
1453 if ($commandpair[1]) {
1454 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1455 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1457 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1458 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1459 array_push($search_words, $k);
1463 if ($commandpair[1]) {
1464 if ($commandpair[1] == "true")
1465 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1466 else if ($commandpair[1] == "false")
1467 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1469 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1470 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1472 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1473 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1474 if (!$not) array_push($search_words, $k);
1479 if ($commandpair[1]) {
1480 if ($commandpair[1] == "true")
1481 array_push($query_keywords, "($not (marked = true))");
1483 array_push($query_keywords, "($not (marked = false))");
1485 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1486 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1487 if (!$not) array_push($search_words, $k);
1491 if ($commandpair[1]) {
1492 if ($commandpair[1] == "true")
1493 array_push($query_keywords, "($not (published = true))");
1495 array_push($query_keywords, "($not (published = false))");
1498 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1499 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1500 if (!$not) array_push($search_words, $k);
1504 if ($commandpair[1]) {
1505 if ($commandpair[1] == "true")
1506 array_push($query_keywords, "($not (unread = true))");
1508 array_push($query_keywords, "($not (unread = false))");
1511 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1512 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1513 if (!$not) array_push($search_words, $k);
1517 if (strpos($k, "@") === 0) {
1519 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1520 $orig_ts = strtotime(substr($k, 1));
1521 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1523 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1525 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1528 if (DB_TYPE
== "pgsql") {
1529 $k = mb_strtolower($k);
1530 array_push($search_query_leftover, $not ?
"!$k" : $k);
1532 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1533 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1536 if (!$not) array_push($search_words, $k);
1541 if (count($search_query_leftover) > 0) {
1542 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1544 if (DB_TYPE
== "pgsql") {
1545 array_push($query_keywords,
1546 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1551 $search_query_part = implode("AND", $query_keywords);
1553 return array($search_query_part, $search_words);
1556 function iframe_whitelisted($entry) {
1557 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1559 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1562 foreach ($whitelist as $w) {
1563 if ($src == $w ||
$src == "www.$w")
1571 // check for locally cached (media) URLs and rewrite to local versions
1572 // this is called separately after sanitize() and plugin render article hooks to allow
1573 // plugins work on original source URLs used before caching
1575 function rewrite_cached_urls($str) {
1576 $charset_hack = '<head>
1577 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1580 $res = trim($str); if (!$res) return '';
1582 $doc = new DOMDocument();
1583 $doc->loadHTML($charset_hack . $res);
1584 $xpath = new DOMXPath($doc);
1586 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
1588 $need_saving = false;
1590 foreach ($entries as $entry) {
1592 if ($entry->hasAttribute('src') ||
$entry->hasAttribute('poster')) {
1594 // should be already absolutized because this is called after sanitize()
1595 $src = $entry->hasAttribute('poster') ?
$entry->getAttribute('poster') : $entry->getAttribute('src');
1596 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1598 if (file_exists($cached_filename)) {
1600 // this is strictly cosmetic
1601 if ($entry->tagName
== 'img') {
1603 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1605 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1611 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1613 if ($entry->hasAttribute('poster'))
1614 $entry->setAttribute('poster', $src);
1616 $entry->setAttribute('src', $src);
1618 $need_saving = true;
1624 $doc->removeChild($doc->firstChild
); //remove doctype
1625 $res = $doc->saveHTML();
1631 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1632 if (!$owner) $owner = $_SESSION["uid"];
1634 $res = trim($str); if (!$res) return '';
1636 $charset_hack = '<head>
1637 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1640 $res = trim($res); if (!$res) return '';
1642 libxml_use_internal_errors(true);
1644 $doc = new DOMDocument();
1645 $doc->loadHTML($charset_hack . $res);
1646 $xpath = new DOMXPath($doc);
1648 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1650 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1652 foreach ($entries as $entry) {
1654 if ($entry->hasAttribute('href')) {
1655 $entry->setAttribute('href',
1656 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1658 $entry->setAttribute('rel', 'noopener noreferrer');
1661 if ($entry->hasAttribute('src')) {
1662 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1664 // cache stuff has gone to rewrite_cached_urls()
1666 $entry->setAttribute('src', $src);
1669 if ($entry->nodeName
== 'img') {
1670 $entry->setAttribute('referrerpolicy', 'no-referrer');
1672 $entry->removeAttribute('width');
1673 $entry->removeAttribute('height');
1675 if ($entry->hasAttribute('src')) {
1676 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1678 if (is_prefix_https() && !$is_https_url) {
1680 if ($entry->hasAttribute('srcset')) {
1681 $entry->removeAttribute('srcset');
1684 if ($entry->hasAttribute('sizes')) {
1685 $entry->removeAttribute('sizes');
1691 if ($entry->hasAttribute('src') &&
1692 ($owner && get_pref("STRIP_IMAGES", $owner)) ||
$force_remove_images ||
$_SESSION["bw_limit"]) {
1694 $p = $doc->createElement('p');
1696 $a = $doc->createElement('a');
1697 $a->setAttribute('href', $entry->getAttribute('src'));
1699 $a->appendChild(new DOMText($entry->getAttribute('src')));
1700 $a->setAttribute('target', '_blank');
1701 $a->setAttribute('rel', 'noopener noreferrer');
1703 $p->appendChild($a);
1705 if ($entry->nodeName
== 'source') {
1707 if ($entry->parentNode
&& $entry->parentNode
->parentNode
)
1708 $entry->parentNode
->parentNode
->replaceChild($p, $entry->parentNode
);
1710 } else if ($entry->nodeName
== 'img') {
1712 if ($entry->parentNode
)
1713 $entry->parentNode
->replaceChild($p, $entry);
1718 if (strtolower($entry->nodeName
) == "a") {
1719 $entry->setAttribute("target", "_blank");
1720 $entry->setAttribute("rel", "noopener noreferrer");
1724 $entries = $xpath->query('//iframe');
1725 foreach ($entries as $entry) {
1726 if (!iframe_whitelisted($entry)) {
1727 $entry->setAttribute('sandbox', 'allow-scripts');
1729 if (is_prefix_https()) {
1730 $entry->setAttribute("src",
1731 str_replace("http://", "https://",
1732 $entry->getAttribute("src")));
1737 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1738 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1739 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1740 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1741 'dt', 'em', 'footer', 'figure', 'figcaption',
1742 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1743 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1744 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1745 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1746 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1747 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1749 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1751 $disallowed_attributes = array('id', 'style', 'class');
1753 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1754 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1755 if (is_array($retval)) {
1757 $allowed_elements = $retval[1];
1758 $disallowed_attributes = $retval[2];
1764 $doc->removeChild($doc->firstChild
); //remove doctype
1765 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1767 if ($highlight_words) {
1768 foreach ($highlight_words as $word) {
1770 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1772 $elements = $xpath->query("//*/text()");
1774 foreach ($elements as $child) {
1776 $fragment = $doc->createDocumentFragment();
1777 $text = $child->textContent
;
1779 while (($pos = mb_stripos($text, $word)) !== false) {
1780 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1781 $word = mb_substr($text, $pos, mb_strlen($word));
1782 $highlight = $doc->createElement('span');
1783 $highlight->appendChild(new DomText($word));
1784 $highlight->setAttribute('class', 'highlight');
1785 $fragment->appendChild($highlight);
1786 $text = mb_substr($text, $pos +
mb_strlen($word));
1789 if (!empty($text)) $fragment->appendChild(new DomText($text));
1791 $child->parentNode
->replaceChild($fragment, $child);
1796 $res = $doc->saveHTML();
1798 /* strip everything outside of <body>...</body> */
1800 $res_frag = array();
1801 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1802 return $res_frag[1];
1808 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1809 $xpath = new DOMXPath($doc);
1810 $entries = $xpath->query('//*');
1812 foreach ($entries as $entry) {
1813 if (!in_array($entry->nodeName
, $allowed_elements)) {
1814 $entry->parentNode
->removeChild($entry);
1817 if ($entry->hasAttributes()) {
1818 $attrs_to_remove = array();
1820 foreach ($entry->attributes
as $attr) {
1822 if (strpos($attr->nodeName
, 'on') === 0) {
1823 array_push($attrs_to_remove, $attr);
1826 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1827 array_push($attrs_to_remove, $attr);
1830 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1831 array_push($attrs_to_remove, $attr);
1835 foreach ($attrs_to_remove as $attr) {
1836 $entry->removeAttributeNode($attr);
1844 function trim_array($array) {
1846 array_walk($tmp, 'trim');
1850 function tag_is_valid($tag) {
1851 if (!$tag ||
is_numeric($tag) ||
mb_strlen($tag) > 250)
1857 function render_login_form() {
1858 header('Cache-Control: public');
1860 require_once "login_form.php";
1864 function T_sprintf() {
1865 $args = func_get_args();
1866 return vsprintf(__(array_shift($args)), $args);
1869 function print_checkpoint($n, $s) {
1870 $ts = microtime(true);
1871 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1875 function sanitize_tag($tag) {
1878 $tag = mb_strtolower($tag, 'utf-8');
1880 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1882 if (DB_TYPE
== "mysql") {
1883 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1889 function is_server_https() {
1890 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1893 function is_prefix_https() {
1894 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1897 // this returns SELF_URL_PATH sans ending slash
1898 function get_self_url_prefix() {
1899 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1900 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1902 return SELF_URL_PATH
;
1906 function encrypt_password($pass, $salt = '', $mode2 = false) {
1907 if ($salt && $mode2) {
1908 return "MODE2:" . hash('sha256', $salt . $pass);
1910 return "SHA1X:" . sha1("$salt:$pass");
1912 return "SHA1:" . sha1($pass);
1914 } // function encrypt_password
1916 function load_filters($feed_id, $owner_uid) {
1919 $feed_id = (int) $feed_id;
1920 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1923 $null_cat_qpart = "cat_id IS NULL OR";
1925 $null_cat_qpart = "";
1929 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1930 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1931 $sth->execute([$owner_uid]);
1933 $check_cats = array_merge(
1934 Feeds
::getParentCategories($cat_id, $owner_uid),
1937 $check_cats_str = join(",", $check_cats);
1938 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1940 while ($line = $sth->fetch()) {
1941 $filter_id = $line["id"];
1943 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1945 $sth2 = $pdo->prepare("SELECT
1946 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1947 FROM ttrss_filters2_rules AS r,
1948 ttrss_filter_types AS t
1950 (match_on IS NOT NULL OR
1951 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1952 (feed_id IS NULL OR feed_id = ?))) AND
1953 filter_type = t.id AND filter_id = ?");
1954 $sth2->execute([$feed_id, $filter_id]);
1959 while ($rule_line = $sth2->fetch()) {
1960 # print_r($rule_line);
1962 if ($rule_line["match_on"]) {
1963 $match_on = json_decode($rule_line["match_on"], true);
1965 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1968 $rule["reg_exp"] = $rule_line["reg_exp"];
1969 $rule["type"] = $rule_line["type_name"];
1970 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1972 array_push($rules, $rule);
1973 } else if (!$match_any_rule) {
1974 // this filter contains a rule that doesn't match to this feed/category combination
1975 // thus filter has to be rejected
1984 $rule["reg_exp"] = $rule_line["reg_exp"];
1985 $rule["type"] = $rule_line["type_name"];
1986 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1988 array_push($rules, $rule);
1992 if (count($rules) > 0) {
1993 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1994 FROM ttrss_filters2_actions AS a,
1995 ttrss_filter_actions AS t
1997 action_id = t.id AND filter_id = ?");
1998 $sth2->execute([$filter_id]);
2000 while ($action_line = $sth2->fetch()) {
2001 # print_r($action_line);
2004 $action["type"] = $action_line["type_name"];
2005 $action["param"] = $action_line["action_param"];
2007 array_push($actions, $action);
2012 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
2013 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
2014 $filter["rules"] = $rules;
2015 $filter["actions"] = $actions;
2017 if (count($rules) > 0 && count($actions) > 0) {
2018 array_push($filters, $filter);
2025 function get_score_pic($score) {
2027 return "score_high.png";
2028 } else if ($score > 0) {
2029 return "score_half_high.png";
2030 } else if ($score < -100) {
2031 return "score_low.png";
2032 } else if ($score < 0) {
2033 return "score_half_low.png";
2035 return "score_neutral.png";
2039 function init_plugins() {
2040 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
2045 function add_feed_category($feed_cat, $parent_cat_id = false) {
2047 if (!$feed_cat) return false;
2049 $feed_cat = mb_substr($feed_cat, 0, 250);
2050 if (!$parent_cat_id) $parent_cat_id = null;
2053 $tr_in_progress = false;
2056 $pdo->beginTransaction();
2057 } catch (Exception
$e) {
2058 $tr_in_progress = true;
2061 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2062 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2063 AND title = :title AND owner_uid = :uid");
2064 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2066 if (!$sth->fetch()) {
2068 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2070 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2072 if (!$tr_in_progress) $pdo->commit();
2083 * Fixes incomplete URLs by prepending "http://".
2084 * Also replaces feed:// with http://, and
2085 * prepends a trailing slash if the url is a domain name only.
2087 * @param string $url Possibly incomplete URL
2089 * @return string Fixed URL.
2091 function fix_url($url) {
2093 // support schema-less urls
2094 if (strpos($url, '//') === 0) {
2095 $url = 'https:' . $url;
2098 if (strpos($url, '://') === false) {
2099 $url = 'http://' . $url;
2100 } else if (substr($url, 0, 5) == 'feed:') {
2101 $url = 'http:' . substr($url, 5);
2104 //prepend slash if the URL has no slash in it
2105 // "http://www.example" -> "http://www.example/"
2106 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2110 //convert IDNA hostname to punycode if possible
2111 if (function_exists("idn_to_ascii")) {
2112 $parts = parse_url($url);
2113 if (mb_detect_encoding($parts['host']) != 'ASCII')
2115 $parts['host'] = idn_to_ascii($parts['host']);
2116 $url = build_url($parts);
2120 if ($url != "http:///")
2126 function validate_feed_url($url) {
2127 $parts = parse_url($url);
2129 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2133 /* function save_email_address($email) {
2134 // FIXME: implement persistent storage of emails
2136 if (!$_SESSION['stored_emails'])
2137 $_SESSION['stored_emails'] = array();
2139 if (!in_array($email, $_SESSION['stored_emails']))
2140 array_push($_SESSION['stored_emails'], $email);
2144 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2146 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2148 $is_cat = bool_to_sql_bool($is_cat);
2152 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2153 WHERE feed_id = ? AND is_cat = ?
2154 AND owner_uid = ?");
2155 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2157 if ($row = $sth->fetch()) {
2158 return $row["access_key"];
2160 $key = uniqid_short();
2162 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2163 (access_key, feed_id, is_cat, owner_uid)
2164 VALUES (?, ?, ?, ?)");
2166 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2172 function get_feeds_from_html($url, $content)
2174 $url = fix_url($url);
2175 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2177 libxml_use_internal_errors(true);
2179 $doc = new DOMDocument();
2180 $doc->loadHTML($content);
2181 $xpath = new DOMXPath($doc);
2182 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2183 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2184 $feedUrls = array();
2185 foreach ($entries as $entry) {
2186 if ($entry->hasAttribute('href')) {
2187 $title = $entry->getAttribute('title');
2189 $title = $entry->getAttribute('type');
2191 $feedUrl = rewrite_relative_url(
2192 $baseUrl, $entry->getAttribute('href')
2194 $feedUrls[$feedUrl] = $title;
2200 function is_html($content) {
2201 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2204 function url_is_html($url, $login = false, $pass = false) {
2205 return is_html(fetch_file_contents($url, false, $login, $pass));
2208 function build_url($parts) {
2209 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2212 function cleanup_url_path($path) {
2213 $path = str_replace("/./", "/", $path);
2214 $path = str_replace("//", "/", $path);
2220 * Converts a (possibly) relative URL to a absolute one.
2222 * @param string $url Base URL (i.e. from where the document is)
2223 * @param string $rel_url Possibly relative URL in the document
2225 * @return string Absolute URL
2227 function rewrite_relative_url($url, $rel_url) {
2228 if (strpos($rel_url, "://") !== false) {
2230 } else if (strpos($rel_url, "//") === 0) {
2231 # protocol-relative URL (rare but they exist)
2233 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2234 # magnet:, feed:, etc
2236 } else if (strpos($rel_url, "/") === 0) {
2237 $parts = parse_url($url);
2238 $parts['path'] = $rel_url;
2239 $parts['path'] = cleanup_url_path($parts['path']);
2241 return build_url($parts);
2244 $parts = parse_url($url);
2245 if (!isset($parts['path'])) {
2246 $parts['path'] = '/';
2248 $dir = $parts['path'];
2249 if (substr($dir, -1) !== '/') {
2250 $dir = dirname($parts['path']);
2251 $dir !== '/' && $dir .= '/';
2253 $parts['path'] = $dir . $rel_url;
2254 $parts['path'] = cleanup_url_path($parts['path']);
2256 return build_url($parts);
2260 function cleanup_tags($days = 14, $limit = 1000) {
2262 $days = (int) $days;
2264 if (DB_TYPE
== "pgsql") {
2265 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2266 } else if (DB_TYPE
== "mysql") {
2267 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2274 while ($limit > 0) {
2277 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2278 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2279 WHERE post_int_id = int_id AND $interval_query AND
2280 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2281 $sth->execute([$limit]);
2285 while ($line = $sth->fetch()) {
2286 array_push($ids, $line['id']);
2289 if (count($ids) > 0) {
2290 $ids = join(",", $ids);
2292 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2293 $tags_deleted = $usth->rowCount();
2298 $limit -= $limit_part;
2301 return $tags_deleted;
2304 function print_user_stylesheet() {
2305 $value = get_pref('USER_STYLESHEET');
2308 print "<style type=\"text/css\">";
2309 print str_replace("<br/>", "\n", $value);
2315 function filter_to_sql($filter, $owner_uid) {
2320 if (DB_TYPE
== "pgsql")
2323 $reg_qpart = "REGEXP";
2325 foreach ($filter["rules"] AS $rule) {
2326 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2327 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2328 $rule['reg_exp']) !== FALSE;
2330 if ($regexp_valid) {
2332 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2334 switch ($rule["type"]) {
2336 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2337 $rule['reg_exp'] . "')";
2340 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2341 $rule['reg_exp'] . "')";
2344 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2345 $rule['reg_exp'] . "') OR LOWER(" .
2346 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2349 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2350 $rule['reg_exp'] . "')";
2353 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2354 $rule['reg_exp'] . "')";
2357 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2358 $rule['reg_exp'] . "')";
2362 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2364 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2365 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2368 if (isset($rule["cat_id"])) {
2370 if ($rule["cat_id"] > 0) {
2371 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2372 array_push($children, $rule["cat_id"]);
2373 $children = array_map("intval", $children);
2375 $children = join(",", $children);
2377 $cat_qpart = "cat_id IN ($children)";
2379 $cat_qpart = "cat_id IS NULL";
2382 $qpart .= " AND $cat_qpart";
2385 $qpart .= " AND feed_id IS NOT NULL";
2387 array_push($query, "($qpart)");
2392 if (count($query) > 0) {
2393 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2395 $fullquery = "(false)";
2398 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2403 if (!function_exists('gzdecode')) {
2404 function gzdecode($string) { // no support for 2nd argument
2405 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2406 base64_encode($string));
2410 function get_random_bytes($length) {
2411 if (function_exists('openssl_random_pseudo_bytes')) {
2412 return openssl_random_pseudo_bytes($length);
2416 for ($i = 0; $i < $length; $i++
)
2417 $output .= chr(mt_rand(0, 255));
2423 function read_stdin() {
2424 $fp = fopen("php://stdin", "r");
2427 $line = trim(fgets($fp));
2435 function implements_interface($class, $interface) {
2436 return in_array($interface, class_implements($class));
2439 function get_minified_js($files) {
2443 foreach ($files as $js) {
2444 if (!isset($_GET['debug'])) {
2445 $cached_file = CACHE_DIR
. "/js/".basename($js);
2447 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2449 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2451 if ($header && $contents) {
2452 list($htag, $hversion) = explode(":", $header);
2454 if ($htag == "tt-rss" && $hversion == VERSION
) {
2461 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2462 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2466 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2473 function calculate_dep_timestamp() {
2474 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2478 foreach ($files as $file) {
2479 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2485 function T_js_decl($s1, $s2) {
2487 $s1 = preg_replace("/\n/", "", $s1);
2488 $s2 = preg_replace("/\n/", "", $s2);
2490 $s1 = preg_replace("/\"/", "\\\"", $s1);
2491 $s2 = preg_replace("/\"/", "\\\"", $s2);
2493 return "T_messages[\"$s1\"] = \"$s2\";\n";
2497 function init_js_translations() {
2499 print 'var T_messages = new Object();
2502 if (T_messages[msg]) {
2503 return T_messages[msg];
2509 function ngettext(msg1, msg2, n) {
2510 return __((parseInt(n) > 1) ? msg2 : msg1);
2513 $l10n = _get_reader();
2515 for ($i = 0; $i < $l10n->total
; $i++
) {
2516 $orig = $l10n->get_original_string($i);
2517 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2518 $key = explode(chr(0), $orig);
2519 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2520 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2522 $translation = __($orig);
2523 print T_js_decl($orig, $translation);
2528 function get_theme_path($theme) {
2529 if ($theme == "default.php")
2530 return "css/default.css";
2532 $check = "themes/$theme";
2533 if (file_exists($check)) return $check;
2535 $check = "themes.local/$theme";
2536 if (file_exists($check)) return $check;
2539 function theme_valid($theme) {
2540 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2542 if (in_array($theme, $bundled_themes)) return true;
2544 $file = "themes/" . basename($theme);
2546 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2548 if (file_exists($file) && is_readable($file)) {
2549 $fh = fopen($file, "r");
2552 $header = fgets($fh);
2555 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2563 * @SuppressWarnings(unused)
2565 function error_json($code) {
2566 require_once "errors.php";
2568 @$message = $ERRORS[$code];
2570 return json_encode(array("error" =>
2571 array("code" => $code, "message" => $message)));
2575 /*function abs_to_rel_path($dir) {
2576 $tmp = str_replace(dirname(__DIR__), "", $dir);
2578 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2583 function get_upload_error_message($code) {
2586 0 => __('There is no error, the file uploaded with success'),
2587 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2588 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2589 3 => __('The uploaded file was only partially uploaded'),
2590 4 => __('No file was uploaded'),
2591 6 => __('Missing a temporary folder'),
2592 7 => __('Failed to write file to disk.'),
2593 8 => __('A PHP extension stopped the file upload.'),
2596 return $errors[$code];
2599 function base64_img($filename) {
2600 if (file_exists($filename)) {
2601 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2603 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2609 /* this is essentially a wrapper for readfile() which allows plugins to hook
2610 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2612 hook function should return true if request was handled (or at least attempted to)
2614 note that this can be called without user context so the plugin to handle this
2615 should be loaded systemwide in config.php */
2616 function send_local_file($filename) {
2617 if (file_exists($filename)) {
2619 if (is_writable($filename)) touch($filename);
2621 $tmppluginhost = new PluginHost();
2623 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2624 $tmppluginhost->load_data();
2626 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2627 if ($plugin->hook_send_local_file($filename)) return true;
2630 $mimetype = mime_content_type($filename);
2632 // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2633 // video files are detected as octet-stream by mime_content_type()
2635 if ($mimetype == "application/octet-stream")
2636 $mimetype = "video/mp4";
2638 header("Content-type: $mimetype");
2640 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2641 header("Last-Modified: $stamp", true);
2643 return readfile($filename);
2649 function check_mysql_tables() {
2652 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2653 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2654 $sth->execute([DB_NAME
]);
2658 while ($line = $sth->fetch()) {
2659 array_push($bad_tables, $line);
2665 function validate_field($string, $allowed, $default = "") {
2666 if (in_array($string, $allowed))
2672 function arr_qmarks($arr) {
2673 return str_repeat('?,', count($arr) - 1) . '?';