2 define('EXPECTED_CONFIG_VERSION', 26);
3 define('SCHEMA_VERSION', 134);
5 define('LABEL_BASE_INDEX', -1024);
6 define('PLUGIN_FEED_BASE_INDEX', -128);
8 define('COOKIE_LIFETIME_LONG', 86400*365);
10 $fetch_last_error = false;
11 $fetch_last_error_code = false;
12 $fetch_last_content_type = false;
13 $fetch_last_error_content = false; // curl only for the time being
14 $fetch_effective_url = false;
15 $fetch_curl_used = false;
16 $suppress_debugging = false;
18 libxml_disable_entity_loader(true);
20 // separate test because this is included before sanity checks
21 if (function_exists("mb_internal_encoding")) mb_internal_encoding("UTF-8");
23 date_default_timezone_set('UTC');
24 if (defined('E_DEPRECATED')) {
25 error_reporting(E_ALL
& ~E_NOTICE
& ~E_DEPRECATED
);
27 error_reporting(E_ALL
& ~E_NOTICE
);
30 require_once 'config.php';
33 * Define a constant if not already defined
35 function define_default($name, $value) {
36 defined($name) or define($name, $value);
39 /* Some tunables you can override in config.php using define(): */
41 define_default('FEED_FETCH_TIMEOUT', 45);
42 // How may seconds to wait for response when requesting feed from a site
43 define_default('FEED_FETCH_NO_CACHE_TIMEOUT', 15);
44 // How may seconds to wait for response when requesting feed from a
45 // site when that feed wasn't cached before
46 define_default('FILE_FETCH_TIMEOUT', 45);
47 // Default timeout when fetching files from remote sites
48 define_default('FILE_FETCH_CONNECT_TIMEOUT', 15);
49 // How many seconds to wait for initial response from website when
50 // fetching files from remote sites
51 define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
52 // stop updating feeds if users haven't logged in for X days
53 define_default('DAEMON_FEED_LIMIT', 500);
54 // feed limit for one update batch
55 define_default('DAEMON_SLEEP_INTERVAL', 120);
56 // default sleep interval between feed updates (sec)
57 define_default('MIN_CACHE_FILE_SIZE', 1024);
58 // do not cache files smaller than that (bytes)
59 define_default('MAX_CACHE_FILE_SIZE', 64*1024*1024);
60 // do not cache files larger than that (bytes)
61 define_default('MAX_DOWNLOAD_FILE_SIZE', 16*1024*1024);
62 // do not download general files larger than that (bytes)
63 define_default('CACHE_MAX_DAYS', 7);
64 // max age in days for various automatically cached (temporary) files
65 define_default('MAX_CONDITIONAL_INTERVAL', 3600*12);
66 // max interval between forced unconditional updates for servers
67 // not complying with http if-modified-since (seconds)
69 /* tunables end here */
71 if (DB_TYPE
== "pgsql") {
72 define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
74 define('SUBSTRING_FOR_DATE', 'SUBSTRING');
78 * Return available translations names.
81 * @return array A array of available translations.
83 function get_translations() {
85 "auto" => "Detect automatically",
86 "ar_SA" => "العربيّة (Arabic)",
87 "bg_BG" => "Bulgarian",
92 "el_GR" => "Ελληνικά",
93 "es_ES" => "Español (España)",
96 "fr_FR" => "Français",
97 "hu_HU" => "Magyar (Hungarian)",
98 "it_IT" => "Italiano",
99 "ja_JP" => "日本語 (Japanese)",
100 "lv_LV" => "Latviešu",
101 "nb_NO" => "Norwegian bokmål",
104 "ru_RU" => "Русский",
105 "pt_BR" => "Portuguese/Brazil",
106 "pt_PT" => "Portuguese/Portugal",
107 "zh_CN" => "Simplified Chinese",
108 "zh_TW" => "Traditional Chinese",
109 "sv_SE" => "Svenska",
111 "tr_TR" => "Türkçe");
116 require_once "lib/accept-to-gettext.php";
117 require_once "lib/gettext/gettext.inc";
119 function startup_gettext() {
121 # Get locale from Accept-Language header
122 $lang = al2gt(array_keys(get_translations()), "text/html");
124 if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
125 $lang = _TRANSLATION_OVERRIDE_DEFAULT
;
128 if ($_SESSION["uid"] && get_schema_version() >= 120) {
129 $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
131 if ($pref_lang && $pref_lang != 'auto') {
137 if (defined('LC_MESSAGES')) {
138 _setlocale(LC_MESSAGES
, $lang);
139 } else if (defined('LC_ALL')) {
140 _setlocale(LC_ALL
, $lang);
143 _bindtextdomain("messages", "locale");
145 _textdomain("messages");
146 _bind_textdomain_codeset("messages", "UTF-8");
150 require_once 'db-prefs.php';
151 require_once 'version.php';
152 require_once 'controls.php';
154 define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION
. ' (http://tt-rss.org/)');
155 ini_set('user_agent', SELF_USER_AGENT
);
157 $schema_version = false;
159 function _debug_suppress($suppress) {
160 global $suppress_debugging;
162 $suppress_debugging = $suppress;
166 * Print a timestamped debug message.
168 * @param string $msg The debug message.
171 function _debug($msg, $show = true) {
172 global $suppress_debugging;
174 //echo "[$suppress_debugging] $msg $show\n";
176 if ($suppress_debugging) return false;
178 $ts = strftime("%H:%M:%S", time());
179 if (function_exists('posix_getpid')) {
180 $ts = "$ts/" . posix_getpid();
183 if ($show && !(defined('QUIET') && QUIET
)) {
184 print "[$ts] $msg\n";
187 if (defined('LOGFILE')) {
188 $fp = fopen(LOGFILE
, 'a+');
193 if (function_exists("flock")) {
196 // try to lock logfile for writing
197 while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB
)) {
208 fputs($fp, "[$ts] $msg\n");
210 if (function_exists("flock")) {
221 * Purge a feed old posts.
223 * @param mixed $link A database connection.
224 * @param mixed $feed_id The id of the purged feed.
225 * @param mixed $purge_interval Olderness of purged posts.
226 * @param boolean $debug Set to True to enable the debug. False by default.
230 function purge_feed($feed_id, $purge_interval, $debug = false) {
232 if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
236 $sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
237 $sth->execute([$feed_id]);
241 if ($row = $sth->fetch()) {
242 $owner_uid = $row["owner_uid"];
245 if ($purge_interval == -1 ||
!$purge_interval) {
247 CCache
::update($feed_id, $owner_uid);
252 if (!$owner_uid) return;
254 if (FORCE_ARTICLE_PURGE
== 0) {
255 $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
258 $purge_unread = true;
259 $purge_interval = FORCE_ARTICLE_PURGE
;
263 $query_limit = " unread = false AND ";
267 $purge_interval = (int) $purge_interval;
269 if (DB_TYPE
== "pgsql") {
270 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
272 WHERE ttrss_entries.id = ref_id AND
276 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
277 $sth->execute([$feed_id]);
280 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
281 USING ttrss_user_entries, ttrss_entries
282 WHERE ttrss_entries.id = ref_id AND
286 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
287 $sth->execute([$feed_id]);
291 $rows = $sth->rowCount();
293 CCache
::update($feed_id, $owner_uid);
296 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
300 } // function purge_feed
302 function feed_purge_interval($feed_id) {
306 $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
308 $sth->execute([$feed_id]);
310 if ($row = $sth->fetch()) {
311 $purge_interval = $row["purge_interval"];
312 $owner_uid = $row["owner_uid"];
314 if ($purge_interval == 0) $purge_interval = get_pref(
315 'PURGE_OLD_DAYS', $owner_uid);
317 return $purge_interval;
324 // TODO: max_size currently only works for CURL transfers
325 // TODO: multiple-argument way is deprecated, first parameter is a hash now
326 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
327 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
329 global $fetch_last_error;
330 global $fetch_last_error_code;
331 global $fetch_last_error_content;
332 global $fetch_last_content_type;
333 global $fetch_last_modified;
334 global $fetch_effective_url;
335 global $fetch_curl_used;
337 $fetch_last_error = false;
338 $fetch_last_error_code = -1;
339 $fetch_last_error_content = "";
340 $fetch_last_content_type = "";
341 $fetch_curl_used = false;
342 $fetch_last_modified = "";
343 $fetch_effective_url = "";
345 if (!is_array($options)) {
347 // falling back on compatibility shim
348 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
351 for ($i = 0; $i < func_num_args(); $i++
) {
352 $tmp[$option_names[$i]] = func_get_arg($i);
358 "url" => func_get_arg(0),
359 "type" => @func_get_arg(1),
360 "login" => @func_get_arg(2),
361 "pass" => @func_get_arg(3),
362 "post_query" => @func_get_arg(4),
363 "timeout" => @func_get_arg(5),
364 "timestamp" => @func_get_arg(6),
365 "useragent" => @func_get_arg(7)
369 $url = $options["url"];
370 $type = isset($options["type"]) ?
$options["type"] : false;
371 $login = isset($options["login"]) ?
$options["login"] : false;
372 $pass = isset($options["pass"]) ?
$options["pass"] : false;
373 $post_query = isset($options["post_query"]) ?
$options["post_query"] : false;
374 $timeout = isset($options["timeout"]) ?
$options["timeout"] : false;
375 $last_modified = isset($options["last_modified"]) ?
$options["last_modified"] : "";
376 $useragent = isset($options["useragent"]) ?
$options["useragent"] : false;
377 $followlocation = isset($options["followlocation"]) ?
$options["followlocation"] : true;
378 $max_size = isset($options["max_size"]) ?
$options["max_size"] : MAX_DOWNLOAD_FILE_SIZE
; // in bytes
379 $http_accept = isset($options["http_accept"]) ?
$options["http_accept"] : false;
381 $url = ltrim($url, ' ');
382 $url = str_replace(' ', '%20', $url);
384 if (strpos($url, "//") === 0)
385 $url = 'http:' . $url;
387 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
389 $fetch_curl_used = true;
391 $ch = curl_init($url);
393 $curl_http_headers = [];
395 if ($last_modified && !$post_query)
396 array_push($curl_http_headers, "If-Modified-Since: $last_modified");
399 array_push($curl_http_headers, "Accept: " . $http_accept);
401 if (count($curl_http_headers) > 0)
402 curl_setopt($ch, CURLOPT_HTTPHEADER
, $curl_http_headers);
404 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
405 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
406 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
407 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
408 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
409 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
410 curl_setopt($ch, CURLOPT_HEADER
, true);
411 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
412 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
414 curl_setopt($ch, CURLOPT_ENCODING
, "");
415 //curl_setopt($ch, CURLOPT_REFERER, $url);
418 curl_setopt($ch, CURLOPT_NOPROGRESS
, false);
419 curl_setopt($ch, CURLOPT_BUFFERSIZE
, 16384); // needed to get 5 arguments in progress function?
421 // holy shit closures in php
422 // download & upload are *expected* sizes respectively, could be zero
423 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION
, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use( &$max_size) {
424 //_debug("[curl progressfunction] $downloaded $max_size");
426 return ($downloaded > $max_size) ?
1 : 0; // if max size is set, abort when exceeding it
431 if (!ini_get("open_basedir")) {
432 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
435 if (defined('_HTTP_PROXY')) {
436 curl_setopt($ch, CURLOPT_PROXY
, _HTTP_PROXY
);
440 curl_setopt($ch, CURLOPT_POST
, true);
441 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
445 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
447 $ret = @curl_exec
($ch);
449 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE
);
450 $headers = explode("\r\n", substr($ret, 0, $headers_length));
451 $contents = substr($ret, $headers_length);
453 foreach ($headers as $header) {
454 if (strstr($header, ": ") !== FALSE) {
455 list ($key, $value) = explode(": ", $header);
457 if (strtolower($key) == "last-modified") {
458 $fetch_last_modified = $value;
462 if (substr(strtolower($header), 0, 7) == 'http/1.') {
463 $fetch_last_error_code = (int) substr($header, 9, 3);
464 $fetch_last_error = $header;
468 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
469 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
470 $contents = @curl_exec
($ch);
473 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
474 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
476 $fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL
);
478 $fetch_last_error_code = $http_code;
480 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
482 if (curl_errno($ch) != 0) {
483 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
486 $fetch_last_error_content = $contents;
492 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
502 $fetch_curl_used = false;
504 if ($login && $pass){
505 $url_parts = array();
507 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
509 $pass = urlencode($pass);
511 if ($url_parts[1] && $url_parts[2]) {
512 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
516 // TODO: should this support POST requests or not? idk
518 $context_options = array(
524 'ignore_errors' => true,
525 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
526 'protocol_version'=> 1.1)
529 if (!$post_query && $last_modified)
530 array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
533 array_push($context_options['http']['header'], "Accept: $http_accept");
535 if (defined('_HTTP_PROXY')) {
536 $context_options['http']['request_fulluri'] = true;
537 $context_options['http']['proxy'] = _HTTP_PROXY
;
540 $context = stream_context_create($context_options);
542 $old_error = error_get_last();
544 $fetch_effective_url = $url;
546 $data = @file_get_contents
($url, false, $context);
548 if (isset($http_response_header) && is_array($http_response_header)) {
549 foreach ($http_response_header as $header) {
550 if (strstr($header, ": ") !== FALSE) {
551 list ($key, $value) = explode(": ", $header);
553 $key = strtolower($key);
555 if ($key == 'content-type') {
556 $fetch_last_content_type = $value;
557 // don't abort here b/c there might be more than one
558 // e.g. if we were being redirected -- last one is the right one
559 } else if ($key == 'last-modified') {
560 $fetch_last_modified = $value;
561 } else if ($key == 'location') {
562 $fetch_effective_url = $value;
566 if (substr(strtolower($header), 0, 7) == 'http/1.') {
567 $fetch_last_error_code = (int) substr($header, 9, 3);
568 $fetch_last_error = $header;
573 if ($fetch_last_error_code != 200) {
574 $error = error_get_last();
576 if ($error['message'] != $old_error['message']) {
577 $fetch_last_error .= "; " . $error["message"];
580 $fetch_last_error_content = $data;
590 * Try to determine the favicon URL for a feed.
591 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
592 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
594 * @param string $url A feed or page URL
596 * @return mixed The favicon URL, or false if none was found.
598 function get_favicon_url($url) {
600 $favicon_url = false;
602 if ($html = @fetch_file_contents
($url)) {
604 libxml_use_internal_errors(true);
606 $doc = new DOMDocument();
607 $doc->loadHTML($html);
608 $xpath = new DOMXPath($doc);
610 $base = $xpath->query('/html/head/base[@href]');
611 foreach ($base as $b) {
612 $url = rewrite_relative_url($url, $b->getAttribute("href"));
616 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
617 if (count($entries) > 0) {
618 foreach ($entries as $entry) {
619 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
626 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
629 } // function get_favicon_url
631 function initialize_user_prefs($uid, $profile = false) {
633 if (get_schema_version() < 63) $profile_qpart = "";
636 $in_nested_tr = false;
639 $pdo->beginTransaction();
640 } catch (Exception
$e) {
641 $in_nested_tr = true;
644 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
646 $profile = $profile ?
$profile : null;
648 $u_sth = $pdo->prepare("SELECT pref_name
649 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
650 (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
651 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
653 $active_prefs = array();
655 while ($line = $u_sth->fetch()) {
656 array_push($active_prefs, $line["pref_name"]);
659 while ($line = $sth->fetch()) {
660 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
661 // print "adding " . $line["pref_name"] . "<br>";
663 if (get_schema_version() < 63) {
664 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
665 (owner_uid,pref_name,value) VALUES
667 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
670 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
671 (owner_uid,pref_name,value, profile) VALUES
673 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
679 if (!$in_nested_tr) $pdo->commit();
683 function get_ssl_certificate_id() {
684 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
685 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
686 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
687 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
688 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
690 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
691 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
692 $_SERVER["SSL_CLIENT_V_START"] .
693 $_SERVER["SSL_CLIENT_V_END"] .
694 $_SERVER["SSL_CLIENT_S_DN"]);
699 function authenticate_user($login, $password, $check_only = false) {
701 if (!SINGLE_USER_MODE
) {
704 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
706 $user_id = (int) $plugin->authenticate($login, $password);
709 $_SESSION["auth_module"] = strtolower(get_class($plugin));
714 if ($user_id && !$check_only) {
717 $_SESSION["uid"] = $user_id;
718 $_SESSION["version"] = VERSION_STATIC
;
721 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
723 $sth->execute([$user_id]);
724 $row = $sth->fetch();
726 $_SESSION["name"] = $row["login"];
727 $_SESSION["access_level"] = $row["access_level"];
728 $_SESSION["csrf_token"] = uniqid_short();
730 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
731 $usth->execute([$user_id]);
733 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
734 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
735 $_SESSION["pwd_hash"] = $row["pwd_hash"];
737 $_SESSION["last_version_check"] = time();
739 initialize_user_prefs($_SESSION["uid"]);
748 $_SESSION["uid"] = 1;
749 $_SESSION["name"] = "admin";
750 $_SESSION["access_level"] = 10;
752 $_SESSION["hide_hello"] = true;
753 $_SESSION["hide_logout"] = true;
755 $_SESSION["auth_module"] = false;
757 if (!$_SESSION["csrf_token"]) {
758 $_SESSION["csrf_token"] = uniqid_short();
761 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
763 initialize_user_prefs($_SESSION["uid"]);
769 // this is used for user http parameters unless HTML code is actually needed
770 function clean($param) {
771 if (is_array($param)) {
772 return array_map("strip_tags", $param);
773 } else if (is_string($param)) {
774 return strip_tags($param);
780 function make_password($length = 8) {
783 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
787 while ($i < $length) {
788 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
790 if (!strstr($password, $char)) {
798 // this is called after user is created to initialize default feeds, labels
801 // user preferences are checked on every login, not here
803 function initialize_user($uid) {
807 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
808 values (?, 'Tiny Tiny RSS: Forum',
809 'http://tt-rss.org/forum/rss.php')");
810 $sth->execute([$uid]);
813 function logout_user() {
815 if (isset($_COOKIE[session_name()])) {
816 setcookie(session_name(), '', time()-42000, '/');
820 function validate_csrf($csrf_token) {
821 return $csrf_token == $_SESSION['csrf_token'];
824 function load_user_plugins($owner_uid, $pluginhost = false) {
826 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
828 if ($owner_uid && SCHEMA_VERSION
>= 100) {
829 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
831 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
833 if (get_schema_version() > 100) {
834 $pluginhost->load_data();
839 function login_sequence() {
842 if (SINGLE_USER_MODE
) {
844 authenticate_user("admin", null);
846 load_user_plugins($_SESSION["uid"]);
848 if (!validate_session()) $_SESSION["uid"] = false;
850 if (!$_SESSION["uid"]) {
852 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
853 $_SESSION["ref_schema_version"] = get_schema_version(true);
855 authenticate_user(null, null, true);
858 if (!$_SESSION["uid"]) {
860 setcookie(session_name(), '', time()-42000, '/');
867 /* bump login timestamp */
868 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
869 $sth->execute([$_SESSION['uid']]);
871 $_SESSION["last_login_update"] = time();
874 if ($_SESSION["uid"]) {
876 load_user_plugins($_SESSION["uid"]);
880 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
882 (SELECT COUNT(id) FROM ttrss_feeds WHERE
883 ttrss_feeds.id = feed_id) = 0");
885 $sth->execute([$_SESSION['uid']]);
887 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
889 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
890 ttrss_feed_categories.id = feed_id) = 0");
892 $sth->execute([$_SESSION['uid']]);
898 function truncate_string($str, $max_len, $suffix = '…') {
899 if (mb_strlen($str, "utf-8") > $max_len) {
900 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
907 function truncate_middle($str, $max_len, $suffix = '…') {
908 if (strlen($str) > $max_len) {
909 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
915 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
918 $source_tz = new DateTimeZone($source_tz);
919 } catch (Exception
$e) {
920 $source_tz = new DateTimeZone('UTC');
924 $dest_tz = new DateTimeZone($dest_tz);
925 } catch (Exception
$e) {
926 $dest_tz = new DateTimeZone('UTC');
929 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
930 return $dt->format('U') +
$dest_tz->getOffset($dt);
933 function make_local_datetime($timestamp, $long, $owner_uid = false,
934 $no_smart_dt = false, $eta_min = false) {
936 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
937 if (!$timestamp) $timestamp = '1970-01-01 0:00';
942 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
944 $timestamp = substr($timestamp, 0, 19);
946 # We store date in UTC internally
947 $dt = new DateTime($timestamp, $utc_tz);
949 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
951 if ($user_tz_string != 'Automatic') {
954 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
955 } catch (Exception
$e) {
959 $tz_offset = $user_tz->getOffset($dt);
961 $tz_offset = (int) -$_SESSION["clientTzOffset"];
964 $user_timestamp = $dt->format('U') +
$tz_offset;
967 return smart_date_time($user_timestamp,
968 $tz_offset, $owner_uid, $eta_min);
971 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
973 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
975 return date($format, $user_timestamp);
979 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
980 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
982 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
983 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
984 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
985 return date("G:i", $timestamp);
986 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
987 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
988 return date($format, $timestamp);
990 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
991 return date($format, $timestamp);
995 function sql_bool_to_bool($s) {
996 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
999 function bool_to_sql_bool($s) {
1003 // Session caching removed due to causing wrong redirects to upgrade
1004 // script when get_schema_version() is called on an obsolete session
1005 // created on a previous schema version.
1006 function get_schema_version($nocache = false) {
1007 global $schema_version;
1011 if (!$schema_version && !$nocache) {
1012 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
1013 $version = $row["schema_version"];
1014 $schema_version = $version;
1017 return $schema_version;
1021 function sanity_check() {
1022 require_once 'errors.php';
1026 $schema_version = get_schema_version(true);
1028 if ($schema_version != SCHEMA_VERSION
) {
1032 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1035 function file_is_locked($filename) {
1036 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
1037 if (function_exists('flock')) {
1038 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1040 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1041 flock($fp, LOCK_UN
);
1051 return true; // consider the file always locked and skip the test
1058 function make_lockfile($filename) {
1059 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1061 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1062 $stat_h = fstat($fp);
1063 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1065 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1066 if ($stat_h["ino"] != $stat_f["ino"] ||
1067 $stat_h["dev"] != $stat_f["dev"]) {
1073 if (function_exists('posix_getpid')) {
1074 fwrite($fp, posix_getpid() . "\n");
1082 function make_stampfile($filename) {
1083 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1085 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1086 fwrite($fp, time() . "\n");
1087 flock($fp, LOCK_UN
);
1095 function sql_random_function() {
1096 if (DB_TYPE
== "mysql") {
1103 function getFeedUnread($feed, $is_cat = false) {
1104 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1107 function checkbox_to_sql_bool($val) {
1108 return ($val == "on") ?
1 : 0;
1111 function uniqid_short() {
1112 return uniqid(base_convert(rand(), 10, 36));
1115 function make_init_params() {
1118 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1119 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1120 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1121 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1123 $params[strtolower($param)] = (int) get_pref($param);
1126 $params["icons_url"] = ICONS_URL
;
1127 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1128 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1129 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1130 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1131 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1132 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1133 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1135 $theme = get_pref( "USER_CSS_THEME", false, false);
1136 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1138 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1140 $params["php_platform"] = PHP_OS
;
1141 $params["php_version"] = PHP_VERSION
;
1143 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1147 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1148 ttrss_feeds WHERE owner_uid = ?");
1149 $sth->execute([$_SESSION['uid']]);
1150 $row = $sth->fetch();
1152 $max_feed_id = $row["mid"];
1153 $num_feeds = $row["nf"];
1155 $params["max_feed_id"] = (int) $max_feed_id;
1156 $params["num_feeds"] = (int) $num_feeds;
1158 $params["hotkeys"] = get_hotkeys_map();
1160 $params["csrf_token"] = $_SESSION["csrf_token"];
1161 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1163 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1165 $params["icon_alert"] = base64_img("images/alert.png");
1166 $params["icon_information"] = base64_img("images/information.png");
1167 $params["icon_cross"] = base64_img("images/cross.png");
1168 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1170 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1175 function get_hotkeys_info() {
1177 __("Navigation") => array(
1178 "next_feed" => __("Open next feed"),
1179 "prev_feed" => __("Open previous feed"),
1180 "next_article" => __("Open next article"),
1181 "prev_article" => __("Open previous article"),
1182 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1183 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1184 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1185 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1186 "search_dialog" => __("Show search dialog")),
1187 __("Article") => array(
1188 "toggle_mark" => __("Toggle starred"),
1189 "toggle_publ" => __("Toggle published"),
1190 "toggle_unread" => __("Toggle unread"),
1191 "edit_tags" => __("Edit tags"),
1192 "open_in_new_window" => __("Open in new window"),
1193 "catchup_below" => __("Mark below as read"),
1194 "catchup_above" => __("Mark above as read"),
1195 "article_scroll_down" => __("Scroll down"),
1196 "article_scroll_up" => __("Scroll up"),
1197 "select_article_cursor" => __("Select article under cursor"),
1198 "email_article" => __("Email article"),
1199 "close_article" => __("Close/collapse article"),
1200 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1201 "toggle_widescreen" => __("Toggle widescreen mode"),
1202 "toggle_embed_original" => __("Toggle embed original")),
1203 __("Article selection") => array(
1204 "select_all" => __("Select all articles"),
1205 "select_unread" => __("Select unread"),
1206 "select_marked" => __("Select starred"),
1207 "select_published" => __("Select published"),
1208 "select_invert" => __("Invert selection"),
1209 "select_none" => __("Deselect everything")),
1210 __("Feed") => array(
1211 "feed_refresh" => __("Refresh current feed"),
1212 "feed_unhide_read" => __("Un/hide read feeds"),
1213 "feed_subscribe" => __("Subscribe to feed"),
1214 "feed_edit" => __("Edit feed"),
1215 "feed_catchup" => __("Mark as read"),
1216 "feed_reverse" => __("Reverse headlines"),
1217 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1218 "feed_debug_update" => __("Debug feed update"),
1219 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1220 "catchup_all" => __("Mark all feeds as read"),
1221 "cat_toggle_collapse" => __("Un/collapse current category"),
1222 "toggle_combined_mode" => __("Toggle combined mode"),
1223 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1224 __("Go to") => array(
1225 "goto_all" => __("All articles"),
1226 "goto_fresh" => __("Fresh"),
1227 "goto_marked" => __("Starred"),
1228 "goto_published" => __("Published"),
1229 "goto_tagcloud" => __("Tag cloud"),
1230 "goto_prefs" => __("Preferences")),
1231 __("Other") => array(
1232 "create_label" => __("Create label"),
1233 "create_filter" => __("Create filter"),
1234 "collapse_sidebar" => __("Un/collapse sidebar"),
1235 "help_dialog" => __("Show help dialog"))
1238 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1239 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1245 function get_hotkeys_map() {
1247 // "navigation" => array(
1250 "n" => "next_article",
1251 "p" => "prev_article",
1252 "(38)|up" => "prev_article",
1253 "(40)|down" => "next_article",
1254 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1255 // "^(40)|Ctrl-down" => "next_article_noscroll",
1256 "(191)|/" => "search_dialog",
1257 // "article" => array(
1258 "s" => "toggle_mark",
1259 "*s" => "toggle_publ",
1260 "u" => "toggle_unread",
1261 "*t" => "edit_tags",
1262 "o" => "open_in_new_window",
1263 "c p" => "catchup_below",
1264 "c n" => "catchup_above",
1265 "*n" => "article_scroll_down",
1266 "*p" => "article_scroll_up",
1267 "*(38)|Shift+up" => "article_scroll_up",
1268 "*(40)|Shift+down" => "article_scroll_down",
1269 "a *w" => "toggle_widescreen",
1270 "a e" => "toggle_embed_original",
1271 "e" => "email_article",
1272 "a q" => "close_article",
1273 // "article_selection" => array(
1274 "a a" => "select_all",
1275 "a u" => "select_unread",
1276 "a *u" => "select_marked",
1277 "a p" => "select_published",
1278 "a i" => "select_invert",
1279 "a n" => "select_none",
1281 "f r" => "feed_refresh",
1282 "f a" => "feed_unhide_read",
1283 "f s" => "feed_subscribe",
1284 "f e" => "feed_edit",
1285 "f q" => "feed_catchup",
1286 "f x" => "feed_reverse",
1287 "f g" => "feed_toggle_vgroup",
1288 "f *d" => "feed_debug_update",
1289 "f *g" => "feed_debug_viewfeed",
1290 "f *c" => "toggle_combined_mode",
1291 "f c" => "toggle_cdm_expanded",
1292 "*q" => "catchup_all",
1293 "x" => "cat_toggle_collapse",
1295 "g a" => "goto_all",
1296 "g f" => "goto_fresh",
1297 "g s" => "goto_marked",
1298 "g p" => "goto_published",
1299 "g t" => "goto_tagcloud",
1300 "g *p" => "goto_prefs",
1301 // "other" => array(
1302 "(9)|Tab" => "select_article_cursor", // tab
1303 "c l" => "create_label",
1304 "c f" => "create_filter",
1305 "c s" => "collapse_sidebar",
1306 "^(191)|Ctrl+/" => "help_dialog",
1309 if (get_pref('COMBINED_DISPLAY_MODE')) {
1310 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1311 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1314 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1315 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1318 $prefixes = array();
1320 foreach (array_keys($hotkeys) as $hotkey) {
1321 $pair = explode(" ", $hotkey, 2);
1323 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1324 array_push($prefixes, $pair[0]);
1328 return array($prefixes, $hotkeys);
1331 function check_for_update() {
1332 if (defined("GIT_VERSION_TIMESTAMP")) {
1333 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1336 $content = json_decode($content, true);
1338 if ($content && isset($content["changeset"])) {
1339 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1340 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1342 return $content["changeset"]["id"];
1351 function make_runtime_info($disable_update_check = false) {
1356 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1357 ttrss_feeds WHERE owner_uid = ?");
1358 $sth->execute([$_SESSION['uid']]);
1359 $row = $sth->fetch();
1361 $max_feed_id = $row['mid'];
1362 $num_feeds = $row['nf'];
1364 $data["max_feed_id"] = (int) $max_feed_id;
1365 $data["num_feeds"] = (int) $num_feeds;
1367 $data['last_article_id'] = Article
::getLastArticleId();
1368 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1370 $data['dep_ts'] = calculate_dep_timestamp();
1371 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1373 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1375 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1376 $update_result = @check_for_update
();
1378 $data["update_result"] = $update_result;
1380 $_SESSION["last_version_check"] = time();
1383 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1385 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1387 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1389 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1392 $stamp_delta = time() - $stamp;
1394 if ($stamp_delta > 1800) {
1398 $_SESSION["daemon_stamp_check"] = time();
1401 $data['daemon_stamp_ok'] = $stamp_check;
1403 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1405 $data['daemon_stamp'] = $stamp_fmt;
1413 function search_to_sql($search, $search_language) {
1415 $keywords = str_getcsv(trim($search), " ");
1416 $query_keywords = array();
1417 $search_words = array();
1418 $search_query_leftover = array();
1422 if ($search_language)
1423 $search_language = $pdo->quote(mb_strtolower($search_language));
1425 $search_language = $pdo->quote("english");
1427 foreach ($keywords as $k) {
1428 if (strpos($k, "-") === 0) {
1435 $commandpair = explode(":", mb_strtolower($k), 2);
1437 switch ($commandpair[0]) {
1439 if ($commandpair[1]) {
1440 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1441 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1443 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1444 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1445 array_push($search_words, $k);
1449 if ($commandpair[1]) {
1450 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1451 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1453 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1454 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1455 array_push($search_words, $k);
1459 if ($commandpair[1]) {
1460 if ($commandpair[1] == "true")
1461 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1462 else if ($commandpair[1] == "false")
1463 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1465 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1466 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1468 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1469 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1470 if (!$not) array_push($search_words, $k);
1475 if ($commandpair[1]) {
1476 if ($commandpair[1] == "true")
1477 array_push($query_keywords, "($not (marked = true))");
1479 array_push($query_keywords, "($not (marked = false))");
1481 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1482 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1483 if (!$not) array_push($search_words, $k);
1487 if ($commandpair[1]) {
1488 if ($commandpair[1] == "true")
1489 array_push($query_keywords, "($not (published = true))");
1491 array_push($query_keywords, "($not (published = false))");
1494 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1495 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1496 if (!$not) array_push($search_words, $k);
1500 if ($commandpair[1]) {
1501 if ($commandpair[1] == "true")
1502 array_push($query_keywords, "($not (unread = true))");
1504 array_push($query_keywords, "($not (unread = false))");
1507 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1508 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1509 if (!$not) array_push($search_words, $k);
1513 if (strpos($k, "@") === 0) {
1515 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1516 $orig_ts = strtotime(substr($k, 1));
1517 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1519 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1521 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1524 if (DB_TYPE
== "pgsql") {
1525 $k = mb_strtolower($k);
1526 array_push($search_query_leftover, $not ?
"!$k" : $k);
1528 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1529 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1532 if (!$not) array_push($search_words, $k);
1537 if (count($search_query_leftover) > 0) {
1538 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1540 if (DB_TYPE
== "pgsql") {
1541 array_push($query_keywords,
1542 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1547 $search_query_part = implode("AND", $query_keywords);
1549 return array($search_query_part, $search_words);
1552 function iframe_whitelisted($entry) {
1553 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1555 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1558 foreach ($whitelist as $w) {
1559 if ($src == $w ||
$src == "www.$w")
1567 // check for locally cached (media) URLs and rewrite to local versions
1568 // this is called separately after sanitize() and plugin render article hooks to allow
1569 // plugins work on original source URLs used before caching
1571 function rewrite_cached_urls($str) {
1572 $charset_hack = '<head>
1573 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1576 $res = trim($str); if (!$res) return '';
1578 $doc = new DOMDocument();
1579 $doc->loadHTML($charset_hack . $res);
1580 $xpath = new DOMXPath($doc);
1582 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
1584 $need_saving = false;
1586 foreach ($entries as $entry) {
1588 if ($entry->hasAttribute('src') ||
$entry->hasAttribute('poster')) {
1590 // should be already absolutized because this is called after sanitize()
1591 $src = $entry->hasAttribute('poster') ?
$entry->getAttribute('poster') : $entry->getAttribute('src');
1592 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1594 if (file_exists($cached_filename)) {
1596 // this is strictly cosmetic
1597 if ($entry->tagName
== 'img') {
1599 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1601 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1607 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1609 if ($entry->hasAttribute('poster'))
1610 $entry->setAttribute('poster', $src);
1612 $entry->setAttribute('src', $src);
1614 $need_saving = true;
1620 $doc->removeChild($doc->firstChild
); //remove doctype
1621 $res = $doc->saveHTML();
1627 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1628 if (!$owner) $owner = $_SESSION["uid"];
1630 $res = trim($str); if (!$res) return '';
1632 $charset_hack = '<head>
1633 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1636 $res = trim($res); if (!$res) return '';
1638 libxml_use_internal_errors(true);
1640 $doc = new DOMDocument();
1641 $doc->loadHTML($charset_hack . $res);
1642 $xpath = new DOMXPath($doc);
1644 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1646 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1648 foreach ($entries as $entry) {
1650 if ($entry->hasAttribute('href')) {
1651 $entry->setAttribute('href',
1652 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1654 $entry->setAttribute('rel', 'noopener noreferrer');
1657 if ($entry->hasAttribute('src')) {
1658 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1660 // cache stuff has gone to rewrite_cached_urls()
1662 $entry->setAttribute('src', $src);
1665 if ($entry->nodeName
== 'img') {
1666 $entry->setAttribute('referrerpolicy', 'no-referrer');
1668 $entry->removeAttribute('width');
1669 $entry->removeAttribute('height');
1671 if ($entry->hasAttribute('src')) {
1672 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1674 if (is_prefix_https() && !$is_https_url) {
1676 if ($entry->hasAttribute('srcset')) {
1677 $entry->removeAttribute('srcset');
1680 if ($entry->hasAttribute('sizes')) {
1681 $entry->removeAttribute('sizes');
1687 if ($entry->hasAttribute('src') &&
1688 ($owner && get_pref("STRIP_IMAGES", $owner)) ||
$force_remove_images ||
$_SESSION["bw_limit"]) {
1690 $p = $doc->createElement('p');
1692 $a = $doc->createElement('a');
1693 $a->setAttribute('href', $entry->getAttribute('src'));
1695 $a->appendChild(new DOMText($entry->getAttribute('src')));
1696 $a->setAttribute('target', '_blank');
1697 $a->setAttribute('rel', 'noopener noreferrer');
1699 $p->appendChild($a);
1701 if ($entry->nodeName
== 'source') {
1703 if ($entry->parentNode
&& $entry->parentNode
->parentNode
)
1704 $entry->parentNode
->parentNode
->replaceChild($p, $entry->parentNode
);
1706 } else if ($entry->nodeName
== 'img') {
1708 if ($entry->parentNode
)
1709 $entry->parentNode
->replaceChild($p, $entry);
1714 if (strtolower($entry->nodeName
) == "a") {
1715 $entry->setAttribute("target", "_blank");
1716 $entry->setAttribute("rel", "noopener noreferrer");
1720 $entries = $xpath->query('//iframe');
1721 foreach ($entries as $entry) {
1722 if (!iframe_whitelisted($entry)) {
1723 $entry->setAttribute('sandbox', 'allow-scripts');
1725 if (is_prefix_https()) {
1726 $entry->setAttribute("src",
1727 str_replace("http://", "https://",
1728 $entry->getAttribute("src")));
1733 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1734 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1735 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1736 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1737 'dt', 'em', 'footer', 'figure', 'figcaption',
1738 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1739 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1740 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1741 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1742 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1743 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1745 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1747 $disallowed_attributes = array('id', 'style', 'class');
1749 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1750 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1751 if (is_array($retval)) {
1753 $allowed_elements = $retval[1];
1754 $disallowed_attributes = $retval[2];
1760 $doc->removeChild($doc->firstChild
); //remove doctype
1761 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1763 if ($highlight_words) {
1764 foreach ($highlight_words as $word) {
1766 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1768 $elements = $xpath->query("//*/text()");
1770 foreach ($elements as $child) {
1772 $fragment = $doc->createDocumentFragment();
1773 $text = $child->textContent
;
1775 while (($pos = mb_stripos($text, $word)) !== false) {
1776 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1777 $word = mb_substr($text, $pos, mb_strlen($word));
1778 $highlight = $doc->createElement('span');
1779 $highlight->appendChild(new DomText($word));
1780 $highlight->setAttribute('class', 'highlight');
1781 $fragment->appendChild($highlight);
1782 $text = mb_substr($text, $pos +
mb_strlen($word));
1785 if (!empty($text)) $fragment->appendChild(new DomText($text));
1787 $child->parentNode
->replaceChild($fragment, $child);
1792 $res = $doc->saveHTML();
1794 /* strip everything outside of <body>...</body> */
1796 $res_frag = array();
1797 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1798 return $res_frag[1];
1804 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1805 $xpath = new DOMXPath($doc);
1806 $entries = $xpath->query('//*');
1808 foreach ($entries as $entry) {
1809 if (!in_array($entry->nodeName
, $allowed_elements)) {
1810 $entry->parentNode
->removeChild($entry);
1813 if ($entry->hasAttributes()) {
1814 $attrs_to_remove = array();
1816 foreach ($entry->attributes
as $attr) {
1818 if (strpos($attr->nodeName
, 'on') === 0) {
1819 array_push($attrs_to_remove, $attr);
1822 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1823 array_push($attrs_to_remove, $attr);
1826 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1827 array_push($attrs_to_remove, $attr);
1831 foreach ($attrs_to_remove as $attr) {
1832 $entry->removeAttributeNode($attr);
1840 function trim_array($array) {
1842 array_walk($tmp, 'trim');
1846 function tag_is_valid($tag) {
1847 if (!$tag ||
is_numeric($tag) ||
mb_strlen($tag) > 250)
1853 function render_login_form() {
1854 header('Cache-Control: public');
1856 require_once "login_form.php";
1860 function T_sprintf() {
1861 $args = func_get_args();
1862 return vsprintf(__(array_shift($args)), $args);
1865 function print_checkpoint($n, $s) {
1866 $ts = microtime(true);
1867 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1871 function sanitize_tag($tag) {
1874 $tag = mb_strtolower($tag, 'utf-8');
1876 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1878 if (DB_TYPE
== "mysql") {
1879 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1885 function is_server_https() {
1886 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1889 function is_prefix_https() {
1890 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1893 // this returns SELF_URL_PATH sans ending slash
1894 function get_self_url_prefix() {
1895 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1896 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1898 return SELF_URL_PATH
;
1902 function encrypt_password($pass, $salt = '', $mode2 = false) {
1903 if ($salt && $mode2) {
1904 return "MODE2:" . hash('sha256', $salt . $pass);
1906 return "SHA1X:" . sha1("$salt:$pass");
1908 return "SHA1:" . sha1($pass);
1910 } // function encrypt_password
1912 function load_filters($feed_id, $owner_uid) {
1915 $feed_id = (int) $feed_id;
1916 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1919 $null_cat_qpart = "cat_id IS NULL OR";
1921 $null_cat_qpart = "";
1925 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1926 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1927 $sth->execute([$owner_uid]);
1929 $check_cats = array_merge(
1930 Feeds
::getParentCategories($cat_id, $owner_uid),
1933 $check_cats_str = join(",", $check_cats);
1934 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1936 while ($line = $sth->fetch()) {
1937 $filter_id = $line["id"];
1939 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1941 $sth2 = $pdo->prepare("SELECT
1942 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1943 FROM ttrss_filters2_rules AS r,
1944 ttrss_filter_types AS t
1946 (match_on IS NOT NULL OR
1947 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1948 (feed_id IS NULL OR feed_id = ?))) AND
1949 filter_type = t.id AND filter_id = ?");
1950 $sth2->execute([$feed_id, $filter_id]);
1955 while ($rule_line = $sth2->fetch()) {
1956 # print_r($rule_line);
1958 if ($rule_line["match_on"]) {
1959 $match_on = json_decode($rule_line["match_on"], true);
1961 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1964 $rule["reg_exp"] = $rule_line["reg_exp"];
1965 $rule["type"] = $rule_line["type_name"];
1966 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1968 array_push($rules, $rule);
1969 } else if (!$match_any_rule) {
1970 // this filter contains a rule that doesn't match to this feed/category combination
1971 // thus filter has to be rejected
1980 $rule["reg_exp"] = $rule_line["reg_exp"];
1981 $rule["type"] = $rule_line["type_name"];
1982 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1984 array_push($rules, $rule);
1988 if (count($rules) > 0) {
1989 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1990 FROM ttrss_filters2_actions AS a,
1991 ttrss_filter_actions AS t
1993 action_id = t.id AND filter_id = ?");
1994 $sth2->execute([$filter_id]);
1996 while ($action_line = $sth2->fetch()) {
1997 # print_r($action_line);
2000 $action["type"] = $action_line["type_name"];
2001 $action["param"] = $action_line["action_param"];
2003 array_push($actions, $action);
2008 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
2009 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
2010 $filter["rules"] = $rules;
2011 $filter["actions"] = $actions;
2013 if (count($rules) > 0 && count($actions) > 0) {
2014 array_push($filters, $filter);
2021 function get_score_pic($score) {
2023 return "score_high.png";
2024 } else if ($score > 0) {
2025 return "score_half_high.png";
2026 } else if ($score < -100) {
2027 return "score_low.png";
2028 } else if ($score < 0) {
2029 return "score_half_low.png";
2031 return "score_neutral.png";
2035 function init_plugins() {
2036 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
2041 function add_feed_category($feed_cat, $parent_cat_id = false) {
2043 if (!$feed_cat) return false;
2045 $feed_cat = mb_substr($feed_cat, 0, 250);
2046 if (!$parent_cat_id) $parent_cat_id = null;
2049 $tr_in_progress = false;
2052 $pdo->beginTransaction();
2053 } catch (Exception
$e) {
2054 $tr_in_progress = true;
2057 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2058 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2059 AND title = :title AND owner_uid = :uid");
2060 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2062 if (!$sth->fetch()) {
2064 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2066 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2068 if (!$tr_in_progress) $pdo->commit();
2079 * Fixes incomplete URLs by prepending "http://".
2080 * Also replaces feed:// with http://, and
2081 * prepends a trailing slash if the url is a domain name only.
2083 * @param string $url Possibly incomplete URL
2085 * @return string Fixed URL.
2087 function fix_url($url) {
2089 // support schema-less urls
2090 if (strpos($url, '//') === 0) {
2091 $url = 'https:' . $url;
2094 if (strpos($url, '://') === false) {
2095 $url = 'http://' . $url;
2096 } else if (substr($url, 0, 5) == 'feed:') {
2097 $url = 'http:' . substr($url, 5);
2100 //prepend slash if the URL has no slash in it
2101 // "http://www.example" -> "http://www.example/"
2102 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2106 //convert IDNA hostname to punycode if possible
2107 if (function_exists("idn_to_ascii")) {
2108 $parts = parse_url($url);
2109 if (mb_detect_encoding($parts['host']) != 'ASCII')
2111 $parts['host'] = idn_to_ascii($parts['host']);
2112 $url = build_url($parts);
2116 if ($url != "http:///")
2122 function validate_feed_url($url) {
2123 $parts = parse_url($url);
2125 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2129 /* function save_email_address($email) {
2130 // FIXME: implement persistent storage of emails
2132 if (!$_SESSION['stored_emails'])
2133 $_SESSION['stored_emails'] = array();
2135 if (!in_array($email, $_SESSION['stored_emails']))
2136 array_push($_SESSION['stored_emails'], $email);
2140 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2142 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2144 $is_cat = bool_to_sql_bool($is_cat);
2148 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2149 WHERE feed_id = ? AND is_cat = ?
2150 AND owner_uid = ?");
2151 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2153 if ($row = $sth->fetch()) {
2154 return $row["access_key"];
2156 $key = uniqid_short();
2158 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2159 (access_key, feed_id, is_cat, owner_uid)
2160 VALUES (?, ?, ?, ?)");
2162 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2168 function get_feeds_from_html($url, $content)
2170 $url = fix_url($url);
2171 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2173 libxml_use_internal_errors(true);
2175 $doc = new DOMDocument();
2176 $doc->loadHTML($content);
2177 $xpath = new DOMXPath($doc);
2178 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2179 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2180 $feedUrls = array();
2181 foreach ($entries as $entry) {
2182 if ($entry->hasAttribute('href')) {
2183 $title = $entry->getAttribute('title');
2185 $title = $entry->getAttribute('type');
2187 $feedUrl = rewrite_relative_url(
2188 $baseUrl, $entry->getAttribute('href')
2190 $feedUrls[$feedUrl] = $title;
2196 function is_html($content) {
2197 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2200 function url_is_html($url, $login = false, $pass = false) {
2201 return is_html(fetch_file_contents($url, false, $login, $pass));
2204 function build_url($parts) {
2205 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2208 function cleanup_url_path($path) {
2209 $path = str_replace("/./", "/", $path);
2210 $path = str_replace("//", "/", $path);
2216 * Converts a (possibly) relative URL to a absolute one.
2218 * @param string $url Base URL (i.e. from where the document is)
2219 * @param string $rel_url Possibly relative URL in the document
2221 * @return string Absolute URL
2223 function rewrite_relative_url($url, $rel_url) {
2224 if (strpos($rel_url, "://") !== false) {
2226 } else if (strpos($rel_url, "//") === 0) {
2227 # protocol-relative URL (rare but they exist)
2229 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2230 # magnet:, feed:, etc
2232 } else if (strpos($rel_url, "/") === 0) {
2233 $parts = parse_url($url);
2234 $parts['path'] = $rel_url;
2235 $parts['path'] = cleanup_url_path($parts['path']);
2237 return build_url($parts);
2240 $parts = parse_url($url);
2241 if (!isset($parts['path'])) {
2242 $parts['path'] = '/';
2244 $dir = $parts['path'];
2245 if (substr($dir, -1) !== '/') {
2246 $dir = dirname($parts['path']);
2247 $dir !== '/' && $dir .= '/';
2249 $parts['path'] = $dir . $rel_url;
2250 $parts['path'] = cleanup_url_path($parts['path']);
2252 return build_url($parts);
2256 function cleanup_tags($days = 14, $limit = 1000) {
2258 $days = (int) $days;
2260 if (DB_TYPE
== "pgsql") {
2261 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2262 } else if (DB_TYPE
== "mysql") {
2263 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2270 while ($limit > 0) {
2273 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2274 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2275 WHERE post_int_id = int_id AND $interval_query AND
2276 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2277 $sth->execute([$limit]);
2281 while ($line = $sth->fetch()) {
2282 array_push($ids, $line['id']);
2285 if (count($ids) > 0) {
2286 $ids = join(",", $ids);
2288 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2289 $tags_deleted = $usth->rowCount();
2294 $limit -= $limit_part;
2297 return $tags_deleted;
2300 function print_user_stylesheet() {
2301 $value = get_pref('USER_STYLESHEET');
2304 print "<style type=\"text/css\">";
2305 print str_replace("<br/>", "\n", $value);
2311 function filter_to_sql($filter, $owner_uid) {
2316 if (DB_TYPE
== "pgsql")
2319 $reg_qpart = "REGEXP";
2321 foreach ($filter["rules"] AS $rule) {
2322 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2323 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2324 $rule['reg_exp']) !== FALSE;
2326 if ($regexp_valid) {
2328 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2330 switch ($rule["type"]) {
2332 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2333 $rule['reg_exp'] . "')";
2336 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2337 $rule['reg_exp'] . "')";
2340 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2341 $rule['reg_exp'] . "') OR LOWER(" .
2342 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2345 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2346 $rule['reg_exp'] . "')";
2349 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2350 $rule['reg_exp'] . "')";
2353 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2354 $rule['reg_exp'] . "')";
2358 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2360 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2361 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2364 if (isset($rule["cat_id"])) {
2366 if ($rule["cat_id"] > 0) {
2367 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2368 array_push($children, $rule["cat_id"]);
2369 $children = array_map("intval", $children);
2371 $children = join(",", $children);
2373 $cat_qpart = "cat_id IN ($children)";
2375 $cat_qpart = "cat_id IS NULL";
2378 $qpart .= " AND $cat_qpart";
2381 $qpart .= " AND feed_id IS NOT NULL";
2383 array_push($query, "($qpart)");
2388 if (count($query) > 0) {
2389 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2391 $fullquery = "(false)";
2394 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2399 if (!function_exists('gzdecode')) {
2400 function gzdecode($string) { // no support for 2nd argument
2401 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2402 base64_encode($string));
2406 function get_random_bytes($length) {
2407 if (function_exists('openssl_random_pseudo_bytes')) {
2408 return openssl_random_pseudo_bytes($length);
2412 for ($i = 0; $i < $length; $i++
)
2413 $output .= chr(mt_rand(0, 255));
2419 function read_stdin() {
2420 $fp = fopen("php://stdin", "r");
2423 $line = trim(fgets($fp));
2431 function implements_interface($class, $interface) {
2432 return in_array($interface, class_implements($class));
2435 function get_minified_js($files) {
2439 foreach ($files as $js) {
2440 if (!isset($_GET['debug'])) {
2441 $cached_file = CACHE_DIR
. "/js/".basename($js);
2443 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2445 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2447 if ($header && $contents) {
2448 list($htag, $hversion) = explode(":", $header);
2450 if ($htag == "tt-rss" && $hversion == VERSION
) {
2457 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2458 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2462 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2469 function calculate_dep_timestamp() {
2470 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2474 foreach ($files as $file) {
2475 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2481 function T_js_decl($s1, $s2) {
2483 $s1 = preg_replace("/\n/", "", $s1);
2484 $s2 = preg_replace("/\n/", "", $s2);
2486 $s1 = preg_replace("/\"/", "\\\"", $s1);
2487 $s2 = preg_replace("/\"/", "\\\"", $s2);
2489 return "T_messages[\"$s1\"] = \"$s2\";\n";
2493 function init_js_translations() {
2495 print 'var T_messages = new Object();
2498 if (T_messages[msg]) {
2499 return T_messages[msg];
2505 function ngettext(msg1, msg2, n) {
2506 return __((parseInt(n) > 1) ? msg2 : msg1);
2509 $l10n = _get_reader();
2511 for ($i = 0; $i < $l10n->total
; $i++
) {
2512 $orig = $l10n->get_original_string($i);
2513 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2514 $key = explode(chr(0), $orig);
2515 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2516 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2518 $translation = __($orig);
2519 print T_js_decl($orig, $translation);
2524 function get_theme_path($theme) {
2525 if ($theme == "default.php")
2526 return "css/default.css";
2528 $check = "themes/$theme";
2529 if (file_exists($check)) return $check;
2531 $check = "themes.local/$theme";
2532 if (file_exists($check)) return $check;
2535 function theme_valid($theme) {
2536 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2538 if (in_array($theme, $bundled_themes)) return true;
2540 $file = "themes/" . basename($theme);
2542 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2544 if (file_exists($file) && is_readable($file)) {
2545 $fh = fopen($file, "r");
2548 $header = fgets($fh);
2551 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2559 * @SuppressWarnings(unused)
2561 function error_json($code) {
2562 require_once "errors.php";
2564 @$message = $ERRORS[$code];
2566 return json_encode(array("error" =>
2567 array("code" => $code, "message" => $message)));
2571 /*function abs_to_rel_path($dir) {
2572 $tmp = str_replace(dirname(__DIR__), "", $dir);
2574 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2579 function get_upload_error_message($code) {
2582 0 => __('There is no error, the file uploaded with success'),
2583 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2584 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2585 3 => __('The uploaded file was only partially uploaded'),
2586 4 => __('No file was uploaded'),
2587 6 => __('Missing a temporary folder'),
2588 7 => __('Failed to write file to disk.'),
2589 8 => __('A PHP extension stopped the file upload.'),
2592 return $errors[$code];
2595 function base64_img($filename) {
2596 if (file_exists($filename)) {
2597 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2599 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2605 /* this is essentially a wrapper for readfile() which allows plugins to hook
2606 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2608 hook function should return true if request was handled (or at least attempted to)
2610 note that this can be called without user context so the plugin to handle this
2611 should be loaded systemwide in config.php */
2612 function send_local_file($filename) {
2613 if (file_exists($filename)) {
2615 if (is_writable($filename)) touch($filename);
2617 $tmppluginhost = new PluginHost();
2619 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2620 $tmppluginhost->load_data();
2622 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2623 if ($plugin->hook_send_local_file($filename)) return true;
2626 $mimetype = mime_content_type($filename);
2628 // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2629 // video files are detected as octet-stream by mime_content_type()
2631 if ($mimetype == "application/octet-stream")
2632 $mimetype = "video/mp4";
2634 header("Content-type: $mimetype");
2636 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2637 header("Last-Modified: $stamp", true);
2639 return readfile($filename);
2645 function check_mysql_tables() {
2648 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2649 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2650 $sth->execute([DB_NAME
]);
2654 while ($line = $sth->fetch()) {
2655 array_push($bad_tables, $line);
2661 function validate_field($string, $allowed, $default = "") {
2662 if (in_array($string, $allowed))
2668 function arr_qmarks($arr) {
2669 return str_repeat('?,', count($arr) - 1) . '?';