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;
17 libxml_disable_entity_loader(true);
19 // separate test because this is included before sanity checks
20 if (function_exists("mb_internal_encoding")) mb_internal_encoding("UTF-8");
22 date_default_timezone_set('UTC');
23 if (defined('E_DEPRECATED')) {
24 error_reporting(E_ALL
& ~E_NOTICE
& ~E_DEPRECATED
);
26 error_reporting(E_ALL
& ~E_NOTICE
);
29 require_once 'config.php';
32 * Define a constant if not already defined
34 function define_default($name, $value) {
35 defined($name) or define($name, $value);
38 /* Some tunables you can override in config.php using define(): */
40 define_default('FEED_FETCH_TIMEOUT', 45);
41 // How may seconds to wait for response when requesting feed from a site
42 define_default('FEED_FETCH_NO_CACHE_TIMEOUT', 15);
43 // How may seconds to wait for response when requesting feed from a
44 // site when that feed wasn't cached before
45 define_default('FILE_FETCH_TIMEOUT', 45);
46 // Default timeout when fetching files from remote sites
47 define_default('FILE_FETCH_CONNECT_TIMEOUT', 15);
48 // How many seconds to wait for initial response from website when
49 // fetching files from remote sites
50 define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
51 // stop updating feeds if users haven't logged in for X days
52 define_default('DAEMON_FEED_LIMIT', 500);
53 // feed limit for one update batch
54 define_default('DAEMON_SLEEP_INTERVAL', 120);
55 // default sleep interval between feed updates (sec)
56 define_default('MIN_CACHE_FILE_SIZE', 1024);
57 // do not cache files smaller than that (bytes)
58 define_default('MAX_CACHE_FILE_SIZE', 64*1024*1024);
59 // do not cache files larger than that (bytes)
60 define_default('MAX_DOWNLOAD_FILE_SIZE', 16*1024*1024);
61 // do not download general files larger than that (bytes)
62 define_default('CACHE_MAX_DAYS', 7);
63 // max age in days for various automatically cached (temporary) files
64 define_default('MAX_CONDITIONAL_INTERVAL', 3600*12);
65 // max interval between forced unconditional updates for servers
66 // not complying with http if-modified-since (seconds)
68 /* tunables end here */
70 if (DB_TYPE
== "pgsql") {
71 define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
73 define('SUBSTRING_FOR_DATE', 'SUBSTRING');
77 * Return available translations names.
80 * @return array A array of available translations.
82 function get_translations() {
84 "auto" => "Detect automatically",
85 "ar_SA" => "العربيّة (Arabic)",
86 "bg_BG" => "Bulgarian",
91 "el_GR" => "Ελληνικά",
92 "es_ES" => "Español (España)",
95 "fr_FR" => "Français",
96 "hu_HU" => "Magyar (Hungarian)",
97 "it_IT" => "Italiano",
98 "ja_JP" => "日本語 (Japanese)",
99 "lv_LV" => "Latviešu",
100 "nb_NO" => "Norwegian bokmål",
103 "ru_RU" => "Русский",
104 "pt_BR" => "Portuguese/Brazil",
105 "pt_PT" => "Portuguese/Portugal",
106 "zh_CN" => "Simplified Chinese",
107 "zh_TW" => "Traditional Chinese",
108 "sv_SE" => "Svenska",
110 "tr_TR" => "Türkçe");
115 require_once "lib/accept-to-gettext.php";
116 require_once "lib/gettext/gettext.inc";
118 function startup_gettext() {
120 # Get locale from Accept-Language header
121 $lang = al2gt(array_keys(get_translations()), "text/html");
123 if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
124 $lang = _TRANSLATION_OVERRIDE_DEFAULT
;
127 if ($_SESSION["uid"] && get_schema_version() >= 120) {
128 $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
130 if ($pref_lang && $pref_lang != 'auto') {
136 if (defined('LC_MESSAGES')) {
137 _setlocale(LC_MESSAGES
, $lang);
138 } else if (defined('LC_ALL')) {
139 _setlocale(LC_ALL
, $lang);
142 _bindtextdomain("messages", "locale");
144 _textdomain("messages");
145 _bind_textdomain_codeset("messages", "UTF-8");
149 require_once 'db-prefs.php';
150 require_once 'version.php';
151 require_once 'controls.php';
153 define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION
. ' (http://tt-rss.org/)');
154 ini_set('user_agent', SELF_USER_AGENT
);
156 $schema_version = false;
158 // TODO: compat wrapper, remove at some point
159 function _debug($msg) {
164 * Purge a feed old posts.
166 * @param mixed $link A database connection.
167 * @param mixed $feed_id The id of the purged feed.
168 * @param mixed $purge_interval Olderness of purged posts.
169 * @param boolean $debug Set to True to enable the debug. False by default.
173 function purge_feed($feed_id, $purge_interval) {
175 if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
179 $sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
180 $sth->execute([$feed_id]);
184 if ($row = $sth->fetch()) {
185 $owner_uid = $row["owner_uid"];
188 if ($purge_interval == -1 ||
!$purge_interval) {
190 CCache
::update($feed_id, $owner_uid);
195 if (!$owner_uid) return;
197 if (FORCE_ARTICLE_PURGE
== 0) {
198 $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
201 $purge_unread = true;
202 $purge_interval = FORCE_ARTICLE_PURGE
;
206 $query_limit = " unread = false AND ";
210 $purge_interval = (int) $purge_interval;
212 if (DB_TYPE
== "pgsql") {
213 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
215 WHERE ttrss_entries.id = ref_id AND
219 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
220 $sth->execute([$feed_id]);
223 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
224 USING ttrss_user_entries, ttrss_entries
225 WHERE ttrss_entries.id = ref_id AND
229 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
230 $sth->execute([$feed_id]);
234 $rows = $sth->rowCount();
236 CCache
::update($feed_id, $owner_uid);
238 Debug
::log("Purged feed $feed_id ($purge_interval): deleted $rows articles");
241 } // function purge_feed
243 function feed_purge_interval($feed_id) {
247 $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
249 $sth->execute([$feed_id]);
251 if ($row = $sth->fetch()) {
252 $purge_interval = $row["purge_interval"];
253 $owner_uid = $row["owner_uid"];
255 if ($purge_interval == 0) $purge_interval = get_pref(
256 'PURGE_OLD_DAYS', $owner_uid);
258 return $purge_interval;
265 // TODO: max_size currently only works for CURL transfers
266 // TODO: multiple-argument way is deprecated, first parameter is a hash now
267 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
268 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
270 global $fetch_last_error;
271 global $fetch_last_error_code;
272 global $fetch_last_error_content;
273 global $fetch_last_content_type;
274 global $fetch_last_modified;
275 global $fetch_effective_url;
276 global $fetch_curl_used;
278 $fetch_last_error = false;
279 $fetch_last_error_code = -1;
280 $fetch_last_error_content = "";
281 $fetch_last_content_type = "";
282 $fetch_curl_used = false;
283 $fetch_last_modified = "";
284 $fetch_effective_url = "";
286 if (!is_array($options)) {
288 // falling back on compatibility shim
289 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
292 for ($i = 0; $i < func_num_args(); $i++
) {
293 $tmp[$option_names[$i]] = func_get_arg($i);
299 "url" => func_get_arg(0),
300 "type" => @func_get_arg(1),
301 "login" => @func_get_arg(2),
302 "pass" => @func_get_arg(3),
303 "post_query" => @func_get_arg(4),
304 "timeout" => @func_get_arg(5),
305 "timestamp" => @func_get_arg(6),
306 "useragent" => @func_get_arg(7)
310 $url = $options["url"];
311 $type = isset($options["type"]) ?
$options["type"] : false;
312 $login = isset($options["login"]) ?
$options["login"] : false;
313 $pass = isset($options["pass"]) ?
$options["pass"] : false;
314 $post_query = isset($options["post_query"]) ?
$options["post_query"] : false;
315 $timeout = isset($options["timeout"]) ?
$options["timeout"] : false;
316 $last_modified = isset($options["last_modified"]) ?
$options["last_modified"] : "";
317 $useragent = isset($options["useragent"]) ?
$options["useragent"] : false;
318 $followlocation = isset($options["followlocation"]) ?
$options["followlocation"] : true;
319 $max_size = isset($options["max_size"]) ?
$options["max_size"] : MAX_DOWNLOAD_FILE_SIZE
; // in bytes
320 $http_accept = isset($options["http_accept"]) ?
$options["http_accept"] : false;
322 $url = ltrim($url, ' ');
323 $url = str_replace(' ', '%20', $url);
325 if (strpos($url, "//") === 0)
326 $url = 'http:' . $url;
328 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
330 $fetch_curl_used = true;
332 $ch = curl_init($url);
334 $curl_http_headers = [];
336 if ($last_modified && !$post_query)
337 array_push($curl_http_headers, "If-Modified-Since: $last_modified");
340 array_push($curl_http_headers, "Accept: " . $http_accept);
342 if (count($curl_http_headers) > 0)
343 curl_setopt($ch, CURLOPT_HTTPHEADER
, $curl_http_headers);
345 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
346 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
347 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
348 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
349 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
350 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
351 curl_setopt($ch, CURLOPT_HEADER
, true);
352 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
353 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
355 curl_setopt($ch, CURLOPT_ENCODING
, "");
356 //curl_setopt($ch, CURLOPT_REFERER, $url);
359 curl_setopt($ch, CURLOPT_NOPROGRESS
, false);
360 curl_setopt($ch, CURLOPT_BUFFERSIZE
, 16384); // needed to get 5 arguments in progress function?
362 // holy shit closures in php
363 // download & upload are *expected* sizes respectively, could be zero
364 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION
, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use( &$max_size) {
365 Debug
::log("[curl progressfunction] $downloaded $max_size", Debug
::$LOG_EXTENDED);
367 return ($downloaded > $max_size) ?
1 : 0; // if max size is set, abort when exceeding it
372 if (!ini_get("open_basedir")) {
373 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
376 if (defined('_HTTP_PROXY')) {
377 curl_setopt($ch, CURLOPT_PROXY
, _HTTP_PROXY
);
381 curl_setopt($ch, CURLOPT_POST
, true);
382 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
386 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
388 $ret = @curl_exec
($ch);
390 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE
);
391 $headers = explode("\r\n", substr($ret, 0, $headers_length));
392 $contents = substr($ret, $headers_length);
394 foreach ($headers as $header) {
395 if (strstr($header, ": ") !== FALSE) {
396 list ($key, $value) = explode(": ", $header);
398 if (strtolower($key) == "last-modified") {
399 $fetch_last_modified = $value;
403 if (substr(strtolower($header), 0, 7) == 'http/1.') {
404 $fetch_last_error_code = (int) substr($header, 9, 3);
405 $fetch_last_error = $header;
409 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
410 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
411 $contents = @curl_exec
($ch);
414 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
415 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
417 $fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL
);
419 $fetch_last_error_code = $http_code;
421 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
423 if (curl_errno($ch) != 0) {
424 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
427 $fetch_last_error_content = $contents;
433 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
443 $fetch_curl_used = false;
445 if ($login && $pass){
446 $url_parts = array();
448 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
450 $pass = urlencode($pass);
452 if ($url_parts[1] && $url_parts[2]) {
453 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
457 // TODO: should this support POST requests or not? idk
459 $context_options = array(
465 'ignore_errors' => true,
466 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
467 'protocol_version'=> 1.1)
470 if (!$post_query && $last_modified)
471 array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
474 array_push($context_options['http']['header'], "Accept: $http_accept");
476 if (defined('_HTTP_PROXY')) {
477 $context_options['http']['request_fulluri'] = true;
478 $context_options['http']['proxy'] = _HTTP_PROXY
;
481 $context = stream_context_create($context_options);
483 $old_error = error_get_last();
485 $fetch_effective_url = $url;
487 $data = @file_get_contents
($url, false, $context);
489 if (isset($http_response_header) && is_array($http_response_header)) {
490 foreach ($http_response_header as $header) {
491 if (strstr($header, ": ") !== FALSE) {
492 list ($key, $value) = explode(": ", $header);
494 $key = strtolower($key);
496 if ($key == 'content-type') {
497 $fetch_last_content_type = $value;
498 // don't abort here b/c there might be more than one
499 // e.g. if we were being redirected -- last one is the right one
500 } else if ($key == 'last-modified') {
501 $fetch_last_modified = $value;
502 } else if ($key == 'location') {
503 $fetch_effective_url = $value;
507 if (substr(strtolower($header), 0, 7) == 'http/1.') {
508 $fetch_last_error_code = (int) substr($header, 9, 3);
509 $fetch_last_error = $header;
514 if ($fetch_last_error_code != 200) {
515 $error = error_get_last();
517 if ($error['message'] != $old_error['message']) {
518 $fetch_last_error .= "; " . $error["message"];
521 $fetch_last_error_content = $data;
531 * Try to determine the favicon URL for a feed.
532 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
533 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
535 * @param string $url A feed or page URL
537 * @return mixed The favicon URL, or false if none was found.
539 function get_favicon_url($url) {
541 $favicon_url = false;
543 if ($html = @fetch_file_contents
($url)) {
545 libxml_use_internal_errors(true);
547 $doc = new DOMDocument();
548 $doc->loadHTML($html);
549 $xpath = new DOMXPath($doc);
551 $base = $xpath->query('/html/head/base[@href]');
552 foreach ($base as $b) {
553 $url = rewrite_relative_url($url, $b->getAttribute("href"));
557 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
558 if (count($entries) > 0) {
559 foreach ($entries as $entry) {
560 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
567 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
570 } // function get_favicon_url
572 function initialize_user_prefs($uid, $profile = false) {
574 if (get_schema_version() < 63) $profile_qpart = "";
577 $in_nested_tr = false;
580 $pdo->beginTransaction();
581 } catch (Exception
$e) {
582 $in_nested_tr = true;
585 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
587 $profile = $profile ?
$profile : null;
589 $u_sth = $pdo->prepare("SELECT pref_name
590 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
591 (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
592 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
594 $active_prefs = array();
596 while ($line = $u_sth->fetch()) {
597 array_push($active_prefs, $line["pref_name"]);
600 while ($line = $sth->fetch()) {
601 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
602 // print "adding " . $line["pref_name"] . "<br>";
604 if (get_schema_version() < 63) {
605 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
606 (owner_uid,pref_name,value) VALUES
608 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
611 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
612 (owner_uid,pref_name,value, profile) VALUES
614 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
620 if (!$in_nested_tr) $pdo->commit();
624 function get_ssl_certificate_id() {
625 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
626 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
627 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
628 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
629 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
631 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
632 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
633 $_SERVER["SSL_CLIENT_V_START"] .
634 $_SERVER["SSL_CLIENT_V_END"] .
635 $_SERVER["SSL_CLIENT_S_DN"]);
640 function authenticate_user($login, $password, $check_only = false) {
642 if (!SINGLE_USER_MODE
) {
644 $auth_module = false;
646 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
648 $user_id = (int) $plugin->authenticate($login, $password);
651 $auth_module = strtolower(get_class($plugin));
656 if ($user_id && !$check_only) {
659 session_regenerate_id(true);
661 $_SESSION["uid"] = $user_id;
662 $_SESSION["version"] = VERSION_STATIC
;
663 $_SESSION["auth_module"] = $auth_module;
666 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
668 $sth->execute([$user_id]);
669 $row = $sth->fetch();
671 $_SESSION["name"] = $row["login"];
672 $_SESSION["access_level"] = $row["access_level"];
673 $_SESSION["csrf_token"] = uniqid_short();
675 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
676 $usth->execute([$user_id]);
678 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
679 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
680 $_SESSION["pwd_hash"] = $row["pwd_hash"];
682 $_SESSION["last_version_check"] = time();
684 initialize_user_prefs($_SESSION["uid"]);
693 $_SESSION["uid"] = 1;
694 $_SESSION["name"] = "admin";
695 $_SESSION["access_level"] = 10;
697 $_SESSION["hide_hello"] = true;
698 $_SESSION["hide_logout"] = true;
700 $_SESSION["auth_module"] = false;
702 if (!$_SESSION["csrf_token"]) {
703 $_SESSION["csrf_token"] = uniqid_short();
706 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
708 initialize_user_prefs($_SESSION["uid"]);
714 // this is used for user http parameters unless HTML code is actually needed
715 function clean($param) {
716 if (is_array($param)) {
717 return array_map("strip_tags", $param);
718 } else if (is_string($param)) {
719 return strip_tags($param);
725 function make_password($length = 8) {
728 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
732 while ($i < $length) {
733 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
735 if (!strstr($password, $char)) {
743 // this is called after user is created to initialize default feeds, labels
746 // user preferences are checked on every login, not here
748 function initialize_user($uid) {
752 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
753 values (?, 'Tiny Tiny RSS: Forum',
754 'http://tt-rss.org/forum/rss.php')");
755 $sth->execute([$uid]);
758 function logout_user() {
760 if (isset($_COOKIE[session_name()])) {
761 setcookie(session_name(), '', time()-42000, '/');
766 function validate_csrf($csrf_token) {
767 return $csrf_token == $_SESSION['csrf_token'];
770 function load_user_plugins($owner_uid, $pluginhost = false) {
772 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
774 if ($owner_uid && SCHEMA_VERSION
>= 100) {
775 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
777 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
779 if (get_schema_version() > 100) {
780 $pluginhost->load_data();
785 function login_sequence() {
788 if (SINGLE_USER_MODE
) {
790 authenticate_user("admin", null);
792 load_user_plugins($_SESSION["uid"]);
794 if (!validate_session()) $_SESSION["uid"] = false;
796 if (!$_SESSION["uid"]) {
798 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
799 $_SESSION["ref_schema_version"] = get_schema_version(true);
801 authenticate_user(null, null, true);
804 if (!$_SESSION["uid"]) {
812 /* bump login timestamp */
813 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
814 $sth->execute([$_SESSION['uid']]);
816 $_SESSION["last_login_update"] = time();
819 if ($_SESSION["uid"]) {
821 load_user_plugins($_SESSION["uid"]);
825 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
827 (SELECT COUNT(id) FROM ttrss_feeds WHERE
828 ttrss_feeds.id = feed_id) = 0");
830 $sth->execute([$_SESSION['uid']]);
832 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
834 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
835 ttrss_feed_categories.id = feed_id) = 0");
837 $sth->execute([$_SESSION['uid']]);
843 function truncate_string($str, $max_len, $suffix = '…') {
844 if (mb_strlen($str, "utf-8") > $max_len) {
845 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
852 function truncate_middle($str, $max_len, $suffix = '…') {
853 if (strlen($str) > $max_len) {
854 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
860 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
863 $source_tz = new DateTimeZone($source_tz);
864 } catch (Exception
$e) {
865 $source_tz = new DateTimeZone('UTC');
869 $dest_tz = new DateTimeZone($dest_tz);
870 } catch (Exception
$e) {
871 $dest_tz = new DateTimeZone('UTC');
874 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
875 return $dt->format('U') +
$dest_tz->getOffset($dt);
878 function make_local_datetime($timestamp, $long, $owner_uid = false,
879 $no_smart_dt = false, $eta_min = false) {
881 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
882 if (!$timestamp) $timestamp = '1970-01-01 0:00';
887 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
889 $timestamp = substr($timestamp, 0, 19);
891 # We store date in UTC internally
892 $dt = new DateTime($timestamp, $utc_tz);
894 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
896 if ($user_tz_string != 'Automatic') {
899 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
900 } catch (Exception
$e) {
904 $tz_offset = $user_tz->getOffset($dt);
906 $tz_offset = (int) -$_SESSION["clientTzOffset"];
909 $user_timestamp = $dt->format('U') +
$tz_offset;
912 return smart_date_time($user_timestamp,
913 $tz_offset, $owner_uid, $eta_min);
916 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
918 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
920 return date($format, $user_timestamp);
924 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
925 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
927 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
928 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
929 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
930 return date("G:i", $timestamp);
931 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
932 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
933 return date($format, $timestamp);
935 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
936 return date($format, $timestamp);
940 function sql_bool_to_bool($s) {
941 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
944 function bool_to_sql_bool($s) {
948 // Session caching removed due to causing wrong redirects to upgrade
949 // script when get_schema_version() is called on an obsolete session
950 // created on a previous schema version.
951 function get_schema_version($nocache = false) {
952 global $schema_version;
956 if (!$schema_version && !$nocache) {
957 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
958 $version = $row["schema_version"];
959 $schema_version = $version;
962 return $schema_version;
966 function sanity_check() {
967 require_once 'errors.php';
971 $schema_version = get_schema_version(true);
973 if ($schema_version != SCHEMA_VERSION
) {
977 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
980 function file_is_locked($filename) {
981 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
982 if (function_exists('flock')) {
983 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
985 if (flock($fp, LOCK_EX | LOCK_NB
)) {
996 return true; // consider the file always locked and skip the test
1003 function make_lockfile($filename) {
1004 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1006 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1007 $stat_h = fstat($fp);
1008 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1010 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1011 if ($stat_h["ino"] != $stat_f["ino"] ||
1012 $stat_h["dev"] != $stat_f["dev"]) {
1018 if (function_exists('posix_getpid')) {
1019 fwrite($fp, posix_getpid() . "\n");
1027 function make_stampfile($filename) {
1028 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1030 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1031 fwrite($fp, time() . "\n");
1032 flock($fp, LOCK_UN
);
1040 function sql_random_function() {
1041 if (DB_TYPE
== "mysql") {
1048 function getFeedUnread($feed, $is_cat = false) {
1049 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1052 function checkbox_to_sql_bool($val) {
1053 return ($val == "on") ?
1 : 0;
1056 function uniqid_short() {
1057 return uniqid(base_convert(rand(), 10, 36));
1060 function make_init_params() {
1063 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1064 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1065 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1066 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1068 $params[strtolower($param)] = (int) get_pref($param);
1071 $params["icons_url"] = ICONS_URL
;
1072 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1073 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1074 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1075 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1076 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1077 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1078 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1080 $theme = get_pref( "USER_CSS_THEME", false, false);
1081 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1083 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1085 $params["php_platform"] = PHP_OS
;
1086 $params["php_version"] = PHP_VERSION
;
1088 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1092 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1093 ttrss_feeds WHERE owner_uid = ?");
1094 $sth->execute([$_SESSION['uid']]);
1095 $row = $sth->fetch();
1097 $max_feed_id = $row["mid"];
1098 $num_feeds = $row["nf"];
1100 $params["max_feed_id"] = (int) $max_feed_id;
1101 $params["num_feeds"] = (int) $num_feeds;
1103 $params["hotkeys"] = get_hotkeys_map();
1105 $params["csrf_token"] = $_SESSION["csrf_token"];
1106 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1108 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1110 $params["icon_alert"] = base64_img("images/alert.png");
1111 $params["icon_information"] = base64_img("images/information.png");
1112 $params["icon_cross"] = base64_img("images/cross.png");
1113 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1115 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1120 function get_hotkeys_info() {
1122 __("Navigation") => array(
1123 "next_feed" => __("Open next feed"),
1124 "prev_feed" => __("Open previous feed"),
1125 "next_article" => __("Open next article"),
1126 "prev_article" => __("Open previous article"),
1127 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1128 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1129 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1130 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1131 "search_dialog" => __("Show search dialog")),
1132 __("Article") => array(
1133 "toggle_mark" => __("Toggle starred"),
1134 "toggle_publ" => __("Toggle published"),
1135 "toggle_unread" => __("Toggle unread"),
1136 "edit_tags" => __("Edit tags"),
1137 "open_in_new_window" => __("Open in new window"),
1138 "catchup_below" => __("Mark below as read"),
1139 "catchup_above" => __("Mark above as read"),
1140 "article_scroll_down" => __("Scroll down"),
1141 "article_scroll_up" => __("Scroll up"),
1142 "select_article_cursor" => __("Select article under cursor"),
1143 "email_article" => __("Email article"),
1144 "close_article" => __("Close/collapse article"),
1145 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1146 "toggle_widescreen" => __("Toggle widescreen mode"),
1147 "toggle_embed_original" => __("Toggle embed original")),
1148 __("Article selection") => array(
1149 "select_all" => __("Select all articles"),
1150 "select_unread" => __("Select unread"),
1151 "select_marked" => __("Select starred"),
1152 "select_published" => __("Select published"),
1153 "select_invert" => __("Invert selection"),
1154 "select_none" => __("Deselect everything")),
1155 __("Feed") => array(
1156 "feed_refresh" => __("Refresh current feed"),
1157 "feed_unhide_read" => __("Un/hide read feeds"),
1158 "feed_subscribe" => __("Subscribe to feed"),
1159 "feed_edit" => __("Edit feed"),
1160 "feed_catchup" => __("Mark as read"),
1161 "feed_reverse" => __("Reverse headlines"),
1162 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1163 "feed_debug_update" => __("Debug feed update"),
1164 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1165 "catchup_all" => __("Mark all feeds as read"),
1166 "cat_toggle_collapse" => __("Un/collapse current category"),
1167 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode"),
1168 "toggle_combined_mode" => __("Toggle combined mode")),
1169 __("Go to") => array(
1170 "goto_all" => __("All articles"),
1171 "goto_fresh" => __("Fresh"),
1172 "goto_marked" => __("Starred"),
1173 "goto_published" => __("Published"),
1174 "goto_tagcloud" => __("Tag cloud"),
1175 "goto_prefs" => __("Preferences")),
1176 __("Other") => array(
1177 "create_label" => __("Create label"),
1178 "create_filter" => __("Create filter"),
1179 "collapse_sidebar" => __("Un/collapse sidebar"),
1180 "help_dialog" => __("Show help dialog"))
1183 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1184 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1190 function get_hotkeys_map() {
1192 // "navigation" => array(
1195 "n" => "next_article",
1196 "p" => "prev_article",
1197 "(38)|up" => "prev_article",
1198 "(40)|down" => "next_article",
1199 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1200 // "^(40)|Ctrl-down" => "next_article_noscroll",
1201 "(191)|/" => "search_dialog",
1202 // "article" => array(
1203 "s" => "toggle_mark",
1204 "*s" => "toggle_publ",
1205 "u" => "toggle_unread",
1206 "*t" => "edit_tags",
1207 "o" => "open_in_new_window",
1208 "c p" => "catchup_below",
1209 "c n" => "catchup_above",
1210 "*n" => "article_scroll_down",
1211 "*p" => "article_scroll_up",
1212 "*(38)|Shift+up" => "article_scroll_up",
1213 "*(40)|Shift+down" => "article_scroll_down",
1214 "a *w" => "toggle_widescreen",
1215 "a e" => "toggle_embed_original",
1216 "e" => "email_article",
1217 "a q" => "close_article",
1218 // "article_selection" => array(
1219 "a a" => "select_all",
1220 "a u" => "select_unread",
1221 "a *u" => "select_marked",
1222 "a p" => "select_published",
1223 "a i" => "select_invert",
1224 "a n" => "select_none",
1226 "f r" => "feed_refresh",
1227 "f a" => "feed_unhide_read",
1228 "f s" => "feed_subscribe",
1229 "f e" => "feed_edit",
1230 "f q" => "feed_catchup",
1231 "f x" => "feed_reverse",
1232 "f g" => "feed_toggle_vgroup",
1233 "f *d" => "feed_debug_update",
1234 "f *g" => "feed_debug_viewfeed",
1235 "f *c" => "toggle_combined_mode",
1236 "f c" => "toggle_cdm_expanded",
1237 "*q" => "catchup_all",
1238 "x" => "cat_toggle_collapse",
1240 "g a" => "goto_all",
1241 "g f" => "goto_fresh",
1242 "g s" => "goto_marked",
1243 "g p" => "goto_published",
1244 "g t" => "goto_tagcloud",
1245 "g *p" => "goto_prefs",
1246 // "other" => array(
1247 "(9)|Tab" => "select_article_cursor", // tab
1248 "c l" => "create_label",
1249 "c f" => "create_filter",
1250 "c s" => "collapse_sidebar",
1251 "^(191)|Ctrl+/" => "help_dialog",
1254 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1255 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1257 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1258 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1261 $prefixes = array();
1263 foreach (array_keys($hotkeys) as $hotkey) {
1264 $pair = explode(" ", $hotkey, 2);
1266 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1267 array_push($prefixes, $pair[0]);
1271 return array($prefixes, $hotkeys);
1274 function check_for_update() {
1275 if (defined("GIT_VERSION_TIMESTAMP")) {
1276 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1279 $content = json_decode($content, true);
1281 if ($content && isset($content["changeset"])) {
1282 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1283 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1285 return $content["changeset"]["id"];
1294 function make_runtime_info($disable_update_check = false) {
1299 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1300 ttrss_feeds WHERE owner_uid = ?");
1301 $sth->execute([$_SESSION['uid']]);
1302 $row = $sth->fetch();
1304 $max_feed_id = $row['mid'];
1305 $num_feeds = $row['nf'];
1307 $data["max_feed_id"] = (int) $max_feed_id;
1308 $data["num_feeds"] = (int) $num_feeds;
1310 $data['last_article_id'] = Article
::getLastArticleId();
1311 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1313 $data['dep_ts'] = calculate_dep_timestamp();
1314 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1316 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1318 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1319 $update_result = @check_for_update
();
1321 $data["update_result"] = $update_result;
1323 $_SESSION["last_version_check"] = time();
1326 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1328 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1330 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1332 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1335 $stamp_delta = time() - $stamp;
1337 if ($stamp_delta > 1800) {
1341 $_SESSION["daemon_stamp_check"] = time();
1344 $data['daemon_stamp_ok'] = $stamp_check;
1346 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1348 $data['daemon_stamp'] = $stamp_fmt;
1356 function search_to_sql($search, $search_language) {
1358 $keywords = str_getcsv(trim($search), " ");
1359 $query_keywords = array();
1360 $search_words = array();
1361 $search_query_leftover = array();
1365 if ($search_language)
1366 $search_language = $pdo->quote(mb_strtolower($search_language));
1368 $search_language = $pdo->quote("english");
1370 foreach ($keywords as $k) {
1371 if (strpos($k, "-") === 0) {
1378 $commandpair = explode(":", mb_strtolower($k), 2);
1380 switch ($commandpair[0]) {
1382 if ($commandpair[1]) {
1383 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1384 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1386 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1387 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1388 array_push($search_words, $k);
1392 if ($commandpair[1]) {
1393 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1394 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1396 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1397 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1398 array_push($search_words, $k);
1402 if ($commandpair[1]) {
1403 if ($commandpair[1] == "true")
1404 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1405 else if ($commandpair[1] == "false")
1406 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1408 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1409 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1411 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1412 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1413 if (!$not) array_push($search_words, $k);
1418 if ($commandpair[1]) {
1419 if ($commandpair[1] == "true")
1420 array_push($query_keywords, "($not (marked = true))");
1422 array_push($query_keywords, "($not (marked = false))");
1424 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1425 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1426 if (!$not) array_push($search_words, $k);
1430 if ($commandpair[1]) {
1431 if ($commandpair[1] == "true")
1432 array_push($query_keywords, "($not (published = true))");
1434 array_push($query_keywords, "($not (published = false))");
1437 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1438 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1439 if (!$not) array_push($search_words, $k);
1443 if ($commandpair[1]) {
1444 if ($commandpair[1] == "true")
1445 array_push($query_keywords, "($not (unread = true))");
1447 array_push($query_keywords, "($not (unread = false))");
1450 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1451 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1452 if (!$not) array_push($search_words, $k);
1456 if (strpos($k, "@") === 0) {
1458 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1459 $orig_ts = strtotime(substr($k, 1));
1460 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1462 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1464 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1467 if (DB_TYPE
== "pgsql") {
1468 $k = mb_strtolower($k);
1469 array_push($search_query_leftover, $not ?
"!$k" : $k);
1471 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1472 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1475 if (!$not) array_push($search_words, $k);
1480 if (count($search_query_leftover) > 0) {
1481 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1483 if (DB_TYPE
== "pgsql") {
1484 array_push($query_keywords,
1485 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1490 $search_query_part = implode("AND", $query_keywords);
1492 return array($search_query_part, $search_words);
1495 function iframe_whitelisted($entry) {
1496 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1498 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1501 foreach ($whitelist as $w) {
1502 if ($src == $w ||
$src == "www.$w")
1510 // check for locally cached (media) URLs and rewrite to local versions
1511 // this is called separately after sanitize() and plugin render article hooks to allow
1512 // plugins work on original source URLs used before caching
1514 function rewrite_cached_urls($str) {
1515 $charset_hack = '<head>
1516 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1519 $res = trim($str); if (!$res) return '';
1521 $doc = new DOMDocument();
1522 $doc->loadHTML($charset_hack . $res);
1523 $xpath = new DOMXPath($doc);
1525 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
1527 $need_saving = false;
1529 foreach ($entries as $entry) {
1531 if ($entry->hasAttribute('src') ||
$entry->hasAttribute('poster')) {
1533 // should be already absolutized because this is called after sanitize()
1534 $src = $entry->hasAttribute('poster') ?
$entry->getAttribute('poster') : $entry->getAttribute('src');
1535 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1537 if (file_exists($cached_filename)) {
1539 // this is strictly cosmetic
1540 if ($entry->tagName
== 'img') {
1542 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1544 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1550 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1552 if ($entry->hasAttribute('poster'))
1553 $entry->setAttribute('poster', $src);
1555 $entry->setAttribute('src', $src);
1557 $need_saving = true;
1563 $doc->removeChild($doc->firstChild
); //remove doctype
1564 $res = $doc->saveHTML();
1570 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1571 if (!$owner) $owner = $_SESSION["uid"];
1573 $res = trim($str); if (!$res) return '';
1575 $charset_hack = '<head>
1576 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1579 $res = trim($res); if (!$res) return '';
1581 libxml_use_internal_errors(true);
1583 $doc = new DOMDocument();
1584 $doc->loadHTML($charset_hack . $res);
1585 $xpath = new DOMXPath($doc);
1587 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1589 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1591 foreach ($entries as $entry) {
1593 if ($entry->hasAttribute('href')) {
1594 $entry->setAttribute('href',
1595 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1597 $entry->setAttribute('rel', 'noopener noreferrer');
1600 if ($entry->hasAttribute('src')) {
1601 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1603 // cache stuff has gone to rewrite_cached_urls()
1605 $entry->setAttribute('src', $src);
1608 if ($entry->nodeName
== 'img') {
1609 $entry->setAttribute('referrerpolicy', 'no-referrer');
1611 $entry->removeAttribute('width');
1612 $entry->removeAttribute('height');
1614 if ($entry->hasAttribute('src')) {
1615 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1617 if (is_prefix_https() && !$is_https_url) {
1619 if ($entry->hasAttribute('srcset')) {
1620 $entry->removeAttribute('srcset');
1623 if ($entry->hasAttribute('sizes')) {
1624 $entry->removeAttribute('sizes');
1630 if ($entry->hasAttribute('src') &&
1631 ($owner && get_pref("STRIP_IMAGES", $owner)) ||
$force_remove_images ||
$_SESSION["bw_limit"]) {
1633 $p = $doc->createElement('p');
1635 $a = $doc->createElement('a');
1636 $a->setAttribute('href', $entry->getAttribute('src'));
1638 $a->appendChild(new DOMText($entry->getAttribute('src')));
1639 $a->setAttribute('target', '_blank');
1640 $a->setAttribute('rel', 'noopener noreferrer');
1642 $p->appendChild($a);
1644 if ($entry->nodeName
== 'source') {
1646 if ($entry->parentNode
&& $entry->parentNode
->parentNode
)
1647 $entry->parentNode
->parentNode
->replaceChild($p, $entry->parentNode
);
1649 } else if ($entry->nodeName
== 'img') {
1651 if ($entry->parentNode
)
1652 $entry->parentNode
->replaceChild($p, $entry);
1657 if (strtolower($entry->nodeName
) == "a") {
1658 $entry->setAttribute("target", "_blank");
1659 $entry->setAttribute("rel", "noopener noreferrer");
1663 $entries = $xpath->query('//iframe');
1664 foreach ($entries as $entry) {
1665 if (!iframe_whitelisted($entry)) {
1666 $entry->setAttribute('sandbox', 'allow-scripts');
1668 if (is_prefix_https()) {
1669 $entry->setAttribute("src",
1670 str_replace("http://", "https://",
1671 $entry->getAttribute("src")));
1676 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1677 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1678 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1679 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1680 'dt', 'em', 'footer', 'figure', 'figcaption',
1681 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1682 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1683 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1684 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1685 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1686 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1688 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1690 $disallowed_attributes = array('id', 'style', 'class');
1692 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1693 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1694 if (is_array($retval)) {
1696 $allowed_elements = $retval[1];
1697 $disallowed_attributes = $retval[2];
1703 $doc->removeChild($doc->firstChild
); //remove doctype
1704 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1706 if ($highlight_words) {
1707 foreach ($highlight_words as $word) {
1709 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1711 $elements = $xpath->query("//*/text()");
1713 foreach ($elements as $child) {
1715 $fragment = $doc->createDocumentFragment();
1716 $text = $child->textContent
;
1718 while (($pos = mb_stripos($text, $word)) !== false) {
1719 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1720 $word = mb_substr($text, $pos, mb_strlen($word));
1721 $highlight = $doc->createElement('span');
1722 $highlight->appendChild(new DomText($word));
1723 $highlight->setAttribute('class', 'highlight');
1724 $fragment->appendChild($highlight);
1725 $text = mb_substr($text, $pos +
mb_strlen($word));
1728 if (!empty($text)) $fragment->appendChild(new DomText($text));
1730 $child->parentNode
->replaceChild($fragment, $child);
1735 $res = $doc->saveHTML();
1737 /* strip everything outside of <body>...</body> */
1739 $res_frag = array();
1740 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1741 return $res_frag[1];
1747 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1748 $xpath = new DOMXPath($doc);
1749 $entries = $xpath->query('//*');
1751 foreach ($entries as $entry) {
1752 if (!in_array($entry->nodeName
, $allowed_elements)) {
1753 $entry->parentNode
->removeChild($entry);
1756 if ($entry->hasAttributes()) {
1757 $attrs_to_remove = array();
1759 foreach ($entry->attributes
as $attr) {
1761 if (strpos($attr->nodeName
, 'on') === 0) {
1762 array_push($attrs_to_remove, $attr);
1765 if (strpos($attr->nodeName
, "data-") === 0) {
1766 array_push($attrs_to_remove, $attr);
1769 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1770 array_push($attrs_to_remove, $attr);
1773 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1774 array_push($attrs_to_remove, $attr);
1778 foreach ($attrs_to_remove as $attr) {
1779 $entry->removeAttributeNode($attr);
1787 function trim_array($array) {
1789 array_walk($tmp, 'trim');
1793 function tag_is_valid($tag) {
1794 if (!$tag ||
is_numeric($tag) ||
mb_strlen($tag) > 250)
1800 function render_login_form() {
1801 header('Cache-Control: public');
1803 require_once "login_form.php";
1807 function T_sprintf() {
1808 $args = func_get_args();
1809 return vsprintf(__(array_shift($args)), $args);
1812 function print_checkpoint($n, $s) {
1813 $ts = microtime(true);
1814 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1818 function sanitize_tag($tag) {
1821 $tag = mb_strtolower($tag, 'utf-8');
1823 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1825 if (DB_TYPE
== "mysql") {
1826 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1832 function is_server_https() {
1833 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1836 function is_prefix_https() {
1837 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1840 // this returns SELF_URL_PATH sans ending slash
1841 function get_self_url_prefix() {
1842 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1843 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1845 return SELF_URL_PATH
;
1849 function encrypt_password($pass, $salt = '', $mode2 = false) {
1850 if ($salt && $mode2) {
1851 return "MODE2:" . hash('sha256', $salt . $pass);
1853 return "SHA1X:" . sha1("$salt:$pass");
1855 return "SHA1:" . sha1($pass);
1857 } // function encrypt_password
1859 function load_filters($feed_id, $owner_uid) {
1862 $feed_id = (int) $feed_id;
1863 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1866 $null_cat_qpart = "cat_id IS NULL OR";
1868 $null_cat_qpart = "";
1872 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1873 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1874 $sth->execute([$owner_uid]);
1876 $check_cats = array_merge(
1877 Feeds
::getParentCategories($cat_id, $owner_uid),
1880 $check_cats_str = join(",", $check_cats);
1881 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1883 while ($line = $sth->fetch()) {
1884 $filter_id = $line["id"];
1886 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1888 $sth2 = $pdo->prepare("SELECT
1889 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1890 FROM ttrss_filters2_rules AS r,
1891 ttrss_filter_types AS t
1893 (match_on IS NOT NULL OR
1894 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1895 (feed_id IS NULL OR feed_id = ?))) AND
1896 filter_type = t.id AND filter_id = ?");
1897 $sth2->execute([$feed_id, $filter_id]);
1902 while ($rule_line = $sth2->fetch()) {
1903 # print_r($rule_line);
1905 if ($rule_line["match_on"]) {
1906 $match_on = json_decode($rule_line["match_on"], true);
1908 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1911 $rule["reg_exp"] = $rule_line["reg_exp"];
1912 $rule["type"] = $rule_line["type_name"];
1913 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1915 array_push($rules, $rule);
1916 } else if (!$match_any_rule) {
1917 // this filter contains a rule that doesn't match to this feed/category combination
1918 // thus filter has to be rejected
1927 $rule["reg_exp"] = $rule_line["reg_exp"];
1928 $rule["type"] = $rule_line["type_name"];
1929 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1931 array_push($rules, $rule);
1935 if (count($rules) > 0) {
1936 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1937 FROM ttrss_filters2_actions AS a,
1938 ttrss_filter_actions AS t
1940 action_id = t.id AND filter_id = ?");
1941 $sth2->execute([$filter_id]);
1943 while ($action_line = $sth2->fetch()) {
1944 # print_r($action_line);
1947 $action["type"] = $action_line["type_name"];
1948 $action["param"] = $action_line["action_param"];
1950 array_push($actions, $action);
1955 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1956 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1957 $filter["rules"] = $rules;
1958 $filter["actions"] = $actions;
1960 if (count($rules) > 0 && count($actions) > 0) {
1961 array_push($filters, $filter);
1968 function get_score_pic($score) {
1970 return "score_high.png";
1971 } else if ($score > 0) {
1972 return "score_half_high.png";
1973 } else if ($score < -100) {
1974 return "score_low.png";
1975 } else if ($score < 0) {
1976 return "score_half_low.png";
1978 return "score_neutral.png";
1982 function init_plugins() {
1983 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1988 function add_feed_category($feed_cat, $parent_cat_id = false) {
1990 if (!$feed_cat) return false;
1992 $feed_cat = mb_substr($feed_cat, 0, 250);
1993 if (!$parent_cat_id) $parent_cat_id = null;
1996 $tr_in_progress = false;
1999 $pdo->beginTransaction();
2000 } catch (Exception
$e) {
2001 $tr_in_progress = true;
2004 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2005 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2006 AND title = :title AND owner_uid = :uid");
2007 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2009 if (!$sth->fetch()) {
2011 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2013 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2015 if (!$tr_in_progress) $pdo->commit();
2026 * Fixes incomplete URLs by prepending "http://".
2027 * Also replaces feed:// with http://, and
2028 * prepends a trailing slash if the url is a domain name only.
2030 * @param string $url Possibly incomplete URL
2032 * @return string Fixed URL.
2034 function fix_url($url) {
2036 // support schema-less urls
2037 if (strpos($url, '//') === 0) {
2038 $url = 'https:' . $url;
2041 if (strpos($url, '://') === false) {
2042 $url = 'http://' . $url;
2043 } else if (substr($url, 0, 5) == 'feed:') {
2044 $url = 'http:' . substr($url, 5);
2047 //prepend slash if the URL has no slash in it
2048 // "http://www.example" -> "http://www.example/"
2049 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2053 //convert IDNA hostname to punycode if possible
2054 if (function_exists("idn_to_ascii")) {
2055 $parts = parse_url($url);
2056 if (mb_detect_encoding($parts['host']) != 'ASCII')
2058 $parts['host'] = idn_to_ascii($parts['host']);
2059 $url = build_url($parts);
2063 if ($url != "http:///")
2069 function validate_feed_url($url) {
2070 $parts = parse_url($url);
2072 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2076 /* function save_email_address($email) {
2077 // FIXME: implement persistent storage of emails
2079 if (!$_SESSION['stored_emails'])
2080 $_SESSION['stored_emails'] = array();
2082 if (!in_array($email, $_SESSION['stored_emails']))
2083 array_push($_SESSION['stored_emails'], $email);
2087 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2089 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2091 $is_cat = bool_to_sql_bool($is_cat);
2095 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2096 WHERE feed_id = ? AND is_cat = ?
2097 AND owner_uid = ?");
2098 $sth->execute([$feed_id, $is_cat, $owner_uid]);
2100 if ($row = $sth->fetch()) {
2101 return $row["access_key"];
2103 $key = uniqid_short();
2105 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2106 (access_key, feed_id, is_cat, owner_uid)
2107 VALUES (?, ?, ?, ?)");
2109 $sth->execute([$key, $feed_id, $is_cat, $owner_uid]);
2115 function get_feeds_from_html($url, $content)
2117 $url = fix_url($url);
2118 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2120 libxml_use_internal_errors(true);
2122 $doc = new DOMDocument();
2123 $doc->loadHTML($content);
2124 $xpath = new DOMXPath($doc);
2125 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2126 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2127 $feedUrls = array();
2128 foreach ($entries as $entry) {
2129 if ($entry->hasAttribute('href')) {
2130 $title = $entry->getAttribute('title');
2132 $title = $entry->getAttribute('type');
2134 $feedUrl = rewrite_relative_url(
2135 $baseUrl, $entry->getAttribute('href')
2137 $feedUrls[$feedUrl] = $title;
2143 function is_html($content) {
2144 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2147 function url_is_html($url, $login = false, $pass = false) {
2148 return is_html(fetch_file_contents($url, false, $login, $pass));
2151 function build_url($parts) {
2152 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2155 function cleanup_url_path($path) {
2156 $path = str_replace("/./", "/", $path);
2157 $path = str_replace("//", "/", $path);
2163 * Converts a (possibly) relative URL to a absolute one.
2165 * @param string $url Base URL (i.e. from where the document is)
2166 * @param string $rel_url Possibly relative URL in the document
2168 * @return string Absolute URL
2170 function rewrite_relative_url($url, $rel_url) {
2171 if (strpos($rel_url, "://") !== false) {
2173 } else if (strpos($rel_url, "//") === 0) {
2174 # protocol-relative URL (rare but they exist)
2176 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2177 # magnet:, feed:, etc
2179 } else if (strpos($rel_url, "/") === 0) {
2180 $parts = parse_url($url);
2181 $parts['path'] = $rel_url;
2182 $parts['path'] = cleanup_url_path($parts['path']);
2184 return build_url($parts);
2187 $parts = parse_url($url);
2188 if (!isset($parts['path'])) {
2189 $parts['path'] = '/';
2191 $dir = $parts['path'];
2192 if (substr($dir, -1) !== '/') {
2193 $dir = dirname($parts['path']);
2194 $dir !== '/' && $dir .= '/';
2196 $parts['path'] = $dir . $rel_url;
2197 $parts['path'] = cleanup_url_path($parts['path']);
2199 return build_url($parts);
2203 function cleanup_tags($days = 14, $limit = 1000) {
2205 $days = (int) $days;
2207 if (DB_TYPE
== "pgsql") {
2208 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2209 } else if (DB_TYPE
== "mysql") {
2210 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2217 while ($limit > 0) {
2220 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2221 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2222 WHERE post_int_id = int_id AND $interval_query AND
2223 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2224 $sth->execute([$limit]);
2228 while ($line = $sth->fetch()) {
2229 array_push($ids, $line['id']);
2232 if (count($ids) > 0) {
2233 $ids = join(",", $ids);
2235 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2236 $tags_deleted = $usth->rowCount();
2241 $limit -= $limit_part;
2244 return $tags_deleted;
2247 function print_user_stylesheet() {
2248 $value = get_pref('USER_STYLESHEET');
2251 print "<style type=\"text/css\">";
2252 print str_replace("<br/>", "\n", $value);
2258 function filter_to_sql($filter, $owner_uid) {
2263 if (DB_TYPE
== "pgsql")
2266 $reg_qpart = "REGEXP";
2268 foreach ($filter["rules"] AS $rule) {
2269 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2270 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2271 $rule['reg_exp']) !== FALSE;
2273 if ($regexp_valid) {
2275 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2277 switch ($rule["type"]) {
2279 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2280 $rule['reg_exp'] . "')";
2283 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2284 $rule['reg_exp'] . "')";
2287 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2288 $rule['reg_exp'] . "') OR LOWER(" .
2289 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2292 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2293 $rule['reg_exp'] . "')";
2296 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2297 $rule['reg_exp'] . "')";
2300 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2301 $rule['reg_exp'] . "')";
2305 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2307 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2308 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2311 if (isset($rule["cat_id"])) {
2313 if ($rule["cat_id"] > 0) {
2314 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2315 array_push($children, $rule["cat_id"]);
2316 $children = array_map("intval", $children);
2318 $children = join(",", $children);
2320 $cat_qpart = "cat_id IN ($children)";
2322 $cat_qpart = "cat_id IS NULL";
2325 $qpart .= " AND $cat_qpart";
2328 $qpart .= " AND feed_id IS NOT NULL";
2330 array_push($query, "($qpart)");
2335 if (count($query) > 0) {
2336 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2338 $fullquery = "(false)";
2341 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2346 if (!function_exists('gzdecode')) {
2347 function gzdecode($string) { // no support for 2nd argument
2348 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2349 base64_encode($string));
2353 function get_random_bytes($length) {
2354 if (function_exists('openssl_random_pseudo_bytes')) {
2355 return openssl_random_pseudo_bytes($length);
2359 for ($i = 0; $i < $length; $i++
)
2360 $output .= chr(mt_rand(0, 255));
2366 function read_stdin() {
2367 $fp = fopen("php://stdin", "r");
2370 $line = trim(fgets($fp));
2378 function implements_interface($class, $interface) {
2379 return in_array($interface, class_implements($class));
2382 function get_minified_js($files) {
2386 foreach ($files as $js) {
2387 if (!isset($_GET['debug'])) {
2388 $cached_file = CACHE_DIR
. "/js/".basename($js);
2390 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2392 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2394 if ($header && $contents) {
2395 list($htag, $hversion) = explode(":", $header);
2397 if ($htag == "tt-rss" && $hversion == VERSION
) {
2404 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2405 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2409 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2416 function calculate_dep_timestamp() {
2417 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2421 foreach ($files as $file) {
2422 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2428 function T_js_decl($s1, $s2) {
2430 $s1 = preg_replace("/\n/", "", $s1);
2431 $s2 = preg_replace("/\n/", "", $s2);
2433 $s1 = preg_replace("/\"/", "\\\"", $s1);
2434 $s2 = preg_replace("/\"/", "\\\"", $s2);
2436 return "T_messages[\"$s1\"] = \"$s2\";\n";
2440 function init_js_translations() {
2442 print 'var T_messages = new Object();
2445 if (T_messages[msg]) {
2446 return T_messages[msg];
2452 function ngettext(msg1, msg2, n) {
2453 return __((parseInt(n) > 1) ? msg2 : msg1);
2456 $l10n = _get_reader();
2458 for ($i = 0; $i < $l10n->total
; $i++
) {
2459 $orig = $l10n->get_original_string($i);
2460 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2461 $key = explode(chr(0), $orig);
2462 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2463 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2465 $translation = __($orig);
2466 print T_js_decl($orig, $translation);
2471 function get_theme_path($theme) {
2472 if ($theme == "default.php")
2473 return "css/default.css";
2475 $check = "themes/$theme";
2476 if (file_exists($check)) return $check;
2478 $check = "themes.local/$theme";
2479 if (file_exists($check)) return $check;
2482 function theme_valid($theme) {
2483 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2485 if (in_array($theme, $bundled_themes)) return true;
2487 $file = "themes/" . basename($theme);
2489 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2491 if (file_exists($file) && is_readable($file)) {
2492 $fh = fopen($file, "r");
2495 $header = fgets($fh);
2498 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2506 * @SuppressWarnings(unused)
2508 function error_json($code) {
2509 require_once "errors.php";
2511 @$message = $ERRORS[$code];
2513 return json_encode(array("error" =>
2514 array("code" => $code, "message" => $message)));
2518 /*function abs_to_rel_path($dir) {
2519 $tmp = str_replace(dirname(__DIR__), "", $dir);
2521 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2526 function get_upload_error_message($code) {
2529 0 => __('There is no error, the file uploaded with success'),
2530 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2531 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2532 3 => __('The uploaded file was only partially uploaded'),
2533 4 => __('No file was uploaded'),
2534 6 => __('Missing a temporary folder'),
2535 7 => __('Failed to write file to disk.'),
2536 8 => __('A PHP extension stopped the file upload.'),
2539 return $errors[$code];
2542 function base64_img($filename) {
2543 if (file_exists($filename)) {
2544 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2546 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2552 /* this is essentially a wrapper for readfile() which allows plugins to hook
2553 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2555 hook function should return true if request was handled (or at least attempted to)
2557 note that this can be called without user context so the plugin to handle this
2558 should be loaded systemwide in config.php */
2559 function send_local_file($filename) {
2560 if (file_exists($filename)) {
2562 if (is_writable($filename)) touch($filename);
2564 $tmppluginhost = new PluginHost();
2566 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2567 $tmppluginhost->load_data();
2569 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2570 if ($plugin->hook_send_local_file($filename)) return true;
2573 $mimetype = mime_content_type($filename);
2575 // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2576 // video files are detected as octet-stream by mime_content_type()
2578 if ($mimetype == "application/octet-stream")
2579 $mimetype = "video/mp4";
2581 header("Content-type: $mimetype");
2583 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2584 header("Last-Modified: $stamp", true);
2586 return readfile($filename);
2592 function check_mysql_tables() {
2595 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2596 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2597 $sth->execute([DB_NAME
]);
2601 while ($line = $sth->fetch()) {
2602 array_push($bad_tables, $line);
2608 function validate_field($string, $allowed, $default = "") {
2609 if (in_array($string, $allowed))
2615 function arr_qmarks($arr) {
2616 return str_repeat('?,', count($arr) - 1) . '?';