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) {
717 /* if a session is started here there's a stale login cookie we need to clean */
719 if (session_status() != PHP_SESSION_NONE
) {
720 $_SESSION["login_error_msg"] = __("Stale session cookie found, try logging in again");
724 session_regenerate_id(true);
727 $_SESSION["uid"] = $user_id;
728 $_SESSION["version"] = VERSION_STATIC
;
729 $_SESSION["auth_module"] = $auth_module;
732 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
734 $sth->execute([$user_id]);
735 $row = $sth->fetch();
737 $_SESSION["name"] = $row["login"];
738 $_SESSION["access_level"] = $row["access_level"];
739 $_SESSION["csrf_token"] = uniqid_short();
741 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
742 $usth->execute([$user_id]);
744 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
745 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
746 $_SESSION["pwd_hash"] = $row["pwd_hash"];
748 $_SESSION["last_version_check"] = time();
750 initialize_user_prefs($_SESSION["uid"]);
759 $_SESSION["uid"] = 1;
760 $_SESSION["name"] = "admin";
761 $_SESSION["access_level"] = 10;
763 $_SESSION["hide_hello"] = true;
764 $_SESSION["hide_logout"] = true;
766 $_SESSION["auth_module"] = false;
768 if (!$_SESSION["csrf_token"]) {
769 $_SESSION["csrf_token"] = uniqid_short();
772 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
774 initialize_user_prefs($_SESSION["uid"]);
780 // this is used for user http parameters unless HTML code is actually needed
781 function clean($param) {
782 if (is_array($param)) {
783 return array_map("strip_tags", $param);
784 } else if (is_string($param)) {
785 return strip_tags($param);
791 function make_password($length = 8) {
794 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
798 while ($i < $length) {
799 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
801 if (!strstr($password, $char)) {
809 // this is called after user is created to initialize default feeds, labels
812 // user preferences are checked on every login, not here
814 function initialize_user($uid) {
818 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
819 values (?, 'Tiny Tiny RSS: Forum',
820 'http://tt-rss.org/forum/rss.php')");
821 $sth->execute([$uid]);
824 function logout_user() {
826 if (isset($_COOKIE[session_name()])) {
827 setcookie(session_name(), '', time()-42000, '/');
832 function validate_csrf($csrf_token) {
833 return $csrf_token == $_SESSION['csrf_token'];
836 function load_user_plugins($owner_uid, $pluginhost = false) {
838 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
840 if ($owner_uid && SCHEMA_VERSION
>= 100) {
841 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
843 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
845 if (get_schema_version() > 100) {
846 $pluginhost->load_data();
851 function login_sequence() {
854 if (SINGLE_USER_MODE
) {
856 authenticate_user("admin", null);
858 load_user_plugins($_SESSION["uid"]);
860 if (!validate_session()) $_SESSION["uid"] = false;
862 if (!$_SESSION["uid"]) {
864 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
865 $_SESSION["ref_schema_version"] = get_schema_version(true);
867 authenticate_user(null, null, true);
870 if (!$_SESSION["uid"]) {
878 /* bump login timestamp */
879 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
880 $sth->execute([$_SESSION['uid']]);
882 $_SESSION["last_login_update"] = time();
885 if ($_SESSION["uid"]) {
887 load_user_plugins($_SESSION["uid"]);
891 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
893 (SELECT COUNT(id) FROM ttrss_feeds WHERE
894 ttrss_feeds.id = feed_id) = 0");
896 $sth->execute([$_SESSION['uid']]);
898 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
900 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
901 ttrss_feed_categories.id = feed_id) = 0");
903 $sth->execute([$_SESSION['uid']]);
909 function truncate_string($str, $max_len, $suffix = '…') {
910 if (mb_strlen($str, "utf-8") > $max_len) {
911 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
918 function truncate_middle($str, $max_len, $suffix = '…') {
919 if (strlen($str) > $max_len) {
920 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
926 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
929 $source_tz = new DateTimeZone($source_tz);
930 } catch (Exception
$e) {
931 $source_tz = new DateTimeZone('UTC');
935 $dest_tz = new DateTimeZone($dest_tz);
936 } catch (Exception
$e) {
937 $dest_tz = new DateTimeZone('UTC');
940 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
941 return $dt->format('U') +
$dest_tz->getOffset($dt);
944 function make_local_datetime($timestamp, $long, $owner_uid = false,
945 $no_smart_dt = false, $eta_min = false) {
947 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
948 if (!$timestamp) $timestamp = '1970-01-01 0:00';
953 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
955 $timestamp = substr($timestamp, 0, 19);
957 # We store date in UTC internally
958 $dt = new DateTime($timestamp, $utc_tz);
960 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
962 if ($user_tz_string != 'Automatic') {
965 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
966 } catch (Exception
$e) {
970 $tz_offset = $user_tz->getOffset($dt);
972 $tz_offset = (int) -$_SESSION["clientTzOffset"];
975 $user_timestamp = $dt->format('U') +
$tz_offset;
978 return smart_date_time($user_timestamp,
979 $tz_offset, $owner_uid, $eta_min);
982 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
984 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
986 return date($format, $user_timestamp);
990 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
991 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
993 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
994 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
995 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
996 return date("G:i", $timestamp);
997 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
998 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
999 return date($format, $timestamp);
1001 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
1002 return date($format, $timestamp);
1006 function sql_bool_to_bool($s) {
1007 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
1010 function bool_to_sql_bool($s) {
1014 // Session caching removed due to causing wrong redirects to upgrade
1015 // script when get_schema_version() is called on an obsolete session
1016 // created on a previous schema version.
1017 function get_schema_version($nocache = false) {
1018 global $schema_version;
1022 if (!$schema_version && !$nocache) {
1023 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
1024 $version = $row["schema_version"];
1025 $schema_version = $version;
1028 return $schema_version;
1032 function sanity_check() {
1033 require_once 'errors.php';
1037 $schema_version = get_schema_version(true);
1039 if ($schema_version != SCHEMA_VERSION
) {
1043 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1046 function file_is_locked($filename) {
1047 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
1048 if (function_exists('flock')) {
1049 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1051 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1052 flock($fp, LOCK_UN
);
1062 return true; // consider the file always locked and skip the test
1069 function make_lockfile($filename) {
1070 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1072 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1073 $stat_h = fstat($fp);
1074 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1076 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1077 if ($stat_h["ino"] != $stat_f["ino"] ||
1078 $stat_h["dev"] != $stat_f["dev"]) {
1084 if (function_exists('posix_getpid')) {
1085 fwrite($fp, posix_getpid() . "\n");
1093 function make_stampfile($filename) {
1094 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1096 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1097 fwrite($fp, time() . "\n");
1098 flock($fp, LOCK_UN
);
1106 function sql_random_function() {
1107 if (DB_TYPE
== "mysql") {
1114 function getFeedUnread($feed, $is_cat = false) {
1115 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1118 function checkbox_to_sql_bool($val) {
1119 return ($val == "on") ?
1 : 0;
1122 function uniqid_short() {
1123 return uniqid(base_convert(rand(), 10, 36));
1126 function make_init_params() {
1129 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1130 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1131 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1132 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1134 $params[strtolower($param)] = (int) get_pref($param);
1137 $params["icons_url"] = ICONS_URL
;
1138 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1139 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1140 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1141 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1142 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1143 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1144 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1146 $theme = get_pref( "USER_CSS_THEME", false, false);
1147 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1149 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1151 $params["php_platform"] = PHP_OS
;
1152 $params["php_version"] = PHP_VERSION
;
1154 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1158 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1159 ttrss_feeds WHERE owner_uid = ?");
1160 $sth->execute([$_SESSION['uid']]);
1161 $row = $sth->fetch();
1163 $max_feed_id = $row["mid"];
1164 $num_feeds = $row["nf"];
1166 $params["max_feed_id"] = (int) $max_feed_id;
1167 $params["num_feeds"] = (int) $num_feeds;
1169 $params["hotkeys"] = get_hotkeys_map();
1171 $params["csrf_token"] = $_SESSION["csrf_token"];
1172 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1174 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1176 $params["icon_alert"] = base64_img("images/alert.png");
1177 $params["icon_information"] = base64_img("images/information.png");
1178 $params["icon_cross"] = base64_img("images/cross.png");
1179 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1181 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1186 function get_hotkeys_info() {
1188 __("Navigation") => array(
1189 "next_feed" => __("Open next feed"),
1190 "prev_feed" => __("Open previous feed"),
1191 "next_article" => __("Open next article"),
1192 "prev_article" => __("Open previous article"),
1193 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1194 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1195 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1196 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1197 "search_dialog" => __("Show search dialog")),
1198 __("Article") => array(
1199 "toggle_mark" => __("Toggle starred"),
1200 "toggle_publ" => __("Toggle published"),
1201 "toggle_unread" => __("Toggle unread"),
1202 "edit_tags" => __("Edit tags"),
1203 "open_in_new_window" => __("Open in new window"),
1204 "catchup_below" => __("Mark below as read"),
1205 "catchup_above" => __("Mark above as read"),
1206 "article_scroll_down" => __("Scroll down"),
1207 "article_scroll_up" => __("Scroll up"),
1208 "select_article_cursor" => __("Select article under cursor"),
1209 "email_article" => __("Email article"),
1210 "close_article" => __("Close/collapse article"),
1211 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1212 "toggle_widescreen" => __("Toggle widescreen mode"),
1213 "toggle_embed_original" => __("Toggle embed original")),
1214 __("Article selection") => array(
1215 "select_all" => __("Select all articles"),
1216 "select_unread" => __("Select unread"),
1217 "select_marked" => __("Select starred"),
1218 "select_published" => __("Select published"),
1219 "select_invert" => __("Invert selection"),
1220 "select_none" => __("Deselect everything")),
1221 __("Feed") => array(
1222 "feed_refresh" => __("Refresh current feed"),
1223 "feed_unhide_read" => __("Un/hide read feeds"),
1224 "feed_subscribe" => __("Subscribe to feed"),
1225 "feed_edit" => __("Edit feed"),
1226 "feed_catchup" => __("Mark as read"),
1227 "feed_reverse" => __("Reverse headlines"),
1228 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1229 "feed_debug_update" => __("Debug feed update"),
1230 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1231 "catchup_all" => __("Mark all feeds as read"),
1232 "cat_toggle_collapse" => __("Un/collapse current category"),
1233 "toggle_combined_mode" => __("Toggle combined mode"),
1234 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1235 __("Go to") => array(
1236 "goto_all" => __("All articles"),
1237 "goto_fresh" => __("Fresh"),
1238 "goto_marked" => __("Starred"),
1239 "goto_published" => __("Published"),
1240 "goto_tagcloud" => __("Tag cloud"),
1241 "goto_prefs" => __("Preferences")),
1242 __("Other") => array(
1243 "create_label" => __("Create label"),
1244 "create_filter" => __("Create filter"),
1245 "collapse_sidebar" => __("Un/collapse sidebar"),
1246 "help_dialog" => __("Show help dialog"))
1249 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1250 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1256 function get_hotkeys_map() {
1258 // "navigation" => array(
1261 "n" => "next_article",
1262 "p" => "prev_article",
1263 "(38)|up" => "prev_article",
1264 "(40)|down" => "next_article",
1265 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1266 // "^(40)|Ctrl-down" => "next_article_noscroll",
1267 "(191)|/" => "search_dialog",
1268 // "article" => array(
1269 "s" => "toggle_mark",
1270 "*s" => "toggle_publ",
1271 "u" => "toggle_unread",
1272 "*t" => "edit_tags",
1273 "o" => "open_in_new_window",
1274 "c p" => "catchup_below",
1275 "c n" => "catchup_above",
1276 "*n" => "article_scroll_down",
1277 "*p" => "article_scroll_up",
1278 "*(38)|Shift+up" => "article_scroll_up",
1279 "*(40)|Shift+down" => "article_scroll_down",
1280 "a *w" => "toggle_widescreen",
1281 "a e" => "toggle_embed_original",
1282 "e" => "email_article",
1283 "a q" => "close_article",
1284 // "article_selection" => array(
1285 "a a" => "select_all",
1286 "a u" => "select_unread",
1287 "a *u" => "select_marked",
1288 "a p" => "select_published",
1289 "a i" => "select_invert",
1290 "a n" => "select_none",
1292 "f r" => "feed_refresh",
1293 "f a" => "feed_unhide_read",
1294 "f s" => "feed_subscribe",
1295 "f e" => "feed_edit",
1296 "f q" => "feed_catchup",
1297 "f x" => "feed_reverse",
1298 "f g" => "feed_toggle_vgroup",
1299 "f *d" => "feed_debug_update",
1300 "f *g" => "feed_debug_viewfeed",
1301 "f *c" => "toggle_combined_mode",
1302 "f c" => "toggle_cdm_expanded",
1303 "*q" => "catchup_all",
1304 "x" => "cat_toggle_collapse",
1306 "g a" => "goto_all",
1307 "g f" => "goto_fresh",
1308 "g s" => "goto_marked",
1309 "g p" => "goto_published",
1310 "g t" => "goto_tagcloud",
1311 "g *p" => "goto_prefs",
1312 // "other" => array(
1313 "(9)|Tab" => "select_article_cursor", // tab
1314 "c l" => "create_label",
1315 "c f" => "create_filter",
1316 "c s" => "collapse_sidebar",
1317 "^(191)|Ctrl+/" => "help_dialog",
1320 if (get_pref('COMBINED_DISPLAY_MODE')) {
1321 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1322 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1325 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1326 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1329 $prefixes = array();
1331 foreach (array_keys($hotkeys) as $hotkey) {
1332 $pair = explode(" ", $hotkey, 2);
1334 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1335 array_push($prefixes, $pair[0]);
1339 return array($prefixes, $hotkeys);
1342 function check_for_update() {
1343 if (defined("GIT_VERSION_TIMESTAMP")) {
1344 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1347 $content = json_decode($content, true);
1349 if ($content && isset($content["changeset"])) {
1350 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1351 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1353 return $content["changeset"]["id"];
1362 function make_runtime_info($disable_update_check = false) {
1367 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1368 ttrss_feeds WHERE owner_uid = ?");
1369 $sth->execute([$_SESSION['uid']]);
1370 $row = $sth->fetch();
1372 $max_feed_id = $row['mid'];
1373 $num_feeds = $row['nf'];
1375 $data["max_feed_id"] = (int) $max_feed_id;
1376 $data["num_feeds"] = (int) $num_feeds;
1378 $data['last_article_id'] = Article
::getLastArticleId();
1379 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1381 $data['dep_ts'] = calculate_dep_timestamp();
1382 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1384 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1386 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1387 $update_result = @check_for_update
();
1389 $data["update_result"] = $update_result;
1391 $_SESSION["last_version_check"] = time();
1394 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1396 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1398 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1400 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1403 $stamp_delta = time() - $stamp;
1405 if ($stamp_delta > 1800) {
1409 $_SESSION["daemon_stamp_check"] = time();
1412 $data['daemon_stamp_ok'] = $stamp_check;
1414 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1416 $data['daemon_stamp'] = $stamp_fmt;
1424 function search_to_sql($search, $search_language) {
1426 $keywords = str_getcsv(trim($search), " ");
1427 $query_keywords = array();
1428 $search_words = array();
1429 $search_query_leftover = array();
1433 if ($search_language)
1434 $search_language = $pdo->quote(mb_strtolower($search_language));
1436 $search_language = $pdo->quote("english");
1438 foreach ($keywords as $k) {
1439 if (strpos($k, "-") === 0) {
1446 $commandpair = explode(":", mb_strtolower($k), 2);
1448 switch ($commandpair[0]) {
1450 if ($commandpair[1]) {
1451 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1452 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1454 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1455 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1456 array_push($search_words, $k);
1460 if ($commandpair[1]) {
1461 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1462 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1464 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1465 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1466 array_push($search_words, $k);
1470 if ($commandpair[1]) {
1471 if ($commandpair[1] == "true")
1472 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1473 else if ($commandpair[1] == "false")
1474 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1476 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1477 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1479 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1480 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1481 if (!$not) array_push($search_words, $k);
1486 if ($commandpair[1]) {
1487 if ($commandpair[1] == "true")
1488 array_push($query_keywords, "($not (marked = true))");
1490 array_push($query_keywords, "($not (marked = false))");
1492 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1493 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1494 if (!$not) array_push($search_words, $k);
1498 if ($commandpair[1]) {
1499 if ($commandpair[1] == "true")
1500 array_push($query_keywords, "($not (published = true))");
1502 array_push($query_keywords, "($not (published = false))");
1505 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1506 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1507 if (!$not) array_push($search_words, $k);
1511 if ($commandpair[1]) {
1512 if ($commandpair[1] == "true")
1513 array_push($query_keywords, "($not (unread = true))");
1515 array_push($query_keywords, "($not (unread = false))");
1518 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1519 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1520 if (!$not) array_push($search_words, $k);
1524 if (strpos($k, "@") === 0) {
1526 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1527 $orig_ts = strtotime(substr($k, 1));
1528 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1530 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1532 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1535 if (DB_TYPE
== "pgsql") {
1536 $k = mb_strtolower($k);
1537 array_push($search_query_leftover, $not ?
"!$k" : $k);
1539 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1540 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1543 if (!$not) array_push($search_words, $k);
1548 if (count($search_query_leftover) > 0) {
1549 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1551 if (DB_TYPE
== "pgsql") {
1552 array_push($query_keywords,
1553 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1558 $search_query_part = implode("AND", $query_keywords);
1560 return array($search_query_part, $search_words);
1563 function iframe_whitelisted($entry) {
1564 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1566 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1569 foreach ($whitelist as $w) {
1570 if ($src == $w ||
$src == "www.$w")
1578 // check for locally cached (media) URLs and rewrite to local versions
1579 // this is called separately after sanitize() and plugin render article hooks to allow
1580 // plugins work on original source URLs used before caching
1582 function rewrite_cached_urls($str) {
1583 $charset_hack = '<head>
1584 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1587 $res = trim($str); if (!$res) return '';
1589 $doc = new DOMDocument();
1590 $doc->loadHTML($charset_hack . $res);
1591 $xpath = new DOMXPath($doc);
1593 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
1595 $need_saving = false;
1597 foreach ($entries as $entry) {
1599 if ($entry->hasAttribute('src') ||
$entry->hasAttribute('poster')) {
1601 // should be already absolutized because this is called after sanitize()
1602 $src = $entry->hasAttribute('poster') ?
$entry->getAttribute('poster') : $entry->getAttribute('src');
1603 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1605 if (file_exists($cached_filename)) {
1607 // this is strictly cosmetic
1608 if ($entry->tagName
== 'img') {
1610 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1612 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1618 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1620 if ($entry->hasAttribute('poster'))
1621 $entry->setAttribute('poster', $src);
1623 $entry->setAttribute('src', $src);
1625 $need_saving = true;
1631 $doc->removeChild($doc->firstChild
); //remove doctype
1632 $res = $doc->saveHTML();
1638 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1639 if (!$owner) $owner = $_SESSION["uid"];
1641 $res = trim($str); if (!$res) return '';
1643 $charset_hack = '<head>
1644 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1647 $res = trim($res); if (!$res) return '';
1649 libxml_use_internal_errors(true);
1651 $doc = new DOMDocument();
1652 $doc->loadHTML($charset_hack . $res);
1653 $xpath = new DOMXPath($doc);
1655 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1657 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1659 foreach ($entries as $entry) {
1661 if ($entry->hasAttribute('href')) {
1662 $entry->setAttribute('href',
1663 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1665 $entry->setAttribute('rel', 'noopener noreferrer');
1668 if ($entry->hasAttribute('src')) {
1669 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1671 // cache stuff has gone to rewrite_cached_urls()
1673 $entry->setAttribute('src', $src);
1676 if ($entry->nodeName
== 'img') {
1677 $entry->setAttribute('referrerpolicy', 'no-referrer');
1679 $entry->removeAttribute('width');
1680 $entry->removeAttribute('height');
1682 if ($entry->hasAttribute('src')) {
1683 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1685 if (is_prefix_https() && !$is_https_url) {
1687 if ($entry->hasAttribute('srcset')) {
1688 $entry->removeAttribute('srcset');
1691 if ($entry->hasAttribute('sizes')) {
1692 $entry->removeAttribute('sizes');
1698 if ($entry->hasAttribute('src') &&
1699 ($owner && get_pref("STRIP_IMAGES", $owner)) ||
$force_remove_images ||
$_SESSION["bw_limit"]) {
1701 $p = $doc->createElement('p');
1703 $a = $doc->createElement('a');
1704 $a->setAttribute('href', $entry->getAttribute('src'));
1706 $a->appendChild(new DOMText($entry->getAttribute('src')));
1707 $a->setAttribute('target', '_blank');
1708 $a->setAttribute('rel', 'noopener noreferrer');
1710 $p->appendChild($a);
1712 if ($entry->nodeName
== 'source') {
1714 if ($entry->parentNode
&& $entry->parentNode
->parentNode
)
1715 $entry->parentNode
->parentNode
->replaceChild($p, $entry->parentNode
);
1717 } else if ($entry->nodeName
== 'img') {
1719 if ($entry->parentNode
)
1720 $entry->parentNode
->replaceChild($p, $entry);
1725 if (strtolower($entry->nodeName
) == "a") {
1726 $entry->setAttribute("target", "_blank");
1727 $entry->setAttribute("rel", "noopener noreferrer");
1731 $entries = $xpath->query('//iframe');
1732 foreach ($entries as $entry) {
1733 if (!iframe_whitelisted($entry)) {
1734 $entry->setAttribute('sandbox', 'allow-scripts');
1736 if (is_prefix_https()) {
1737 $entry->setAttribute("src",
1738 str_replace("http://", "https://",
1739 $entry->getAttribute("src")));
1744 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1745 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1746 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1747 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1748 'dt', 'em', 'footer', 'figure', 'figcaption',
1749 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1750 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1751 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1752 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1753 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1754 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1756 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1758 $disallowed_attributes = array('id', 'style', 'class');
1760 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1761 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1762 if (is_array($retval)) {
1764 $allowed_elements = $retval[1];
1765 $disallowed_attributes = $retval[2];
1771 $doc->removeChild($doc->firstChild
); //remove doctype
1772 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1774 if ($highlight_words) {
1775 foreach ($highlight_words as $word) {
1777 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1779 $elements = $xpath->query("//*/text()");
1781 foreach ($elements as $child) {
1783 $fragment = $doc->createDocumentFragment();
1784 $text = $child->textContent
;
1786 while (($pos = mb_stripos($text, $word)) !== false) {
1787 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1788 $word = mb_substr($text, $pos, mb_strlen($word));
1789 $highlight = $doc->createElement('span');
1790 $highlight->appendChild(new DomText($word));
1791 $highlight->setAttribute('class', 'highlight');
1792 $fragment->appendChild($highlight);
1793 $text = mb_substr($text, $pos +
mb_strlen($word));
1796 if (!empty($text)) $fragment->appendChild(new DomText($text));
1798 $child->parentNode
->replaceChild($fragment, $child);
1803 $res = $doc->saveHTML();
1805 /* strip everything outside of <body>...</body> */
1807 $res_frag = array();
1808 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1809 return $res_frag[1];
1815 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1816 $xpath = new DOMXPath($doc);
1817 $entries = $xpath->query('//*');
1819 foreach ($entries as $entry) {
1820 if (!in_array($entry->nodeName
, $allowed_elements)) {
1821 $entry->parentNode
->removeChild($entry);
1824 if ($entry->hasAttributes()) {
1825 $attrs_to_remove = array();
1827 foreach ($entry->attributes
as $attr) {
1829 if (strpos($attr->nodeName
, 'on') === 0) {
1830 array_push($attrs_to_remove, $attr);
1833 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1834 array_push($attrs_to_remove, $attr);
1837 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1838 array_push($attrs_to_remove, $attr);
1842 foreach ($attrs_to_remove as $attr) {
1843 $entry->removeAttributeNode($attr);
1851 function trim_array($array) {
1853 array_walk($tmp, 'trim');
1857 function tag_is_valid($tag) {
1858 if (!$tag ||
is_numeric($tag) ||
mb_strlen($tag) > 250)
1864 function render_login_form() {
1865 header('Cache-Control: public');
1867 require_once "login_form.php";
1871 function T_sprintf() {
1872 $args = func_get_args();
1873 return vsprintf(__(array_shift($args)), $args);
1876 function print_checkpoint($n, $s) {
1877 $ts = microtime(true);
1878 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1882 function sanitize_tag($tag) {
1885 $tag = mb_strtolower($tag, 'utf-8');
1887 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1889 if (DB_TYPE
== "mysql") {
1890 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1896 function is_server_https() {
1897 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1900 function is_prefix_https() {
1901 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1904 // this returns SELF_URL_PATH sans ending slash
1905 function get_self_url_prefix() {
1906 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1907 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1909 return SELF_URL_PATH
;
1913 function encrypt_password($pass, $salt = '', $mode2 = false) {
1914 if ($salt && $mode2) {
1915 return "MODE2:" . hash('sha256', $salt . $pass);
1917 return "SHA1X:" . sha1("$salt:$pass");
1919 return "SHA1:" . sha1($pass);
1921 } // function encrypt_password
1923 function load_filters($feed_id, $owner_uid) {
1926 $feed_id = (int) $feed_id;
1927 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1930 $null_cat_qpart = "cat_id IS NULL OR";
1932 $null_cat_qpart = "";
1936 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1937 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1938 $sth->execute([$owner_uid]);
1940 $check_cats = array_merge(
1941 Feeds
::getParentCategories($cat_id, $owner_uid),
1944 $check_cats_str = join(",", $check_cats);
1945 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1947 while ($line = $sth->fetch()) {
1948 $filter_id = $line["id"];
1950 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1952 $sth2 = $pdo->prepare("SELECT
1953 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1954 FROM ttrss_filters2_rules AS r,
1955 ttrss_filter_types AS t
1957 (match_on IS NOT NULL OR
1958 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1959 (feed_id IS NULL OR feed_id = ?))) AND
1960 filter_type = t.id AND filter_id = ?");
1961 $sth2->execute([$feed_id, $filter_id]);
1966 while ($rule_line = $sth2->fetch()) {
1967 # print_r($rule_line);
1969 if ($rule_line["match_on"]) {
1970 $match_on = json_decode($rule_line["match_on"], true);
1972 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1975 $rule["reg_exp"] = $rule_line["reg_exp"];
1976 $rule["type"] = $rule_line["type_name"];
1977 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1979 array_push($rules, $rule);
1980 } else if (!$match_any_rule) {
1981 // this filter contains a rule that doesn't match to this feed/category combination
1982 // thus filter has to be rejected
1991 $rule["reg_exp"] = $rule_line["reg_exp"];
1992 $rule["type"] = $rule_line["type_name"];
1993 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1995 array_push($rules, $rule);
1999 if (count($rules) > 0) {
2000 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
2001 FROM ttrss_filters2_actions AS a,
2002 ttrss_filter_actions AS t
2004 action_id = t.id AND filter_id = ?");
2005 $sth2->execute([$filter_id]);
2007 while ($action_line = $sth2->fetch()) {
2008 # print_r($action_line);
2011 $action["type"] = $action_line["type_name"];
2012 $action["param"] = $action_line["action_param"];
2014 array_push($actions, $action);
2019 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
2020 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
2021 $filter["rules"] = $rules;
2022 $filter["actions"] = $actions;
2024 if (count($rules) > 0 && count($actions) > 0) {
2025 array_push($filters, $filter);
2032 function get_score_pic($score) {
2034 return "score_high.png";
2035 } else if ($score > 0) {
2036 return "score_half_high.png";
2037 } else if ($score < -100) {
2038 return "score_low.png";
2039 } else if ($score < 0) {
2040 return "score_half_low.png";
2042 return "score_neutral.png";
2046 function init_plugins() {
2047 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
2052 function add_feed_category($feed_cat, $parent_cat_id = false) {
2054 if (!$feed_cat) return false;
2056 $feed_cat = mb_substr($feed_cat, 0, 250);
2057 if (!$parent_cat_id) $parent_cat_id = null;
2060 $tr_in_progress = false;
2063 $pdo->beginTransaction();
2064 } catch (Exception
$e) {
2065 $tr_in_progress = true;
2068 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2069 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2070 AND title = :title AND owner_uid = :uid");
2071 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2073 if (!$sth->fetch()) {
2075 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2077 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2079 if (!$tr_in_progress) $pdo->commit();
2090 * Fixes incomplete URLs by prepending "http://".
2091 * Also replaces feed:// with http://, and
2092 * prepends a trailing slash if the url is a domain name only.
2094 * @param string $url Possibly incomplete URL
2096 * @return string Fixed URL.
2098 function fix_url($url) {
2100 // support schema-less urls
2101 if (strpos($url, '//') === 0) {
2102 $url = 'https:' . $url;
2105 if (strpos($url, '://') === false) {
2106 $url = 'http://' . $url;
2107 } else if (substr($url, 0, 5) == 'feed:') {
2108 $url = 'http:' . substr($url, 5);
2111 //prepend slash if the URL has no slash in it
2112 // "http://www.example" -> "http://www.example/"
2113 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2117 //convert IDNA hostname to punycode if possible
2118 if (function_exists("idn_to_ascii")) {
2119 $parts = parse_url($url);
2120 if (mb_detect_encoding($parts['host']) != 'ASCII')
2122 $parts['host'] = idn_to_ascii($parts['host']);
2123 $url = build_url($parts);
2127 if ($url != "http:///")
2133 function validate_feed_url($url) {
2134 $parts = parse_url($url);
2136 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2140 /* function save_email_address($email) {
2141 // FIXME: implement persistent storage of emails
2143 if (!$_SESSION['stored_emails'])
2144 $_SESSION['stored_emails'] = array();
2146 if (!in_array($email, $_SESSION['stored_emails']))
2147 array_push($_SESSION['stored_emails'], $email);
2151 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2153 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2155 $is_cat = bool_to_sql_bool($is_cat);
2159 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2160 WHERE feed_id = ? AND is_cat = ?
2161 AND owner_uid = ?");
2162 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2164 if ($row = $sth->fetch()) {
2165 return $row["access_key"];
2167 $key = uniqid_short();
2169 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2170 (access_key, feed_id, is_cat, owner_uid)
2171 VALUES (?, ?, ?, ?)");
2173 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2179 function get_feeds_from_html($url, $content)
2181 $url = fix_url($url);
2182 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2184 libxml_use_internal_errors(true);
2186 $doc = new DOMDocument();
2187 $doc->loadHTML($content);
2188 $xpath = new DOMXPath($doc);
2189 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2190 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2191 $feedUrls = array();
2192 foreach ($entries as $entry) {
2193 if ($entry->hasAttribute('href')) {
2194 $title = $entry->getAttribute('title');
2196 $title = $entry->getAttribute('type');
2198 $feedUrl = rewrite_relative_url(
2199 $baseUrl, $entry->getAttribute('href')
2201 $feedUrls[$feedUrl] = $title;
2207 function is_html($content) {
2208 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2211 function url_is_html($url, $login = false, $pass = false) {
2212 return is_html(fetch_file_contents($url, false, $login, $pass));
2215 function build_url($parts) {
2216 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2219 function cleanup_url_path($path) {
2220 $path = str_replace("/./", "/", $path);
2221 $path = str_replace("//", "/", $path);
2227 * Converts a (possibly) relative URL to a absolute one.
2229 * @param string $url Base URL (i.e. from where the document is)
2230 * @param string $rel_url Possibly relative URL in the document
2232 * @return string Absolute URL
2234 function rewrite_relative_url($url, $rel_url) {
2235 if (strpos($rel_url, "://") !== false) {
2237 } else if (strpos($rel_url, "//") === 0) {
2238 # protocol-relative URL (rare but they exist)
2240 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2241 # magnet:, feed:, etc
2243 } else if (strpos($rel_url, "/") === 0) {
2244 $parts = parse_url($url);
2245 $parts['path'] = $rel_url;
2246 $parts['path'] = cleanup_url_path($parts['path']);
2248 return build_url($parts);
2251 $parts = parse_url($url);
2252 if (!isset($parts['path'])) {
2253 $parts['path'] = '/';
2255 $dir = $parts['path'];
2256 if (substr($dir, -1) !== '/') {
2257 $dir = dirname($parts['path']);
2258 $dir !== '/' && $dir .= '/';
2260 $parts['path'] = $dir . $rel_url;
2261 $parts['path'] = cleanup_url_path($parts['path']);
2263 return build_url($parts);
2267 function cleanup_tags($days = 14, $limit = 1000) {
2269 $days = (int) $days;
2271 if (DB_TYPE
== "pgsql") {
2272 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2273 } else if (DB_TYPE
== "mysql") {
2274 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2281 while ($limit > 0) {
2284 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2285 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2286 WHERE post_int_id = int_id AND $interval_query AND
2287 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2288 $sth->execute([$limit]);
2292 while ($line = $sth->fetch()) {
2293 array_push($ids, $line['id']);
2296 if (count($ids) > 0) {
2297 $ids = join(",", $ids);
2299 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2300 $tags_deleted = $usth->rowCount();
2305 $limit -= $limit_part;
2308 return $tags_deleted;
2311 function print_user_stylesheet() {
2312 $value = get_pref('USER_STYLESHEET');
2315 print "<style type=\"text/css\">";
2316 print str_replace("<br/>", "\n", $value);
2322 function filter_to_sql($filter, $owner_uid) {
2327 if (DB_TYPE
== "pgsql")
2330 $reg_qpart = "REGEXP";
2332 foreach ($filter["rules"] AS $rule) {
2333 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2334 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2335 $rule['reg_exp']) !== FALSE;
2337 if ($regexp_valid) {
2339 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2341 switch ($rule["type"]) {
2343 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2344 $rule['reg_exp'] . "')";
2347 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2348 $rule['reg_exp'] . "')";
2351 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2352 $rule['reg_exp'] . "') OR LOWER(" .
2353 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2356 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2357 $rule['reg_exp'] . "')";
2360 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2361 $rule['reg_exp'] . "')";
2364 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2365 $rule['reg_exp'] . "')";
2369 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2371 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2372 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2375 if (isset($rule["cat_id"])) {
2377 if ($rule["cat_id"] > 0) {
2378 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2379 array_push($children, $rule["cat_id"]);
2380 $children = array_map("intval", $children);
2382 $children = join(",", $children);
2384 $cat_qpart = "cat_id IN ($children)";
2386 $cat_qpart = "cat_id IS NULL";
2389 $qpart .= " AND $cat_qpart";
2392 $qpart .= " AND feed_id IS NOT NULL";
2394 array_push($query, "($qpart)");
2399 if (count($query) > 0) {
2400 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2402 $fullquery = "(false)";
2405 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2410 if (!function_exists('gzdecode')) {
2411 function gzdecode($string) { // no support for 2nd argument
2412 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2413 base64_encode($string));
2417 function get_random_bytes($length) {
2418 if (function_exists('openssl_random_pseudo_bytes')) {
2419 return openssl_random_pseudo_bytes($length);
2423 for ($i = 0; $i < $length; $i++
)
2424 $output .= chr(mt_rand(0, 255));
2430 function read_stdin() {
2431 $fp = fopen("php://stdin", "r");
2434 $line = trim(fgets($fp));
2442 function implements_interface($class, $interface) {
2443 return in_array($interface, class_implements($class));
2446 function get_minified_js($files) {
2450 foreach ($files as $js) {
2451 if (!isset($_GET['debug'])) {
2452 $cached_file = CACHE_DIR
. "/js/".basename($js);
2454 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2456 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2458 if ($header && $contents) {
2459 list($htag, $hversion) = explode(":", $header);
2461 if ($htag == "tt-rss" && $hversion == VERSION
) {
2468 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2469 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2473 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2480 function calculate_dep_timestamp() {
2481 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2485 foreach ($files as $file) {
2486 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2492 function T_js_decl($s1, $s2) {
2494 $s1 = preg_replace("/\n/", "", $s1);
2495 $s2 = preg_replace("/\n/", "", $s2);
2497 $s1 = preg_replace("/\"/", "\\\"", $s1);
2498 $s2 = preg_replace("/\"/", "\\\"", $s2);
2500 return "T_messages[\"$s1\"] = \"$s2\";\n";
2504 function init_js_translations() {
2506 print 'var T_messages = new Object();
2509 if (T_messages[msg]) {
2510 return T_messages[msg];
2516 function ngettext(msg1, msg2, n) {
2517 return __((parseInt(n) > 1) ? msg2 : msg1);
2520 $l10n = _get_reader();
2522 for ($i = 0; $i < $l10n->total
; $i++
) {
2523 $orig = $l10n->get_original_string($i);
2524 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2525 $key = explode(chr(0), $orig);
2526 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2527 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2529 $translation = __($orig);
2530 print T_js_decl($orig, $translation);
2535 function get_theme_path($theme) {
2536 if ($theme == "default.php")
2537 return "css/default.css";
2539 $check = "themes/$theme";
2540 if (file_exists($check)) return $check;
2542 $check = "themes.local/$theme";
2543 if (file_exists($check)) return $check;
2546 function theme_valid($theme) {
2547 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2549 if (in_array($theme, $bundled_themes)) return true;
2551 $file = "themes/" . basename($theme);
2553 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2555 if (file_exists($file) && is_readable($file)) {
2556 $fh = fopen($file, "r");
2559 $header = fgets($fh);
2562 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2570 * @SuppressWarnings(unused)
2572 function error_json($code) {
2573 require_once "errors.php";
2575 @$message = $ERRORS[$code];
2577 return json_encode(array("error" =>
2578 array("code" => $code, "message" => $message)));
2582 /*function abs_to_rel_path($dir) {
2583 $tmp = str_replace(dirname(__DIR__), "", $dir);
2585 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2590 function get_upload_error_message($code) {
2593 0 => __('There is no error, the file uploaded with success'),
2594 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2595 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2596 3 => __('The uploaded file was only partially uploaded'),
2597 4 => __('No file was uploaded'),
2598 6 => __('Missing a temporary folder'),
2599 7 => __('Failed to write file to disk.'),
2600 8 => __('A PHP extension stopped the file upload.'),
2603 return $errors[$code];
2606 function base64_img($filename) {
2607 if (file_exists($filename)) {
2608 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2610 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2616 /* this is essentially a wrapper for readfile() which allows plugins to hook
2617 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2619 hook function should return true if request was handled (or at least attempted to)
2621 note that this can be called without user context so the plugin to handle this
2622 should be loaded systemwide in config.php */
2623 function send_local_file($filename) {
2624 if (file_exists($filename)) {
2626 if (is_writable($filename)) touch($filename);
2628 $tmppluginhost = new PluginHost();
2630 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2631 $tmppluginhost->load_data();
2633 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2634 if ($plugin->hook_send_local_file($filename)) return true;
2637 $mimetype = mime_content_type($filename);
2639 // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2640 // video files are detected as octet-stream by mime_content_type()
2642 if ($mimetype == "application/octet-stream")
2643 $mimetype = "video/mp4";
2645 header("Content-type: $mimetype");
2647 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2648 header("Last-Modified: $stamp", true);
2650 return readfile($filename);
2656 function check_mysql_tables() {
2659 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2660 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2661 $sth->execute([DB_NAME
]);
2665 while ($line = $sth->fetch()) {
2666 array_push($bad_tables, $line);
2672 function validate_field($string, $allowed, $default = "") {
2673 if (in_array($string, $allowed))
2679 function arr_qmarks($arr) {
2680 return str_repeat('?,', count($arr) - 1) . '?';