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
380 $url = ltrim($url, ' ');
381 $url = str_replace(' ', '%20', $url);
383 if (strpos($url, "//") === 0)
384 $url = 'http:' . $url;
386 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
388 $fetch_curl_used = true;
390 $ch = curl_init($url);
392 if ($last_modified && !$post_query) {
393 curl_setopt($ch, CURLOPT_HTTPHEADER
,
394 array("If-Modified-Since: $last_modified"));
397 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
398 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
399 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
400 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
401 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
402 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
403 curl_setopt($ch, CURLOPT_HEADER
, true);
404 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
405 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
407 curl_setopt($ch, CURLOPT_ENCODING
, "");
408 //curl_setopt($ch, CURLOPT_REFERER, $url);
411 curl_setopt($ch, CURLOPT_NOPROGRESS
, false);
412 curl_setopt($ch, CURLOPT_BUFFERSIZE
, 16384); // needed to get 5 arguments in progress function?
414 // holy shit closures in php
415 // download & upload are *expected* sizes respectively, could be zero
416 curl_setopt($ch, CURLOPT_PROGRESSFUNCTION
, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use( &$max_size) {
417 //_debug("[curl progressfunction] $downloaded $max_size");
419 return ($downloaded > $max_size) ?
1 : 0; // if max size is set, abort when exceeding it
424 if (!ini_get("open_basedir")) {
425 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
428 if (defined('_HTTP_PROXY')) {
429 curl_setopt($ch, CURLOPT_PROXY
, _HTTP_PROXY
);
433 curl_setopt($ch, CURLOPT_POST
, true);
434 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
438 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
440 $ret = @curl_exec
($ch);
442 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE
);
443 $headers = explode("\r\n", substr($ret, 0, $headers_length));
444 $contents = substr($ret, $headers_length);
446 foreach ($headers as $header) {
447 if (strstr($header, ": ") !== FALSE) {
448 list ($key, $value) = explode(": ", $header);
450 if (strtolower($key) == "last-modified") {
451 $fetch_last_modified = $value;
455 if (substr(strtolower($header), 0, 7) == 'http/1.') {
456 $fetch_last_error_code = (int) substr($header, 9, 3);
457 $fetch_last_error = $header;
461 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
462 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
463 $contents = @curl_exec
($ch);
466 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
467 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
469 $fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL
);
471 $fetch_last_error_code = $http_code;
473 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
475 if (curl_errno($ch) != 0) {
476 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
479 $fetch_last_error_content = $contents;
485 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
495 $fetch_curl_used = false;
497 if ($login && $pass){
498 $url_parts = array();
500 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
502 $pass = urlencode($pass);
504 if ($url_parts[1] && $url_parts[2]) {
505 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
509 // TODO: should this support POST requests or not? idk
511 $context_options = array(
517 'ignore_errors' => true,
518 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
519 'protocol_version'=> 1.1)
522 if (!$post_query && $last_modified) {
523 array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
526 if (defined('_HTTP_PROXY')) {
527 $context_options['http']['request_fulluri'] = true;
528 $context_options['http']['proxy'] = _HTTP_PROXY
;
531 $context = stream_context_create($context_options);
533 $old_error = error_get_last();
535 $fetch_effective_url = $url;
537 $data = @file_get_contents
($url, false, $context);
539 if (isset($http_response_header) && is_array($http_response_header)) {
540 foreach ($http_response_header as $header) {
541 if (strstr($header, ": ") !== FALSE) {
542 list ($key, $value) = explode(": ", $header);
544 $key = strtolower($key);
546 if ($key == 'content-type') {
547 $fetch_last_content_type = $value;
548 // don't abort here b/c there might be more than one
549 // e.g. if we were being redirected -- last one is the right one
550 } else if ($key == 'last-modified') {
551 $fetch_last_modified = $value;
552 } else if ($key == 'location') {
553 $fetch_effective_url = $value;
557 if (substr(strtolower($header), 0, 7) == 'http/1.') {
558 $fetch_last_error_code = (int) substr($header, 9, 3);
559 $fetch_last_error = $header;
564 if ($fetch_last_error_code != 200) {
565 $error = error_get_last();
567 if ($error['message'] != $old_error['message']) {
568 $fetch_last_error .= "; " . $error["message"];
571 $fetch_last_error_content = $data;
581 * Try to determine the favicon URL for a feed.
582 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
583 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
585 * @param string $url A feed or page URL
587 * @return mixed The favicon URL, or false if none was found.
589 function get_favicon_url($url) {
591 $favicon_url = false;
593 if ($html = @fetch_file_contents
($url)) {
595 libxml_use_internal_errors(true);
597 $doc = new DOMDocument();
598 $doc->loadHTML($html);
599 $xpath = new DOMXPath($doc);
601 $base = $xpath->query('/html/head/base[@href]');
602 foreach ($base as $b) {
603 $url = rewrite_relative_url($url, $b->getAttribute("href"));
607 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
608 if (count($entries) > 0) {
609 foreach ($entries as $entry) {
610 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
617 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
620 } // function get_favicon_url
622 function initialize_user_prefs($uid, $profile = false) {
624 if (get_schema_version() < 63) $profile_qpart = "";
627 $in_nested_tr = false;
630 $pdo->beginTransaction();
631 } catch (Exception
$e) {
632 $in_nested_tr = true;
635 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
637 $profile = $profile ?
$profile : null;
639 $u_sth = $pdo->prepare("SELECT pref_name
640 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
641 (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
642 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
644 $active_prefs = array();
646 while ($line = $u_sth->fetch()) {
647 array_push($active_prefs, $line["pref_name"]);
650 while ($line = $sth->fetch()) {
651 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
652 // print "adding " . $line["pref_name"] . "<br>";
654 if (get_schema_version() < 63) {
655 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
656 (owner_uid,pref_name,value) VALUES
658 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
661 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
662 (owner_uid,pref_name,value, profile) VALUES
664 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
670 if (!$in_nested_tr) $pdo->commit();
674 function get_ssl_certificate_id() {
675 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
676 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
677 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
678 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
679 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
681 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
682 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
683 $_SERVER["SSL_CLIENT_V_START"] .
684 $_SERVER["SSL_CLIENT_V_END"] .
685 $_SERVER["SSL_CLIENT_S_DN"]);
690 function authenticate_user($login, $password, $check_only = false) {
692 if (!SINGLE_USER_MODE
) {
695 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
697 $user_id = (int) $plugin->authenticate($login, $password);
700 $_SESSION["auth_module"] = strtolower(get_class($plugin));
705 if ($user_id && !$check_only) {
708 $_SESSION["uid"] = $user_id;
709 $_SESSION["version"] = VERSION_STATIC
;
712 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
714 $sth->execute([$user_id]);
715 $row = $sth->fetch();
717 $_SESSION["name"] = $row["login"];
718 $_SESSION["access_level"] = $row["access_level"];
719 $_SESSION["csrf_token"] = uniqid_short();
721 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
722 $usth->execute([$user_id]);
724 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
725 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
726 $_SESSION["pwd_hash"] = $row["pwd_hash"];
728 $_SESSION["last_version_check"] = time();
730 initialize_user_prefs($_SESSION["uid"]);
739 $_SESSION["uid"] = 1;
740 $_SESSION["name"] = "admin";
741 $_SESSION["access_level"] = 10;
743 $_SESSION["hide_hello"] = true;
744 $_SESSION["hide_logout"] = true;
746 $_SESSION["auth_module"] = false;
748 if (!$_SESSION["csrf_token"]) {
749 $_SESSION["csrf_token"] = uniqid_short();
752 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
754 initialize_user_prefs($_SESSION["uid"]);
760 // this is used for user http parameters unless HTML code is actually needed
761 function clean($param) {
762 if (is_array($param)) {
763 return array_map("strip_tags", $param);
764 } else if (is_string($param)) {
765 return strip_tags($param);
771 function make_password($length = 8) {
774 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
778 while ($i < $length) {
779 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
781 if (!strstr($password, $char)) {
789 // this is called after user is created to initialize default feeds, labels
792 // user preferences are checked on every login, not here
794 function initialize_user($uid) {
798 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
799 values (?, 'Tiny Tiny RSS: Forum',
800 'http://tt-rss.org/forum/rss.php')");
801 $sth->execute([$uid]);
804 function logout_user() {
806 if (isset($_COOKIE[session_name()])) {
807 setcookie(session_name(), '', time()-42000, '/');
811 function validate_csrf($csrf_token) {
812 return $csrf_token == $_SESSION['csrf_token'];
815 function load_user_plugins($owner_uid, $pluginhost = false) {
817 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
819 if ($owner_uid && SCHEMA_VERSION
>= 100) {
820 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
822 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
824 if (get_schema_version() > 100) {
825 $pluginhost->load_data();
830 function login_sequence() {
833 if (SINGLE_USER_MODE
) {
835 authenticate_user("admin", null);
837 load_user_plugins($_SESSION["uid"]);
839 if (!validate_session()) $_SESSION["uid"] = false;
841 if (!$_SESSION["uid"]) {
843 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
844 $_SESSION["ref_schema_version"] = get_schema_version(true);
846 authenticate_user(null, null, true);
849 if (!$_SESSION["uid"]) {
851 setcookie(session_name(), '', time()-42000, '/');
858 /* bump login timestamp */
859 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
860 $sth->execute([$_SESSION['uid']]);
862 $_SESSION["last_login_update"] = time();
865 if ($_SESSION["uid"]) {
867 load_user_plugins($_SESSION["uid"]);
871 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
873 (SELECT COUNT(id) FROM ttrss_feeds WHERE
874 ttrss_feeds.id = feed_id) = 0");
876 $sth->execute([$_SESSION['uid']]);
878 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
880 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
881 ttrss_feed_categories.id = feed_id) = 0");
883 $sth->execute([$_SESSION['uid']]);
889 function truncate_string($str, $max_len, $suffix = '…') {
890 if (mb_strlen($str, "utf-8") > $max_len) {
891 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
898 function truncate_middle($str, $max_len, $suffix = '…') {
899 if (strlen($str) > $max_len) {
900 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
906 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
909 $source_tz = new DateTimeZone($source_tz);
910 } catch (Exception
$e) {
911 $source_tz = new DateTimeZone('UTC');
915 $dest_tz = new DateTimeZone($dest_tz);
916 } catch (Exception
$e) {
917 $dest_tz = new DateTimeZone('UTC');
920 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
921 return $dt->format('U') +
$dest_tz->getOffset($dt);
924 function make_local_datetime($timestamp, $long, $owner_uid = false,
925 $no_smart_dt = false, $eta_min = false) {
927 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
928 if (!$timestamp) $timestamp = '1970-01-01 0:00';
933 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
935 $timestamp = substr($timestamp, 0, 19);
937 # We store date in UTC internally
938 $dt = new DateTime($timestamp, $utc_tz);
940 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
942 if ($user_tz_string != 'Automatic') {
945 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
946 } catch (Exception
$e) {
950 $tz_offset = $user_tz->getOffset($dt);
952 $tz_offset = (int) -$_SESSION["clientTzOffset"];
955 $user_timestamp = $dt->format('U') +
$tz_offset;
958 return smart_date_time($user_timestamp,
959 $tz_offset, $owner_uid, $eta_min);
962 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
964 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
966 return date($format, $user_timestamp);
970 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
971 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
973 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
974 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
975 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
976 return date("G:i", $timestamp);
977 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
978 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
979 return date($format, $timestamp);
981 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
982 return date($format, $timestamp);
986 function sql_bool_to_bool($s) {
987 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
990 function bool_to_sql_bool($s) {
994 // Session caching removed due to causing wrong redirects to upgrade
995 // script when get_schema_version() is called on an obsolete session
996 // created on a previous schema version.
997 function get_schema_version($nocache = false) {
998 global $schema_version;
1002 if (!$schema_version && !$nocache) {
1003 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
1004 $version = $row["schema_version"];
1005 $schema_version = $version;
1008 return $schema_version;
1012 function sanity_check() {
1013 require_once 'errors.php';
1017 $schema_version = get_schema_version(true);
1019 if ($schema_version != SCHEMA_VERSION
) {
1023 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1026 function file_is_locked($filename) {
1027 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
1028 if (function_exists('flock')) {
1029 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1031 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1032 flock($fp, LOCK_UN
);
1042 return true; // consider the file always locked and skip the test
1049 function make_lockfile($filename) {
1050 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1052 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1053 $stat_h = fstat($fp);
1054 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1056 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1057 if ($stat_h["ino"] != $stat_f["ino"] ||
1058 $stat_h["dev"] != $stat_f["dev"]) {
1064 if (function_exists('posix_getpid')) {
1065 fwrite($fp, posix_getpid() . "\n");
1073 function make_stampfile($filename) {
1074 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1076 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1077 fwrite($fp, time() . "\n");
1078 flock($fp, LOCK_UN
);
1086 function sql_random_function() {
1087 if (DB_TYPE
== "mysql") {
1094 function getFeedUnread($feed, $is_cat = false) {
1095 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1098 function checkbox_to_sql_bool($val) {
1099 return ($val == "on") ?
1 : 0;
1102 function uniqid_short() {
1103 return uniqid(base_convert(rand(), 10, 36));
1106 function make_init_params() {
1109 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1110 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1111 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1112 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1114 $params[strtolower($param)] = (int) get_pref($param);
1117 $params["icons_url"] = ICONS_URL
;
1118 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1119 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1120 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1121 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1122 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1123 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1124 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1126 $theme = get_pref( "USER_CSS_THEME", false, false);
1127 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1129 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1131 $params["php_platform"] = PHP_OS
;
1132 $params["php_version"] = PHP_VERSION
;
1134 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1138 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1139 ttrss_feeds WHERE owner_uid = ?");
1140 $sth->execute([$_SESSION['uid']]);
1141 $row = $sth->fetch();
1143 $max_feed_id = $row["mid"];
1144 $num_feeds = $row["nf"];
1146 $params["max_feed_id"] = (int) $max_feed_id;
1147 $params["num_feeds"] = (int) $num_feeds;
1149 $params["hotkeys"] = get_hotkeys_map();
1151 $params["csrf_token"] = $_SESSION["csrf_token"];
1152 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1154 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1156 $params["icon_alert"] = base64_img("images/alert.png");
1157 $params["icon_information"] = base64_img("images/information.png");
1158 $params["icon_cross"] = base64_img("images/cross.png");
1159 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1161 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1166 function get_hotkeys_info() {
1168 __("Navigation") => array(
1169 "next_feed" => __("Open next feed"),
1170 "prev_feed" => __("Open previous feed"),
1171 "next_article" => __("Open next article"),
1172 "prev_article" => __("Open previous article"),
1173 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1174 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1175 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1176 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1177 "search_dialog" => __("Show search dialog")),
1178 __("Article") => array(
1179 "toggle_mark" => __("Toggle starred"),
1180 "toggle_publ" => __("Toggle published"),
1181 "toggle_unread" => __("Toggle unread"),
1182 "edit_tags" => __("Edit tags"),
1183 "open_in_new_window" => __("Open in new window"),
1184 "catchup_below" => __("Mark below as read"),
1185 "catchup_above" => __("Mark above as read"),
1186 "article_scroll_down" => __("Scroll down"),
1187 "article_scroll_up" => __("Scroll up"),
1188 "select_article_cursor" => __("Select article under cursor"),
1189 "email_article" => __("Email article"),
1190 "close_article" => __("Close/collapse article"),
1191 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1192 "toggle_widescreen" => __("Toggle widescreen mode"),
1193 "toggle_embed_original" => __("Toggle embed original")),
1194 __("Article selection") => array(
1195 "select_all" => __("Select all articles"),
1196 "select_unread" => __("Select unread"),
1197 "select_marked" => __("Select starred"),
1198 "select_published" => __("Select published"),
1199 "select_invert" => __("Invert selection"),
1200 "select_none" => __("Deselect everything")),
1201 __("Feed") => array(
1202 "feed_refresh" => __("Refresh current feed"),
1203 "feed_unhide_read" => __("Un/hide read feeds"),
1204 "feed_subscribe" => __("Subscribe to feed"),
1205 "feed_edit" => __("Edit feed"),
1206 "feed_catchup" => __("Mark as read"),
1207 "feed_reverse" => __("Reverse headlines"),
1208 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1209 "feed_debug_update" => __("Debug feed update"),
1210 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1211 "catchup_all" => __("Mark all feeds as read"),
1212 "cat_toggle_collapse" => __("Un/collapse current category"),
1213 "toggle_combined_mode" => __("Toggle combined mode"),
1214 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1215 __("Go to") => array(
1216 "goto_all" => __("All articles"),
1217 "goto_fresh" => __("Fresh"),
1218 "goto_marked" => __("Starred"),
1219 "goto_published" => __("Published"),
1220 "goto_tagcloud" => __("Tag cloud"),
1221 "goto_prefs" => __("Preferences")),
1222 __("Other") => array(
1223 "create_label" => __("Create label"),
1224 "create_filter" => __("Create filter"),
1225 "collapse_sidebar" => __("Un/collapse sidebar"),
1226 "help_dialog" => __("Show help dialog"))
1229 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1230 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1236 function get_hotkeys_map() {
1238 // "navigation" => array(
1241 "n" => "next_article",
1242 "p" => "prev_article",
1243 "(38)|up" => "prev_article",
1244 "(40)|down" => "next_article",
1245 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1246 // "^(40)|Ctrl-down" => "next_article_noscroll",
1247 "(191)|/" => "search_dialog",
1248 // "article" => array(
1249 "s" => "toggle_mark",
1250 "*s" => "toggle_publ",
1251 "u" => "toggle_unread",
1252 "*t" => "edit_tags",
1253 "o" => "open_in_new_window",
1254 "c p" => "catchup_below",
1255 "c n" => "catchup_above",
1256 "*n" => "article_scroll_down",
1257 "*p" => "article_scroll_up",
1258 "*(38)|Shift+up" => "article_scroll_up",
1259 "*(40)|Shift+down" => "article_scroll_down",
1260 "a *w" => "toggle_widescreen",
1261 "a e" => "toggle_embed_original",
1262 "e" => "email_article",
1263 "a q" => "close_article",
1264 // "article_selection" => array(
1265 "a a" => "select_all",
1266 "a u" => "select_unread",
1267 "a *u" => "select_marked",
1268 "a p" => "select_published",
1269 "a i" => "select_invert",
1270 "a n" => "select_none",
1272 "f r" => "feed_refresh",
1273 "f a" => "feed_unhide_read",
1274 "f s" => "feed_subscribe",
1275 "f e" => "feed_edit",
1276 "f q" => "feed_catchup",
1277 "f x" => "feed_reverse",
1278 "f g" => "feed_toggle_vgroup",
1279 "f *d" => "feed_debug_update",
1280 "f *g" => "feed_debug_viewfeed",
1281 "f *c" => "toggle_combined_mode",
1282 "f c" => "toggle_cdm_expanded",
1283 "*q" => "catchup_all",
1284 "x" => "cat_toggle_collapse",
1286 "g a" => "goto_all",
1287 "g f" => "goto_fresh",
1288 "g s" => "goto_marked",
1289 "g p" => "goto_published",
1290 "g t" => "goto_tagcloud",
1291 "g *p" => "goto_prefs",
1292 // "other" => array(
1293 "(9)|Tab" => "select_article_cursor", // tab
1294 "c l" => "create_label",
1295 "c f" => "create_filter",
1296 "c s" => "collapse_sidebar",
1297 "^(191)|Ctrl+/" => "help_dialog",
1300 if (get_pref('COMBINED_DISPLAY_MODE')) {
1301 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1302 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1305 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1306 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1309 $prefixes = array();
1311 foreach (array_keys($hotkeys) as $hotkey) {
1312 $pair = explode(" ", $hotkey, 2);
1314 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1315 array_push($prefixes, $pair[0]);
1319 return array($prefixes, $hotkeys);
1322 function check_for_update() {
1323 if (defined("GIT_VERSION_TIMESTAMP")) {
1324 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1327 $content = json_decode($content, true);
1329 if ($content && isset($content["changeset"])) {
1330 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1331 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1333 return $content["changeset"]["id"];
1342 function make_runtime_info($disable_update_check = false) {
1347 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1348 ttrss_feeds WHERE owner_uid = ?");
1349 $sth->execute([$_SESSION['uid']]);
1350 $row = $sth->fetch();
1352 $max_feed_id = $row['mid'];
1353 $num_feeds = $row['nf'];
1355 $data["max_feed_id"] = (int) $max_feed_id;
1356 $data["num_feeds"] = (int) $num_feeds;
1358 $data['last_article_id'] = Article
::getLastArticleId();
1359 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1361 $data['dep_ts'] = calculate_dep_timestamp();
1362 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1364 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1366 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1367 $update_result = @check_for_update
();
1369 $data["update_result"] = $update_result;
1371 $_SESSION["last_version_check"] = time();
1374 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1376 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1378 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1380 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1383 $stamp_delta = time() - $stamp;
1385 if ($stamp_delta > 1800) {
1389 $_SESSION["daemon_stamp_check"] = time();
1392 $data['daemon_stamp_ok'] = $stamp_check;
1394 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1396 $data['daemon_stamp'] = $stamp_fmt;
1404 function search_to_sql($search, $search_language) {
1406 $keywords = str_getcsv(trim($search), " ");
1407 $query_keywords = array();
1408 $search_words = array();
1409 $search_query_leftover = array();
1413 if ($search_language)
1414 $search_language = $pdo->quote(mb_strtolower($search_language));
1416 $search_language = $pdo->quote("english");
1418 foreach ($keywords as $k) {
1419 if (strpos($k, "-") === 0) {
1426 $commandpair = explode(":", mb_strtolower($k), 2);
1428 switch ($commandpair[0]) {
1430 if ($commandpair[1]) {
1431 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1432 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1434 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1435 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1436 array_push($search_words, $k);
1440 if ($commandpair[1]) {
1441 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1442 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1444 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1445 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1446 array_push($search_words, $k);
1450 if ($commandpair[1]) {
1451 if ($commandpair[1] == "true")
1452 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1453 else if ($commandpair[1] == "false")
1454 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1456 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1457 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1459 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1460 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1461 if (!$not) array_push($search_words, $k);
1466 if ($commandpair[1]) {
1467 if ($commandpair[1] == "true")
1468 array_push($query_keywords, "($not (marked = true))");
1470 array_push($query_keywords, "($not (marked = false))");
1472 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1473 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1474 if (!$not) array_push($search_words, $k);
1478 if ($commandpair[1]) {
1479 if ($commandpair[1] == "true")
1480 array_push($query_keywords, "($not (published = true))");
1482 array_push($query_keywords, "($not (published = false))");
1485 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1486 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1487 if (!$not) array_push($search_words, $k);
1491 if ($commandpair[1]) {
1492 if ($commandpair[1] == "true")
1493 array_push($query_keywords, "($not (unread = true))");
1495 array_push($query_keywords, "($not (unread = false))");
1498 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1499 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1500 if (!$not) array_push($search_words, $k);
1504 if (strpos($k, "@") === 0) {
1506 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1507 $orig_ts = strtotime(substr($k, 1));
1508 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1510 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1512 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1515 if (DB_TYPE
== "pgsql") {
1516 $k = mb_strtolower($k);
1517 array_push($search_query_leftover, $not ?
"!$k" : $k);
1519 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1520 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1523 if (!$not) array_push($search_words, $k);
1528 if (count($search_query_leftover) > 0) {
1529 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1531 if (DB_TYPE
== "pgsql") {
1532 array_push($query_keywords,
1533 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1538 $search_query_part = implode("AND", $query_keywords);
1540 return array($search_query_part, $search_words);
1543 function iframe_whitelisted($entry) {
1544 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1546 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1549 foreach ($whitelist as $w) {
1550 if ($src == $w ||
$src == "www.$w")
1558 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1559 if (!$owner) $owner = $_SESSION["uid"];
1561 $res = trim($str); if (!$res) return '';
1563 $charset_hack = '<head>
1564 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1567 $res = trim($res); if (!$res) return '';
1569 libxml_use_internal_errors(true);
1571 $doc = new DOMDocument();
1572 $doc->loadHTML($charset_hack . $res);
1573 $xpath = new DOMXPath($doc);
1575 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1577 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1579 foreach ($entries as $entry) {
1581 if ($entry->hasAttribute('href')) {
1582 $entry->setAttribute('href',
1583 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1585 $entry->setAttribute('rel', 'noopener noreferrer');
1588 if ($entry->hasAttribute('src')) {
1589 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1590 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1592 if (file_exists($cached_filename)) {
1594 // this is strictly cosmetic
1595 if ($entry->tagName
== 'img') {
1597 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1599 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1605 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1607 if ($entry->hasAttribute('srcset')) {
1608 $entry->removeAttribute('srcset');
1611 if ($entry->hasAttribute('sizes')) {
1612 $entry->removeAttribute('sizes');
1616 $entry->setAttribute('src', $src);
1619 if ($entry->nodeName
== 'img') {
1620 $entry->setAttribute('referrerpolicy', 'no-referrer');
1622 $entry->removeAttribute('width');
1623 $entry->removeAttribute('height');
1625 if ($entry->hasAttribute('src')) {
1626 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1628 if (is_prefix_https() && !$is_https_url) {
1630 if ($entry->hasAttribute('srcset')) {
1631 $entry->removeAttribute('srcset');
1634 if ($entry->hasAttribute('sizes')) {
1635 $entry->removeAttribute('sizes');
1640 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1641 $force_remove_images ||
$_SESSION["bw_limit"]) {
1643 $p = $doc->createElement('p');
1645 $a = $doc->createElement('a');
1646 $a->setAttribute('href', $entry->getAttribute('src'));
1648 $a->appendChild(new DOMText($entry->getAttribute('src')));
1649 $a->setAttribute('target', '_blank');
1650 $a->setAttribute('rel', 'noopener noreferrer');
1652 $p->appendChild($a);
1654 $entry->parentNode
->replaceChild($p, $entry);
1658 if (strtolower($entry->nodeName
) == "a") {
1659 $entry->setAttribute("target", "_blank");
1660 $entry->setAttribute("rel", "noopener noreferrer");
1664 $entries = $xpath->query('//iframe');
1665 foreach ($entries as $entry) {
1666 if (!iframe_whitelisted($entry)) {
1667 $entry->setAttribute('sandbox', 'allow-scripts');
1669 if (is_prefix_https()) {
1670 $entry->setAttribute("src",
1671 str_replace("http://", "https://",
1672 $entry->getAttribute("src")));
1677 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1678 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1679 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1680 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1681 'dt', 'em', 'footer', 'figure', 'figcaption',
1682 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1683 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1684 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1685 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1686 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1687 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1689 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1691 $disallowed_attributes = array('id', 'style', 'class');
1693 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1694 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1695 if (is_array($retval)) {
1697 $allowed_elements = $retval[1];
1698 $disallowed_attributes = $retval[2];
1704 $doc->removeChild($doc->firstChild
); //remove doctype
1705 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1707 if ($highlight_words) {
1708 foreach ($highlight_words as $word) {
1710 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1712 $elements = $xpath->query("//*/text()");
1714 foreach ($elements as $child) {
1716 $fragment = $doc->createDocumentFragment();
1717 $text = $child->textContent
;
1719 while (($pos = mb_stripos($text, $word)) !== false) {
1720 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1721 $word = mb_substr($text, $pos, mb_strlen($word));
1722 $highlight = $doc->createElement('span');
1723 $highlight->appendChild(new DomText($word));
1724 $highlight->setAttribute('class', 'highlight');
1725 $fragment->appendChild($highlight);
1726 $text = mb_substr($text, $pos +
mb_strlen($word));
1729 if (!empty($text)) $fragment->appendChild(new DomText($text));
1731 $child->parentNode
->replaceChild($fragment, $child);
1736 $res = $doc->saveHTML();
1738 /* strip everything outside of <body>...</body> */
1740 $res_frag = array();
1741 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1742 return $res_frag[1];
1748 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1749 $xpath = new DOMXPath($doc);
1750 $entries = $xpath->query('//*');
1752 foreach ($entries as $entry) {
1753 if (!in_array($entry->nodeName
, $allowed_elements)) {
1754 $entry->parentNode
->removeChild($entry);
1757 if ($entry->hasAttributes()) {
1758 $attrs_to_remove = array();
1760 foreach ($entry->attributes
as $attr) {
1762 if (strpos($attr->nodeName
, 'on') === 0) {
1763 array_push($attrs_to_remove, $attr);
1766 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1767 array_push($attrs_to_remove, $attr);
1770 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1771 array_push($attrs_to_remove, $attr);
1775 foreach ($attrs_to_remove as $attr) {
1776 $entry->removeAttributeNode($attr);
1784 function trim_array($array) {
1786 array_walk($tmp, 'trim');
1790 function tag_is_valid($tag) {
1791 if (!$tag ||
is_numeric($tag) ||
mb_strlen($tag) > 250)
1797 function render_login_form() {
1798 header('Cache-Control: public');
1800 require_once "login_form.php";
1804 function T_sprintf() {
1805 $args = func_get_args();
1806 return vsprintf(__(array_shift($args)), $args);
1809 function print_checkpoint($n, $s) {
1810 $ts = microtime(true);
1811 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1815 function sanitize_tag($tag) {
1818 $tag = mb_strtolower($tag, 'utf-8');
1820 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1822 if (DB_TYPE
== "mysql") {
1823 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1829 function is_server_https() {
1830 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1833 function is_prefix_https() {
1834 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1837 // this returns SELF_URL_PATH sans ending slash
1838 function get_self_url_prefix() {
1839 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1840 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1842 return SELF_URL_PATH
;
1846 function encrypt_password($pass, $salt = '', $mode2 = false) {
1847 if ($salt && $mode2) {
1848 return "MODE2:" . hash('sha256', $salt . $pass);
1850 return "SHA1X:" . sha1("$salt:$pass");
1852 return "SHA1:" . sha1($pass);
1854 } // function encrypt_password
1856 function load_filters($feed_id, $owner_uid) {
1859 $feed_id = (int) $feed_id;
1860 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1863 $null_cat_qpart = "cat_id IS NULL OR";
1865 $null_cat_qpart = "";
1869 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1870 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1871 $sth->execute([$owner_uid]);
1873 $check_cats = array_merge(
1874 Feeds
::getParentCategories($cat_id, $owner_uid),
1877 $check_cats_str = join(",", $check_cats);
1878 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1880 while ($line = $sth->fetch()) {
1881 $filter_id = $line["id"];
1883 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1885 $sth2 = $pdo->prepare("SELECT
1886 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1887 FROM ttrss_filters2_rules AS r,
1888 ttrss_filter_types AS t
1890 (match_on IS NOT NULL OR
1891 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1892 (feed_id IS NULL OR feed_id = ?))) AND
1893 filter_type = t.id AND filter_id = ?");
1894 $sth2->execute([$feed_id, $filter_id]);
1899 while ($rule_line = $sth2->fetch()) {
1900 # print_r($rule_line);
1902 if ($rule_line["match_on"]) {
1903 $match_on = json_decode($rule_line["match_on"], true);
1905 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1908 $rule["reg_exp"] = $rule_line["reg_exp"];
1909 $rule["type"] = $rule_line["type_name"];
1910 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1912 array_push($rules, $rule);
1913 } else if (!$match_any_rule) {
1914 // this filter contains a rule that doesn't match to this feed/category combination
1915 // thus filter has to be rejected
1924 $rule["reg_exp"] = $rule_line["reg_exp"];
1925 $rule["type"] = $rule_line["type_name"];
1926 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1928 array_push($rules, $rule);
1932 if (count($rules) > 0) {
1933 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1934 FROM ttrss_filters2_actions AS a,
1935 ttrss_filter_actions AS t
1937 action_id = t.id AND filter_id = ?");
1938 $sth2->execute([$filter_id]);
1940 while ($action_line = $sth2->fetch()) {
1941 # print_r($action_line);
1944 $action["type"] = $action_line["type_name"];
1945 $action["param"] = $action_line["action_param"];
1947 array_push($actions, $action);
1952 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1953 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1954 $filter["rules"] = $rules;
1955 $filter["actions"] = $actions;
1957 if (count($rules) > 0 && count($actions) > 0) {
1958 array_push($filters, $filter);
1965 function get_score_pic($score) {
1967 return "score_high.png";
1968 } else if ($score > 0) {
1969 return "score_half_high.png";
1970 } else if ($score < -100) {
1971 return "score_low.png";
1972 } else if ($score < 0) {
1973 return "score_half_low.png";
1975 return "score_neutral.png";
1979 function init_plugins() {
1980 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1985 function add_feed_category($feed_cat, $parent_cat_id = false) {
1987 if (!$feed_cat) return false;
1989 $feed_cat = mb_substr($feed_cat, 0, 250);
1990 if (!$parent_cat_id) $parent_cat_id = null;
1993 $tr_in_progress = false;
1996 $pdo->beginTransaction();
1997 } catch (Exception
$e) {
1998 $tr_in_progress = true;
2001 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2002 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2003 AND title = :title AND owner_uid = :uid");
2004 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2006 if (!$sth->fetch()) {
2008 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2010 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2012 if (!$tr_in_progress) $pdo->commit();
2023 * Fixes incomplete URLs by prepending "http://".
2024 * Also replaces feed:// with http://, and
2025 * prepends a trailing slash if the url is a domain name only.
2027 * @param string $url Possibly incomplete URL
2029 * @return string Fixed URL.
2031 function fix_url($url) {
2033 // support schema-less urls
2034 if (strpos($url, '//') === 0) {
2035 $url = 'https:' . $url;
2038 if (strpos($url, '://') === false) {
2039 $url = 'http://' . $url;
2040 } else if (substr($url, 0, 5) == 'feed:') {
2041 $url = 'http:' . substr($url, 5);
2044 //prepend slash if the URL has no slash in it
2045 // "http://www.example" -> "http://www.example/"
2046 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2050 //convert IDNA hostname to punycode if possible
2051 if (function_exists("idn_to_ascii")) {
2052 $parts = parse_url($url);
2053 if (mb_detect_encoding($parts['host']) != 'ASCII')
2055 $parts['host'] = idn_to_ascii($parts['host']);
2056 $url = build_url($parts);
2060 if ($url != "http:///")
2066 function validate_feed_url($url) {
2067 $parts = parse_url($url);
2069 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2073 /* function save_email_address($email) {
2074 // FIXME: implement persistent storage of emails
2076 if (!$_SESSION['stored_emails'])
2077 $_SESSION['stored_emails'] = array();
2079 if (!in_array($email, $_SESSION['stored_emails']))
2080 array_push($_SESSION['stored_emails'], $email);
2084 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2086 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2088 $is_cat = bool_to_sql_bool($is_cat);
2092 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2093 WHERE feed_id = ? AND is_cat = ?
2094 AND owner_uid = ?");
2095 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2097 if ($row = $sth->fetch()) {
2098 return $row["access_key"];
2100 $key = uniqid_short();
2102 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2103 (access_key, feed_id, is_cat, owner_uid)
2104 VALUES (?, ?, ?, ?)");
2106 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2112 function get_feeds_from_html($url, $content)
2114 $url = fix_url($url);
2115 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2117 libxml_use_internal_errors(true);
2119 $doc = new DOMDocument();
2120 $doc->loadHTML($content);
2121 $xpath = new DOMXPath($doc);
2122 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2123 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2124 $feedUrls = array();
2125 foreach ($entries as $entry) {
2126 if ($entry->hasAttribute('href')) {
2127 $title = $entry->getAttribute('title');
2129 $title = $entry->getAttribute('type');
2131 $feedUrl = rewrite_relative_url(
2132 $baseUrl, $entry->getAttribute('href')
2134 $feedUrls[$feedUrl] = $title;
2140 function is_html($content) {
2141 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2144 function url_is_html($url, $login = false, $pass = false) {
2145 return is_html(fetch_file_contents($url, false, $login, $pass));
2148 function build_url($parts) {
2149 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2152 function cleanup_url_path($path) {
2153 $path = str_replace("/./", "/", $path);
2154 $path = str_replace("//", "/", $path);
2160 * Converts a (possibly) relative URL to a absolute one.
2162 * @param string $url Base URL (i.e. from where the document is)
2163 * @param string $rel_url Possibly relative URL in the document
2165 * @return string Absolute URL
2167 function rewrite_relative_url($url, $rel_url) {
2168 if (strpos($rel_url, "://") !== false) {
2170 } else if (strpos($rel_url, "//") === 0) {
2171 # protocol-relative URL (rare but they exist)
2173 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2174 # magnet:, feed:, etc
2176 } else if (strpos($rel_url, "/") === 0) {
2177 $parts = parse_url($url);
2178 $parts['path'] = $rel_url;
2179 $parts['path'] = cleanup_url_path($parts['path']);
2181 return build_url($parts);
2184 $parts = parse_url($url);
2185 if (!isset($parts['path'])) {
2186 $parts['path'] = '/';
2188 $dir = $parts['path'];
2189 if (substr($dir, -1) !== '/') {
2190 $dir = dirname($parts['path']);
2191 $dir !== '/' && $dir .= '/';
2193 $parts['path'] = $dir . $rel_url;
2194 $parts['path'] = cleanup_url_path($parts['path']);
2196 return build_url($parts);
2200 function cleanup_tags($days = 14, $limit = 1000) {
2202 $days = (int) $days;
2204 if (DB_TYPE
== "pgsql") {
2205 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2206 } else if (DB_TYPE
== "mysql") {
2207 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2214 while ($limit > 0) {
2217 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2218 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2219 WHERE post_int_id = int_id AND $interval_query AND
2220 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2221 $sth->execute([$limit]);
2225 while ($line = $sth->fetch()) {
2226 array_push($ids, $line['id']);
2229 if (count($ids) > 0) {
2230 $ids = join(",", $ids);
2232 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2233 $tags_deleted = $usth->rowCount();
2238 $limit -= $limit_part;
2241 return $tags_deleted;
2244 function print_user_stylesheet() {
2245 $value = get_pref('USER_STYLESHEET');
2248 print "<style type=\"text/css\">";
2249 print str_replace("<br/>", "\n", $value);
2255 function filter_to_sql($filter, $owner_uid) {
2260 if (DB_TYPE
== "pgsql")
2263 $reg_qpart = "REGEXP";
2265 foreach ($filter["rules"] AS $rule) {
2266 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2267 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2268 $rule['reg_exp']) !== FALSE;
2270 if ($regexp_valid) {
2272 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2274 switch ($rule["type"]) {
2276 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2277 $rule['reg_exp'] . "')";
2280 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2281 $rule['reg_exp'] . "')";
2284 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2285 $rule['reg_exp'] . "') OR LOWER(" .
2286 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2289 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2290 $rule['reg_exp'] . "')";
2293 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2294 $rule['reg_exp'] . "')";
2297 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2298 $rule['reg_exp'] . "')";
2302 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2304 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2305 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2308 if (isset($rule["cat_id"])) {
2310 if ($rule["cat_id"] > 0) {
2311 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2312 array_push($children, $rule["cat_id"]);
2313 $children = array_map("intval", $children);
2315 $children = join(",", $children);
2317 $cat_qpart = "cat_id IN ($children)";
2319 $cat_qpart = "cat_id IS NULL";
2322 $qpart .= " AND $cat_qpart";
2325 $qpart .= " AND feed_id IS NOT NULL";
2327 array_push($query, "($qpart)");
2332 if (count($query) > 0) {
2333 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2335 $fullquery = "(false)";
2338 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2343 if (!function_exists('gzdecode')) {
2344 function gzdecode($string) { // no support for 2nd argument
2345 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2346 base64_encode($string));
2350 function get_random_bytes($length) {
2351 if (function_exists('openssl_random_pseudo_bytes')) {
2352 return openssl_random_pseudo_bytes($length);
2356 for ($i = 0; $i < $length; $i++
)
2357 $output .= chr(mt_rand(0, 255));
2363 function read_stdin() {
2364 $fp = fopen("php://stdin", "r");
2367 $line = trim(fgets($fp));
2375 function implements_interface($class, $interface) {
2376 return in_array($interface, class_implements($class));
2379 function get_minified_js($files) {
2380 require_once 'lib/jshrink/Minifier.php';
2384 foreach ($files as $js) {
2385 if (!isset($_GET['debug'])) {
2386 $cached_file = CACHE_DIR
. "/js/".basename($js);
2388 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2390 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2392 if ($header && $contents) {
2393 list($htag, $hversion) = explode(":", $header);
2395 if ($htag == "tt-rss" && $hversion == VERSION
) {
2402 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2403 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2407 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2414 function calculate_dep_timestamp() {
2415 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2419 foreach ($files as $file) {
2420 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2426 function T_js_decl($s1, $s2) {
2428 $s1 = preg_replace("/\n/", "", $s1);
2429 $s2 = preg_replace("/\n/", "", $s2);
2431 $s1 = preg_replace("/\"/", "\\\"", $s1);
2432 $s2 = preg_replace("/\"/", "\\\"", $s2);
2434 return "T_messages[\"$s1\"] = \"$s2\";\n";
2438 function init_js_translations() {
2440 print 'var T_messages = new Object();
2443 if (T_messages[msg]) {
2444 return T_messages[msg];
2450 function ngettext(msg1, msg2, n) {
2451 return __((parseInt(n) > 1) ? msg2 : msg1);
2454 $l10n = _get_reader();
2456 for ($i = 0; $i < $l10n->total
; $i++
) {
2457 $orig = $l10n->get_original_string($i);
2458 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2459 $key = explode(chr(0), $orig);
2460 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2461 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2463 $translation = __($orig);
2464 print T_js_decl($orig, $translation);
2469 function get_theme_path($theme) {
2470 if ($theme == "default.php")
2471 return "css/default.css";
2473 $check = "themes/$theme";
2474 if (file_exists($check)) return $check;
2476 $check = "themes.local/$theme";
2477 if (file_exists($check)) return $check;
2480 function theme_valid($theme) {
2481 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2483 if (in_array($theme, $bundled_themes)) return true;
2485 $file = "themes/" . basename($theme);
2487 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2489 if (file_exists($file) && is_readable($file)) {
2490 $fh = fopen($file, "r");
2493 $header = fgets($fh);
2496 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2504 * @SuppressWarnings(unused)
2506 function error_json($code) {
2507 require_once "errors.php";
2509 @$message = $ERRORS[$code];
2511 return json_encode(array("error" =>
2512 array("code" => $code, "message" => $message)));
2516 /*function abs_to_rel_path($dir) {
2517 $tmp = str_replace(dirname(__DIR__), "", $dir);
2519 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2524 function get_upload_error_message($code) {
2527 0 => __('There is no error, the file uploaded with success'),
2528 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2529 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2530 3 => __('The uploaded file was only partially uploaded'),
2531 4 => __('No file was uploaded'),
2532 6 => __('Missing a temporary folder'),
2533 7 => __('Failed to write file to disk.'),
2534 8 => __('A PHP extension stopped the file upload.'),
2537 return $errors[$code];
2540 function base64_img($filename) {
2541 if (file_exists($filename)) {
2542 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2544 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2550 /* this is essentially a wrapper for readfile() which allows plugins to hook
2551 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2553 hook function should return true if request was handled (or at least attempted to)
2555 note that this can be called without user context so the plugin to handle this
2556 should be loaded systemwide in config.php */
2557 function send_local_file($filename) {
2558 if (file_exists($filename)) {
2559 $tmppluginhost = new PluginHost();
2561 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2562 $tmppluginhost->load_data();
2564 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2565 if ($plugin->hook_send_local_file($filename)) return true;
2568 $mimetype = mime_content_type($filename);
2569 header("Content-type: $mimetype");
2571 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2572 header("Last-Modified: $stamp", true);
2574 return readfile($filename);
2580 function check_mysql_tables() {
2583 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2584 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2585 $sth->execute([DB_NAME
]);
2589 while ($line = $sth->fetch()) {
2590 array_push($bad_tables, $line);
2596 function validate_field($string, $allowed, $default = "") {
2597 if (in_array($string, $allowed))
2603 function arr_qmarks($arr) {
2604 return str_repeat('?,', count($arr) - 1) . '?';