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 (session_status() != PHP_SESSION_NONE
) {
722 session_regenerate_id(true);
725 $_SESSION["uid"] = $user_id;
726 $_SESSION["version"] = VERSION_STATIC
;
727 $_SESSION["auth_module"] = $auth_module;
730 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
732 $sth->execute([$user_id]);
733 $row = $sth->fetch();
735 $_SESSION["name"] = $row["login"];
736 $_SESSION["access_level"] = $row["access_level"];
737 $_SESSION["csrf_token"] = uniqid_short();
739 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
740 $usth->execute([$user_id]);
742 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
743 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
744 $_SESSION["pwd_hash"] = $row["pwd_hash"];
746 $_SESSION["last_version_check"] = time();
748 initialize_user_prefs($_SESSION["uid"]);
757 $_SESSION["uid"] = 1;
758 $_SESSION["name"] = "admin";
759 $_SESSION["access_level"] = 10;
761 $_SESSION["hide_hello"] = true;
762 $_SESSION["hide_logout"] = true;
764 $_SESSION["auth_module"] = false;
766 if (!$_SESSION["csrf_token"]) {
767 $_SESSION["csrf_token"] = uniqid_short();
770 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
772 initialize_user_prefs($_SESSION["uid"]);
778 // this is used for user http parameters unless HTML code is actually needed
779 function clean($param) {
780 if (is_array($param)) {
781 return array_map("strip_tags", $param);
782 } else if (is_string($param)) {
783 return strip_tags($param);
789 function make_password($length = 8) {
792 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
796 while ($i < $length) {
797 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
799 if (!strstr($password, $char)) {
807 // this is called after user is created to initialize default feeds, labels
810 // user preferences are checked on every login, not here
812 function initialize_user($uid) {
816 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
817 values (?, 'Tiny Tiny RSS: Forum',
818 'http://tt-rss.org/forum/rss.php')");
819 $sth->execute([$uid]);
822 function logout_user() {
824 if (isset($_COOKIE[session_name()])) {
825 setcookie(session_name(), '', time()-42000, '/');
830 function validate_csrf($csrf_token) {
831 return $csrf_token == $_SESSION['csrf_token'];
834 function load_user_plugins($owner_uid, $pluginhost = false) {
836 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
838 if ($owner_uid && SCHEMA_VERSION
>= 100) {
839 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
841 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
843 if (get_schema_version() > 100) {
844 $pluginhost->load_data();
849 function login_sequence() {
852 if (SINGLE_USER_MODE
) {
854 authenticate_user("admin", null);
856 load_user_plugins($_SESSION["uid"]);
858 if (!validate_session()) $_SESSION["uid"] = false;
860 if (!$_SESSION["uid"]) {
862 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
863 $_SESSION["ref_schema_version"] = get_schema_version(true);
865 authenticate_user(null, null, true);
868 if (!$_SESSION["uid"]) {
876 /* bump login timestamp */
877 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
878 $sth->execute([$_SESSION['uid']]);
880 $_SESSION["last_login_update"] = time();
883 if ($_SESSION["uid"]) {
885 load_user_plugins($_SESSION["uid"]);
889 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
891 (SELECT COUNT(id) FROM ttrss_feeds WHERE
892 ttrss_feeds.id = feed_id) = 0");
894 $sth->execute([$_SESSION['uid']]);
896 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
898 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
899 ttrss_feed_categories.id = feed_id) = 0");
901 $sth->execute([$_SESSION['uid']]);
907 function truncate_string($str, $max_len, $suffix = '…') {
908 if (mb_strlen($str, "utf-8") > $max_len) {
909 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
916 function truncate_middle($str, $max_len, $suffix = '…') {
917 if (strlen($str) > $max_len) {
918 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
924 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
927 $source_tz = new DateTimeZone($source_tz);
928 } catch (Exception
$e) {
929 $source_tz = new DateTimeZone('UTC');
933 $dest_tz = new DateTimeZone($dest_tz);
934 } catch (Exception
$e) {
935 $dest_tz = new DateTimeZone('UTC');
938 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
939 return $dt->format('U') +
$dest_tz->getOffset($dt);
942 function make_local_datetime($timestamp, $long, $owner_uid = false,
943 $no_smart_dt = false, $eta_min = false) {
945 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
946 if (!$timestamp) $timestamp = '1970-01-01 0:00';
951 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
953 $timestamp = substr($timestamp, 0, 19);
955 # We store date in UTC internally
956 $dt = new DateTime($timestamp, $utc_tz);
958 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
960 if ($user_tz_string != 'Automatic') {
963 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
964 } catch (Exception
$e) {
968 $tz_offset = $user_tz->getOffset($dt);
970 $tz_offset = (int) -$_SESSION["clientTzOffset"];
973 $user_timestamp = $dt->format('U') +
$tz_offset;
976 return smart_date_time($user_timestamp,
977 $tz_offset, $owner_uid, $eta_min);
980 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
982 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
984 return date($format, $user_timestamp);
988 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
989 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
991 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
992 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
993 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
994 return date("G:i", $timestamp);
995 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
996 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
997 return date($format, $timestamp);
999 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
1000 return date($format, $timestamp);
1004 function sql_bool_to_bool($s) {
1005 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
1008 function bool_to_sql_bool($s) {
1012 // Session caching removed due to causing wrong redirects to upgrade
1013 // script when get_schema_version() is called on an obsolete session
1014 // created on a previous schema version.
1015 function get_schema_version($nocache = false) {
1016 global $schema_version;
1020 if (!$schema_version && !$nocache) {
1021 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
1022 $version = $row["schema_version"];
1023 $schema_version = $version;
1026 return $schema_version;
1030 function sanity_check() {
1031 require_once 'errors.php';
1035 $schema_version = get_schema_version(true);
1037 if ($schema_version != SCHEMA_VERSION
) {
1041 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1044 function file_is_locked($filename) {
1045 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
1046 if (function_exists('flock')) {
1047 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1049 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1050 flock($fp, LOCK_UN
);
1060 return true; // consider the file always locked and skip the test
1067 function make_lockfile($filename) {
1068 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1070 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1071 $stat_h = fstat($fp);
1072 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1074 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1075 if ($stat_h["ino"] != $stat_f["ino"] ||
1076 $stat_h["dev"] != $stat_f["dev"]) {
1082 if (function_exists('posix_getpid')) {
1083 fwrite($fp, posix_getpid() . "\n");
1091 function make_stampfile($filename) {
1092 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1094 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1095 fwrite($fp, time() . "\n");
1096 flock($fp, LOCK_UN
);
1104 function sql_random_function() {
1105 if (DB_TYPE
== "mysql") {
1112 function getFeedUnread($feed, $is_cat = false) {
1113 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1116 function checkbox_to_sql_bool($val) {
1117 return ($val == "on") ?
1 : 0;
1120 function uniqid_short() {
1121 return uniqid(base_convert(rand(), 10, 36));
1124 function make_init_params() {
1127 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1128 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1129 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1130 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1132 $params[strtolower($param)] = (int) get_pref($param);
1135 $params["icons_url"] = ICONS_URL
;
1136 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1137 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1138 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1139 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1140 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1141 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1142 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1144 $theme = get_pref( "USER_CSS_THEME", false, false);
1145 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1147 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1149 $params["php_platform"] = PHP_OS
;
1150 $params["php_version"] = PHP_VERSION
;
1152 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1156 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1157 ttrss_feeds WHERE owner_uid = ?");
1158 $sth->execute([$_SESSION['uid']]);
1159 $row = $sth->fetch();
1161 $max_feed_id = $row["mid"];
1162 $num_feeds = $row["nf"];
1164 $params["max_feed_id"] = (int) $max_feed_id;
1165 $params["num_feeds"] = (int) $num_feeds;
1167 $params["hotkeys"] = get_hotkeys_map();
1169 $params["csrf_token"] = $_SESSION["csrf_token"];
1170 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1172 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1174 $params["icon_alert"] = base64_img("images/alert.png");
1175 $params["icon_information"] = base64_img("images/information.png");
1176 $params["icon_cross"] = base64_img("images/cross.png");
1177 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1179 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1184 function get_hotkeys_info() {
1186 __("Navigation") => array(
1187 "next_feed" => __("Open next feed"),
1188 "prev_feed" => __("Open previous feed"),
1189 "next_article" => __("Open next article"),
1190 "prev_article" => __("Open previous article"),
1191 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1192 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1193 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1194 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1195 "search_dialog" => __("Show search dialog")),
1196 __("Article") => array(
1197 "toggle_mark" => __("Toggle starred"),
1198 "toggle_publ" => __("Toggle published"),
1199 "toggle_unread" => __("Toggle unread"),
1200 "edit_tags" => __("Edit tags"),
1201 "open_in_new_window" => __("Open in new window"),
1202 "catchup_below" => __("Mark below as read"),
1203 "catchup_above" => __("Mark above as read"),
1204 "article_scroll_down" => __("Scroll down"),
1205 "article_scroll_up" => __("Scroll up"),
1206 "select_article_cursor" => __("Select article under cursor"),
1207 "email_article" => __("Email article"),
1208 "close_article" => __("Close/collapse article"),
1209 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1210 "toggle_widescreen" => __("Toggle widescreen mode"),
1211 "toggle_embed_original" => __("Toggle embed original")),
1212 __("Article selection") => array(
1213 "select_all" => __("Select all articles"),
1214 "select_unread" => __("Select unread"),
1215 "select_marked" => __("Select starred"),
1216 "select_published" => __("Select published"),
1217 "select_invert" => __("Invert selection"),
1218 "select_none" => __("Deselect everything")),
1219 __("Feed") => array(
1220 "feed_refresh" => __("Refresh current feed"),
1221 "feed_unhide_read" => __("Un/hide read feeds"),
1222 "feed_subscribe" => __("Subscribe to feed"),
1223 "feed_edit" => __("Edit feed"),
1224 "feed_catchup" => __("Mark as read"),
1225 "feed_reverse" => __("Reverse headlines"),
1226 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1227 "feed_debug_update" => __("Debug feed update"),
1228 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1229 "catchup_all" => __("Mark all feeds as read"),
1230 "cat_toggle_collapse" => __("Un/collapse current category"),
1231 "toggle_combined_mode" => __("Toggle combined mode"),
1232 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1233 __("Go to") => array(
1234 "goto_all" => __("All articles"),
1235 "goto_fresh" => __("Fresh"),
1236 "goto_marked" => __("Starred"),
1237 "goto_published" => __("Published"),
1238 "goto_tagcloud" => __("Tag cloud"),
1239 "goto_prefs" => __("Preferences")),
1240 __("Other") => array(
1241 "create_label" => __("Create label"),
1242 "create_filter" => __("Create filter"),
1243 "collapse_sidebar" => __("Un/collapse sidebar"),
1244 "help_dialog" => __("Show help dialog"))
1247 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1248 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1254 function get_hotkeys_map() {
1256 // "navigation" => array(
1259 "n" => "next_article",
1260 "p" => "prev_article",
1261 "(38)|up" => "prev_article",
1262 "(40)|down" => "next_article",
1263 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1264 // "^(40)|Ctrl-down" => "next_article_noscroll",
1265 "(191)|/" => "search_dialog",
1266 // "article" => array(
1267 "s" => "toggle_mark",
1268 "*s" => "toggle_publ",
1269 "u" => "toggle_unread",
1270 "*t" => "edit_tags",
1271 "o" => "open_in_new_window",
1272 "c p" => "catchup_below",
1273 "c n" => "catchup_above",
1274 "*n" => "article_scroll_down",
1275 "*p" => "article_scroll_up",
1276 "*(38)|Shift+up" => "article_scroll_up",
1277 "*(40)|Shift+down" => "article_scroll_down",
1278 "a *w" => "toggle_widescreen",
1279 "a e" => "toggle_embed_original",
1280 "e" => "email_article",
1281 "a q" => "close_article",
1282 // "article_selection" => array(
1283 "a a" => "select_all",
1284 "a u" => "select_unread",
1285 "a *u" => "select_marked",
1286 "a p" => "select_published",
1287 "a i" => "select_invert",
1288 "a n" => "select_none",
1290 "f r" => "feed_refresh",
1291 "f a" => "feed_unhide_read",
1292 "f s" => "feed_subscribe",
1293 "f e" => "feed_edit",
1294 "f q" => "feed_catchup",
1295 "f x" => "feed_reverse",
1296 "f g" => "feed_toggle_vgroup",
1297 "f *d" => "feed_debug_update",
1298 "f *g" => "feed_debug_viewfeed",
1299 "f *c" => "toggle_combined_mode",
1300 "f c" => "toggle_cdm_expanded",
1301 "*q" => "catchup_all",
1302 "x" => "cat_toggle_collapse",
1304 "g a" => "goto_all",
1305 "g f" => "goto_fresh",
1306 "g s" => "goto_marked",
1307 "g p" => "goto_published",
1308 "g t" => "goto_tagcloud",
1309 "g *p" => "goto_prefs",
1310 // "other" => array(
1311 "(9)|Tab" => "select_article_cursor", // tab
1312 "c l" => "create_label",
1313 "c f" => "create_filter",
1314 "c s" => "collapse_sidebar",
1315 "^(191)|Ctrl+/" => "help_dialog",
1318 if (get_pref('COMBINED_DISPLAY_MODE')) {
1319 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1320 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1323 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1324 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1327 $prefixes = array();
1329 foreach (array_keys($hotkeys) as $hotkey) {
1330 $pair = explode(" ", $hotkey, 2);
1332 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1333 array_push($prefixes, $pair[0]);
1337 return array($prefixes, $hotkeys);
1340 function check_for_update() {
1341 if (defined("GIT_VERSION_TIMESTAMP")) {
1342 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1345 $content = json_decode($content, true);
1347 if ($content && isset($content["changeset"])) {
1348 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1349 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1351 return $content["changeset"]["id"];
1360 function make_runtime_info($disable_update_check = false) {
1365 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1366 ttrss_feeds WHERE owner_uid = ?");
1367 $sth->execute([$_SESSION['uid']]);
1368 $row = $sth->fetch();
1370 $max_feed_id = $row['mid'];
1371 $num_feeds = $row['nf'];
1373 $data["max_feed_id"] = (int) $max_feed_id;
1374 $data["num_feeds"] = (int) $num_feeds;
1376 $data['last_article_id'] = Article
::getLastArticleId();
1377 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1379 $data['dep_ts'] = calculate_dep_timestamp();
1380 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1382 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1384 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1385 $update_result = @check_for_update
();
1387 $data["update_result"] = $update_result;
1389 $_SESSION["last_version_check"] = time();
1392 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1394 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1396 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1398 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1401 $stamp_delta = time() - $stamp;
1403 if ($stamp_delta > 1800) {
1407 $_SESSION["daemon_stamp_check"] = time();
1410 $data['daemon_stamp_ok'] = $stamp_check;
1412 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1414 $data['daemon_stamp'] = $stamp_fmt;
1422 function search_to_sql($search, $search_language) {
1424 $keywords = str_getcsv(trim($search), " ");
1425 $query_keywords = array();
1426 $search_words = array();
1427 $search_query_leftover = array();
1431 if ($search_language)
1432 $search_language = $pdo->quote(mb_strtolower($search_language));
1434 $search_language = $pdo->quote("english");
1436 foreach ($keywords as $k) {
1437 if (strpos($k, "-") === 0) {
1444 $commandpair = explode(":", mb_strtolower($k), 2);
1446 switch ($commandpair[0]) {
1448 if ($commandpair[1]) {
1449 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1450 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1452 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1453 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1454 array_push($search_words, $k);
1458 if ($commandpair[1]) {
1459 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1460 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1462 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1463 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1464 array_push($search_words, $k);
1468 if ($commandpair[1]) {
1469 if ($commandpair[1] == "true")
1470 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1471 else if ($commandpair[1] == "false")
1472 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1474 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1475 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1477 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1478 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1479 if (!$not) array_push($search_words, $k);
1484 if ($commandpair[1]) {
1485 if ($commandpair[1] == "true")
1486 array_push($query_keywords, "($not (marked = true))");
1488 array_push($query_keywords, "($not (marked = false))");
1490 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1491 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1492 if (!$not) array_push($search_words, $k);
1496 if ($commandpair[1]) {
1497 if ($commandpair[1] == "true")
1498 array_push($query_keywords, "($not (published = true))");
1500 array_push($query_keywords, "($not (published = false))");
1503 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1504 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1505 if (!$not) array_push($search_words, $k);
1509 if ($commandpair[1]) {
1510 if ($commandpair[1] == "true")
1511 array_push($query_keywords, "($not (unread = true))");
1513 array_push($query_keywords, "($not (unread = false))");
1516 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1517 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1518 if (!$not) array_push($search_words, $k);
1522 if (strpos($k, "@") === 0) {
1524 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1525 $orig_ts = strtotime(substr($k, 1));
1526 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1528 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1530 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1533 if (DB_TYPE
== "pgsql") {
1534 $k = mb_strtolower($k);
1535 array_push($search_query_leftover, $not ?
"!$k" : $k);
1537 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1538 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1541 if (!$not) array_push($search_words, $k);
1546 if (count($search_query_leftover) > 0) {
1547 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1549 if (DB_TYPE
== "pgsql") {
1550 array_push($query_keywords,
1551 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1556 $search_query_part = implode("AND", $query_keywords);
1558 return array($search_query_part, $search_words);
1561 function iframe_whitelisted($entry) {
1562 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1564 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1567 foreach ($whitelist as $w) {
1568 if ($src == $w ||
$src == "www.$w")
1576 // check for locally cached (media) URLs and rewrite to local versions
1577 // this is called separately after sanitize() and plugin render article hooks to allow
1578 // plugins work on original source URLs used before caching
1580 function rewrite_cached_urls($str) {
1581 $charset_hack = '<head>
1582 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1585 $res = trim($str); if (!$res) return '';
1587 $doc = new DOMDocument();
1588 $doc->loadHTML($charset_hack . $res);
1589 $xpath = new DOMXPath($doc);
1591 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
1593 $need_saving = false;
1595 foreach ($entries as $entry) {
1597 if ($entry->hasAttribute('src') ||
$entry->hasAttribute('poster')) {
1599 // should be already absolutized because this is called after sanitize()
1600 $src = $entry->hasAttribute('poster') ?
$entry->getAttribute('poster') : $entry->getAttribute('src');
1601 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1603 if (file_exists($cached_filename)) {
1605 // this is strictly cosmetic
1606 if ($entry->tagName
== 'img') {
1608 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1610 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1616 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1618 if ($entry->hasAttribute('poster'))
1619 $entry->setAttribute('poster', $src);
1621 $entry->setAttribute('src', $src);
1623 $need_saving = true;
1629 $doc->removeChild($doc->firstChild
); //remove doctype
1630 $res = $doc->saveHTML();
1636 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1637 if (!$owner) $owner = $_SESSION["uid"];
1639 $res = trim($str); if (!$res) return '';
1641 $charset_hack = '<head>
1642 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1645 $res = trim($res); if (!$res) return '';
1647 libxml_use_internal_errors(true);
1649 $doc = new DOMDocument();
1650 $doc->loadHTML($charset_hack . $res);
1651 $xpath = new DOMXPath($doc);
1653 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1655 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1657 foreach ($entries as $entry) {
1659 if ($entry->hasAttribute('href')) {
1660 $entry->setAttribute('href',
1661 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1663 $entry->setAttribute('rel', 'noopener noreferrer');
1666 if ($entry->hasAttribute('src')) {
1667 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1669 // cache stuff has gone to rewrite_cached_urls()
1671 $entry->setAttribute('src', $src);
1674 if ($entry->nodeName
== 'img') {
1675 $entry->setAttribute('referrerpolicy', 'no-referrer');
1677 $entry->removeAttribute('width');
1678 $entry->removeAttribute('height');
1680 if ($entry->hasAttribute('src')) {
1681 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1683 if (is_prefix_https() && !$is_https_url) {
1685 if ($entry->hasAttribute('srcset')) {
1686 $entry->removeAttribute('srcset');
1689 if ($entry->hasAttribute('sizes')) {
1690 $entry->removeAttribute('sizes');
1696 if ($entry->hasAttribute('src') &&
1697 ($owner && get_pref("STRIP_IMAGES", $owner)) ||
$force_remove_images ||
$_SESSION["bw_limit"]) {
1699 $p = $doc->createElement('p');
1701 $a = $doc->createElement('a');
1702 $a->setAttribute('href', $entry->getAttribute('src'));
1704 $a->appendChild(new DOMText($entry->getAttribute('src')));
1705 $a->setAttribute('target', '_blank');
1706 $a->setAttribute('rel', 'noopener noreferrer');
1708 $p->appendChild($a);
1710 if ($entry->nodeName
== 'source') {
1712 if ($entry->parentNode
&& $entry->parentNode
->parentNode
)
1713 $entry->parentNode
->parentNode
->replaceChild($p, $entry->parentNode
);
1715 } else if ($entry->nodeName
== 'img') {
1717 if ($entry->parentNode
)
1718 $entry->parentNode
->replaceChild($p, $entry);
1723 if (strtolower($entry->nodeName
) == "a") {
1724 $entry->setAttribute("target", "_blank");
1725 $entry->setAttribute("rel", "noopener noreferrer");
1729 $entries = $xpath->query('//iframe');
1730 foreach ($entries as $entry) {
1731 if (!iframe_whitelisted($entry)) {
1732 $entry->setAttribute('sandbox', 'allow-scripts');
1734 if (is_prefix_https()) {
1735 $entry->setAttribute("src",
1736 str_replace("http://", "https://",
1737 $entry->getAttribute("src")));
1742 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1743 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1744 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1745 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1746 'dt', 'em', 'footer', 'figure', 'figcaption',
1747 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1748 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1749 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1750 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1751 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1752 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1754 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1756 $disallowed_attributes = array('id', 'style', 'class');
1758 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1759 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1760 if (is_array($retval)) {
1762 $allowed_elements = $retval[1];
1763 $disallowed_attributes = $retval[2];
1769 $doc->removeChild($doc->firstChild
); //remove doctype
1770 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1772 if ($highlight_words) {
1773 foreach ($highlight_words as $word) {
1775 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1777 $elements = $xpath->query("//*/text()");
1779 foreach ($elements as $child) {
1781 $fragment = $doc->createDocumentFragment();
1782 $text = $child->textContent
;
1784 while (($pos = mb_stripos($text, $word)) !== false) {
1785 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1786 $word = mb_substr($text, $pos, mb_strlen($word));
1787 $highlight = $doc->createElement('span');
1788 $highlight->appendChild(new DomText($word));
1789 $highlight->setAttribute('class', 'highlight');
1790 $fragment->appendChild($highlight);
1791 $text = mb_substr($text, $pos +
mb_strlen($word));
1794 if (!empty($text)) $fragment->appendChild(new DomText($text));
1796 $child->parentNode
->replaceChild($fragment, $child);
1801 $res = $doc->saveHTML();
1803 /* strip everything outside of <body>...</body> */
1805 $res_frag = array();
1806 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1807 return $res_frag[1];
1813 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1814 $xpath = new DOMXPath($doc);
1815 $entries = $xpath->query('//*');
1817 foreach ($entries as $entry) {
1818 if (!in_array($entry->nodeName
, $allowed_elements)) {
1819 $entry->parentNode
->removeChild($entry);
1822 if ($entry->hasAttributes()) {
1823 $attrs_to_remove = array();
1825 foreach ($entry->attributes
as $attr) {
1827 if (strpos($attr->nodeName
, 'on') === 0) {
1828 array_push($attrs_to_remove, $attr);
1831 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1832 array_push($attrs_to_remove, $attr);
1835 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1836 array_push($attrs_to_remove, $attr);
1840 foreach ($attrs_to_remove as $attr) {
1841 $entry->removeAttributeNode($attr);
1849 function trim_array($array) {
1851 array_walk($tmp, 'trim');
1855 function tag_is_valid($tag) {
1856 if (!$tag ||
is_numeric($tag) ||
mb_strlen($tag) > 250)
1862 function render_login_form() {
1863 header('Cache-Control: public');
1865 require_once "login_form.php";
1869 function T_sprintf() {
1870 $args = func_get_args();
1871 return vsprintf(__(array_shift($args)), $args);
1874 function print_checkpoint($n, $s) {
1875 $ts = microtime(true);
1876 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1880 function sanitize_tag($tag) {
1883 $tag = mb_strtolower($tag, 'utf-8');
1885 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1887 if (DB_TYPE
== "mysql") {
1888 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1894 function is_server_https() {
1895 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1898 function is_prefix_https() {
1899 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1902 // this returns SELF_URL_PATH sans ending slash
1903 function get_self_url_prefix() {
1904 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1905 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1907 return SELF_URL_PATH
;
1911 function encrypt_password($pass, $salt = '', $mode2 = false) {
1912 if ($salt && $mode2) {
1913 return "MODE2:" . hash('sha256', $salt . $pass);
1915 return "SHA1X:" . sha1("$salt:$pass");
1917 return "SHA1:" . sha1($pass);
1919 } // function encrypt_password
1921 function load_filters($feed_id, $owner_uid) {
1924 $feed_id = (int) $feed_id;
1925 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1928 $null_cat_qpart = "cat_id IS NULL OR";
1930 $null_cat_qpart = "";
1934 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1935 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1936 $sth->execute([$owner_uid]);
1938 $check_cats = array_merge(
1939 Feeds
::getParentCategories($cat_id, $owner_uid),
1942 $check_cats_str = join(",", $check_cats);
1943 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1945 while ($line = $sth->fetch()) {
1946 $filter_id = $line["id"];
1948 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1950 $sth2 = $pdo->prepare("SELECT
1951 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1952 FROM ttrss_filters2_rules AS r,
1953 ttrss_filter_types AS t
1955 (match_on IS NOT NULL OR
1956 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1957 (feed_id IS NULL OR feed_id = ?))) AND
1958 filter_type = t.id AND filter_id = ?");
1959 $sth2->execute([$feed_id, $filter_id]);
1964 while ($rule_line = $sth2->fetch()) {
1965 # print_r($rule_line);
1967 if ($rule_line["match_on"]) {
1968 $match_on = json_decode($rule_line["match_on"], true);
1970 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1973 $rule["reg_exp"] = $rule_line["reg_exp"];
1974 $rule["type"] = $rule_line["type_name"];
1975 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1977 array_push($rules, $rule);
1978 } else if (!$match_any_rule) {
1979 // this filter contains a rule that doesn't match to this feed/category combination
1980 // thus filter has to be rejected
1989 $rule["reg_exp"] = $rule_line["reg_exp"];
1990 $rule["type"] = $rule_line["type_name"];
1991 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1993 array_push($rules, $rule);
1997 if (count($rules) > 0) {
1998 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1999 FROM ttrss_filters2_actions AS a,
2000 ttrss_filter_actions AS t
2002 action_id = t.id AND filter_id = ?");
2003 $sth2->execute([$filter_id]);
2005 while ($action_line = $sth2->fetch()) {
2006 # print_r($action_line);
2009 $action["type"] = $action_line["type_name"];
2010 $action["param"] = $action_line["action_param"];
2012 array_push($actions, $action);
2017 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
2018 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
2019 $filter["rules"] = $rules;
2020 $filter["actions"] = $actions;
2022 if (count($rules) > 0 && count($actions) > 0) {
2023 array_push($filters, $filter);
2030 function get_score_pic($score) {
2032 return "score_high.png";
2033 } else if ($score > 0) {
2034 return "score_half_high.png";
2035 } else if ($score < -100) {
2036 return "score_low.png";
2037 } else if ($score < 0) {
2038 return "score_half_low.png";
2040 return "score_neutral.png";
2044 function init_plugins() {
2045 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
2050 function add_feed_category($feed_cat, $parent_cat_id = false) {
2052 if (!$feed_cat) return false;
2054 $feed_cat = mb_substr($feed_cat, 0, 250);
2055 if (!$parent_cat_id) $parent_cat_id = null;
2058 $tr_in_progress = false;
2061 $pdo->beginTransaction();
2062 } catch (Exception
$e) {
2063 $tr_in_progress = true;
2066 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2067 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2068 AND title = :title AND owner_uid = :uid");
2069 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2071 if (!$sth->fetch()) {
2073 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2075 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2077 if (!$tr_in_progress) $pdo->commit();
2088 * Fixes incomplete URLs by prepending "http://".
2089 * Also replaces feed:// with http://, and
2090 * prepends a trailing slash if the url is a domain name only.
2092 * @param string $url Possibly incomplete URL
2094 * @return string Fixed URL.
2096 function fix_url($url) {
2098 // support schema-less urls
2099 if (strpos($url, '//') === 0) {
2100 $url = 'https:' . $url;
2103 if (strpos($url, '://') === false) {
2104 $url = 'http://' . $url;
2105 } else if (substr($url, 0, 5) == 'feed:') {
2106 $url = 'http:' . substr($url, 5);
2109 //prepend slash if the URL has no slash in it
2110 // "http://www.example" -> "http://www.example/"
2111 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2115 //convert IDNA hostname to punycode if possible
2116 if (function_exists("idn_to_ascii")) {
2117 $parts = parse_url($url);
2118 if (mb_detect_encoding($parts['host']) != 'ASCII')
2120 $parts['host'] = idn_to_ascii($parts['host']);
2121 $url = build_url($parts);
2125 if ($url != "http:///")
2131 function validate_feed_url($url) {
2132 $parts = parse_url($url);
2134 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2138 /* function save_email_address($email) {
2139 // FIXME: implement persistent storage of emails
2141 if (!$_SESSION['stored_emails'])
2142 $_SESSION['stored_emails'] = array();
2144 if (!in_array($email, $_SESSION['stored_emails']))
2145 array_push($_SESSION['stored_emails'], $email);
2149 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2151 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2153 $is_cat = bool_to_sql_bool($is_cat);
2157 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2158 WHERE feed_id = ? AND is_cat = ?
2159 AND owner_uid = ?");
2160 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2162 if ($row = $sth->fetch()) {
2163 return $row["access_key"];
2165 $key = uniqid_short();
2167 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2168 (access_key, feed_id, is_cat, owner_uid)
2169 VALUES (?, ?, ?, ?)");
2171 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2177 function get_feeds_from_html($url, $content)
2179 $url = fix_url($url);
2180 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2182 libxml_use_internal_errors(true);
2184 $doc = new DOMDocument();
2185 $doc->loadHTML($content);
2186 $xpath = new DOMXPath($doc);
2187 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2188 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2189 $feedUrls = array();
2190 foreach ($entries as $entry) {
2191 if ($entry->hasAttribute('href')) {
2192 $title = $entry->getAttribute('title');
2194 $title = $entry->getAttribute('type');
2196 $feedUrl = rewrite_relative_url(
2197 $baseUrl, $entry->getAttribute('href')
2199 $feedUrls[$feedUrl] = $title;
2205 function is_html($content) {
2206 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2209 function url_is_html($url, $login = false, $pass = false) {
2210 return is_html(fetch_file_contents($url, false, $login, $pass));
2213 function build_url($parts) {
2214 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2217 function cleanup_url_path($path) {
2218 $path = str_replace("/./", "/", $path);
2219 $path = str_replace("//", "/", $path);
2225 * Converts a (possibly) relative URL to a absolute one.
2227 * @param string $url Base URL (i.e. from where the document is)
2228 * @param string $rel_url Possibly relative URL in the document
2230 * @return string Absolute URL
2232 function rewrite_relative_url($url, $rel_url) {
2233 if (strpos($rel_url, "://") !== false) {
2235 } else if (strpos($rel_url, "//") === 0) {
2236 # protocol-relative URL (rare but they exist)
2238 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2239 # magnet:, feed:, etc
2241 } else if (strpos($rel_url, "/") === 0) {
2242 $parts = parse_url($url);
2243 $parts['path'] = $rel_url;
2244 $parts['path'] = cleanup_url_path($parts['path']);
2246 return build_url($parts);
2249 $parts = parse_url($url);
2250 if (!isset($parts['path'])) {
2251 $parts['path'] = '/';
2253 $dir = $parts['path'];
2254 if (substr($dir, -1) !== '/') {
2255 $dir = dirname($parts['path']);
2256 $dir !== '/' && $dir .= '/';
2258 $parts['path'] = $dir . $rel_url;
2259 $parts['path'] = cleanup_url_path($parts['path']);
2261 return build_url($parts);
2265 function cleanup_tags($days = 14, $limit = 1000) {
2267 $days = (int) $days;
2269 if (DB_TYPE
== "pgsql") {
2270 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2271 } else if (DB_TYPE
== "mysql") {
2272 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2279 while ($limit > 0) {
2282 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2283 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2284 WHERE post_int_id = int_id AND $interval_query AND
2285 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2286 $sth->execute([$limit]);
2290 while ($line = $sth->fetch()) {
2291 array_push($ids, $line['id']);
2294 if (count($ids) > 0) {
2295 $ids = join(",", $ids);
2297 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2298 $tags_deleted = $usth->rowCount();
2303 $limit -= $limit_part;
2306 return $tags_deleted;
2309 function print_user_stylesheet() {
2310 $value = get_pref('USER_STYLESHEET');
2313 print "<style type=\"text/css\">";
2314 print str_replace("<br/>", "\n", $value);
2320 function filter_to_sql($filter, $owner_uid) {
2325 if (DB_TYPE
== "pgsql")
2328 $reg_qpart = "REGEXP";
2330 foreach ($filter["rules"] AS $rule) {
2331 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2332 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2333 $rule['reg_exp']) !== FALSE;
2335 if ($regexp_valid) {
2337 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2339 switch ($rule["type"]) {
2341 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2342 $rule['reg_exp'] . "')";
2345 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2346 $rule['reg_exp'] . "')";
2349 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2350 $rule['reg_exp'] . "') OR LOWER(" .
2351 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2354 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2355 $rule['reg_exp'] . "')";
2358 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2359 $rule['reg_exp'] . "')";
2362 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2363 $rule['reg_exp'] . "')";
2367 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2369 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2370 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2373 if (isset($rule["cat_id"])) {
2375 if ($rule["cat_id"] > 0) {
2376 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2377 array_push($children, $rule["cat_id"]);
2378 $children = array_map("intval", $children);
2380 $children = join(",", $children);
2382 $cat_qpart = "cat_id IN ($children)";
2384 $cat_qpart = "cat_id IS NULL";
2387 $qpart .= " AND $cat_qpart";
2390 $qpart .= " AND feed_id IS NOT NULL";
2392 array_push($query, "($qpart)");
2397 if (count($query) > 0) {
2398 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2400 $fullquery = "(false)";
2403 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2408 if (!function_exists('gzdecode')) {
2409 function gzdecode($string) { // no support for 2nd argument
2410 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2411 base64_encode($string));
2415 function get_random_bytes($length) {
2416 if (function_exists('openssl_random_pseudo_bytes')) {
2417 return openssl_random_pseudo_bytes($length);
2421 for ($i = 0; $i < $length; $i++
)
2422 $output .= chr(mt_rand(0, 255));
2428 function read_stdin() {
2429 $fp = fopen("php://stdin", "r");
2432 $line = trim(fgets($fp));
2440 function implements_interface($class, $interface) {
2441 return in_array($interface, class_implements($class));
2444 function get_minified_js($files) {
2448 foreach ($files as $js) {
2449 if (!isset($_GET['debug'])) {
2450 $cached_file = CACHE_DIR
. "/js/".basename($js);
2452 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2454 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2456 if ($header && $contents) {
2457 list($htag, $hversion) = explode(":", $header);
2459 if ($htag == "tt-rss" && $hversion == VERSION
) {
2466 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2467 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2471 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2478 function calculate_dep_timestamp() {
2479 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2483 foreach ($files as $file) {
2484 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2490 function T_js_decl($s1, $s2) {
2492 $s1 = preg_replace("/\n/", "", $s1);
2493 $s2 = preg_replace("/\n/", "", $s2);
2495 $s1 = preg_replace("/\"/", "\\\"", $s1);
2496 $s2 = preg_replace("/\"/", "\\\"", $s2);
2498 return "T_messages[\"$s1\"] = \"$s2\";\n";
2502 function init_js_translations() {
2504 print 'var T_messages = new Object();
2507 if (T_messages[msg]) {
2508 return T_messages[msg];
2514 function ngettext(msg1, msg2, n) {
2515 return __((parseInt(n) > 1) ? msg2 : msg1);
2518 $l10n = _get_reader();
2520 for ($i = 0; $i < $l10n->total
; $i++
) {
2521 $orig = $l10n->get_original_string($i);
2522 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2523 $key = explode(chr(0), $orig);
2524 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2525 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2527 $translation = __($orig);
2528 print T_js_decl($orig, $translation);
2533 function get_theme_path($theme) {
2534 if ($theme == "default.php")
2535 return "css/default.css";
2537 $check = "themes/$theme";
2538 if (file_exists($check)) return $check;
2540 $check = "themes.local/$theme";
2541 if (file_exists($check)) return $check;
2544 function theme_valid($theme) {
2545 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2547 if (in_array($theme, $bundled_themes)) return true;
2549 $file = "themes/" . basename($theme);
2551 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2553 if (file_exists($file) && is_readable($file)) {
2554 $fh = fopen($file, "r");
2557 $header = fgets($fh);
2560 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2568 * @SuppressWarnings(unused)
2570 function error_json($code) {
2571 require_once "errors.php";
2573 @$message = $ERRORS[$code];
2575 return json_encode(array("error" =>
2576 array("code" => $code, "message" => $message)));
2580 /*function abs_to_rel_path($dir) {
2581 $tmp = str_replace(dirname(__DIR__), "", $dir);
2583 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2588 function get_upload_error_message($code) {
2591 0 => __('There is no error, the file uploaded with success'),
2592 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2593 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2594 3 => __('The uploaded file was only partially uploaded'),
2595 4 => __('No file was uploaded'),
2596 6 => __('Missing a temporary folder'),
2597 7 => __('Failed to write file to disk.'),
2598 8 => __('A PHP extension stopped the file upload.'),
2601 return $errors[$code];
2604 function base64_img($filename) {
2605 if (file_exists($filename)) {
2606 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2608 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2614 /* this is essentially a wrapper for readfile() which allows plugins to hook
2615 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2617 hook function should return true if request was handled (or at least attempted to)
2619 note that this can be called without user context so the plugin to handle this
2620 should be loaded systemwide in config.php */
2621 function send_local_file($filename) {
2622 if (file_exists($filename)) {
2624 if (is_writable($filename)) touch($filename);
2626 $tmppluginhost = new PluginHost();
2628 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2629 $tmppluginhost->load_data();
2631 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2632 if ($plugin->hook_send_local_file($filename)) return true;
2635 $mimetype = mime_content_type($filename);
2637 // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2638 // video files are detected as octet-stream by mime_content_type()
2640 if ($mimetype == "application/octet-stream")
2641 $mimetype = "video/mp4";
2643 header("Content-type: $mimetype");
2645 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2646 header("Last-Modified: $stamp", true);
2648 return readfile($filename);
2654 function check_mysql_tables() {
2657 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2658 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2659 $sth->execute([DB_NAME
]);
2663 while ($line = $sth->fetch()) {
2664 array_push($bad_tables, $line);
2670 function validate_field($string, $allowed, $default = "") {
2671 if (in_array($string, $allowed))
2677 function arr_qmarks($arr) {
2678 return str_repeat('?,', count($arr) - 1) . '?';