2 define('EXPECTED_CONFIG_VERSION', 26);
3 define('SCHEMA_VERSION', 133);
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('CACHE_MAX_DAYS', 7);
60 // max age in days for various automatically cached (temporary) files
61 define_default('MAX_CONDITIONAL_INTERVAL', 3600*12);
62 // max interval between forced unconditional updates for servers
63 // not complying with http if-modified-since (seconds)
65 /* tunables end here */
67 if (DB_TYPE
== "pgsql") {
68 define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
70 define('SUBSTRING_FOR_DATE', 'SUBSTRING');
74 * Return available translations names.
77 * @return array A array of available translations.
79 function get_translations() {
81 "auto" => "Detect automatically",
82 "ar_SA" => "العربيّة (Arabic)",
83 "bg_BG" => "Bulgarian",
88 "el_GR" => "Ελληνικά",
89 "es_ES" => "Español (España)",
92 "fr_FR" => "Français",
93 "hu_HU" => "Magyar (Hungarian)",
94 "it_IT" => "Italiano",
95 "ja_JP" => "日本語 (Japanese)",
96 "lv_LV" => "Latviešu",
97 "nb_NO" => "Norwegian bokmål",
100 "ru_RU" => "Русский",
101 "pt_BR" => "Portuguese/Brazil",
102 "pt_PT" => "Portuguese/Portugal",
103 "zh_CN" => "Simplified Chinese",
104 "zh_TW" => "Traditional Chinese",
105 "sv_SE" => "Svenska",
107 "tr_TR" => "Türkçe");
112 require_once "lib/accept-to-gettext.php";
113 require_once "lib/gettext/gettext.inc";
115 function startup_gettext() {
117 # Get locale from Accept-Language header
118 $lang = al2gt(array_keys(get_translations()), "text/html");
120 if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
121 $lang = _TRANSLATION_OVERRIDE_DEFAULT
;
124 if ($_SESSION["uid"] && get_schema_version() >= 120) {
125 $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
127 if ($pref_lang && $pref_lang != 'auto') {
133 if (defined('LC_MESSAGES')) {
134 _setlocale(LC_MESSAGES
, $lang);
135 } else if (defined('LC_ALL')) {
136 _setlocale(LC_ALL
, $lang);
139 _bindtextdomain("messages", "locale");
141 _textdomain("messages");
142 _bind_textdomain_codeset("messages", "UTF-8");
146 require_once 'db-prefs.php';
147 require_once 'version.php';
148 require_once 'controls.php';
150 define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION
. ' (http://tt-rss.org/)');
151 ini_set('user_agent', SELF_USER_AGENT
);
153 $schema_version = false;
155 function _debug_suppress($suppress) {
156 global $suppress_debugging;
158 $suppress_debugging = $suppress;
162 * Print a timestamped debug message.
164 * @param string $msg The debug message.
167 function _debug($msg, $show = true) {
168 global $suppress_debugging;
170 //echo "[$suppress_debugging] $msg $show\n";
172 if ($suppress_debugging) return false;
174 $ts = strftime("%H:%M:%S", time());
175 if (function_exists('posix_getpid')) {
176 $ts = "$ts/" . posix_getpid();
179 if ($show && !(defined('QUIET') && QUIET
)) {
180 print "[$ts] $msg\n";
183 if (defined('LOGFILE')) {
184 $fp = fopen(LOGFILE
, 'a+');
189 if (function_exists("flock")) {
192 // try to lock logfile for writing
193 while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB
)) {
204 fputs($fp, "[$ts] $msg\n");
206 if (function_exists("flock")) {
217 * Purge a feed old posts.
219 * @param mixed $link A database connection.
220 * @param mixed $feed_id The id of the purged feed.
221 * @param mixed $purge_interval Olderness of purged posts.
222 * @param boolean $debug Set to True to enable the debug. False by default.
226 function purge_feed($feed_id, $purge_interval, $debug = false) {
228 if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
232 $sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
233 $sth->execute([$feed_id]);
237 if ($row = $sth->fetch()) {
238 $owner_uid = $row["owner_uid"];
241 if ($purge_interval == -1 ||
!$purge_interval) {
243 CCache
::update($feed_id, $owner_uid);
248 if (!$owner_uid) return;
250 if (FORCE_ARTICLE_PURGE
== 0) {
251 $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
254 $purge_unread = true;
255 $purge_interval = FORCE_ARTICLE_PURGE
;
259 $query_limit = " unread = false AND ";
263 $purge_interval = (int) $purge_interval;
265 if (DB_TYPE
== "pgsql") {
266 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
268 WHERE ttrss_entries.id = ref_id AND
272 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
273 $sth->execute([$feed_id]);
276 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
277 USING ttrss_user_entries, ttrss_entries
278 WHERE ttrss_entries.id = ref_id AND
282 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
283 $sth->execute([$feed_id]);
287 $rows = $sth->rowCount();
289 CCache
::update($feed_id, $owner_uid);
292 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
296 } // function purge_feed
298 function feed_purge_interval($feed_id) {
302 $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
304 $sth->execute([$feed_id]);
306 if ($row = $sth->fetch()) {
307 $purge_interval = $row["purge_interval"];
308 $owner_uid = $row["owner_uid"];
310 if ($purge_interval == 0) $purge_interval = get_pref(
311 'PURGE_OLD_DAYS', $owner_uid);
313 return $purge_interval;
320 // TODO: multiple-argument way is deprecated, first parameter is a hash now
321 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
322 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
324 global $fetch_last_error;
325 global $fetch_last_error_code;
326 global $fetch_last_error_content;
327 global $fetch_last_content_type;
328 global $fetch_last_modified;
329 global $fetch_effective_url;
330 global $fetch_curl_used;
332 $fetch_last_error = false;
333 $fetch_last_error_code = -1;
334 $fetch_last_error_content = "";
335 $fetch_last_content_type = "";
336 $fetch_curl_used = false;
337 $fetch_last_modified = "";
338 $fetch_effective_url = "";
340 if (!is_array($options)) {
342 // falling back on compatibility shim
343 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
346 for ($i = 0; $i < func_num_args(); $i++
) {
347 $tmp[$option_names[$i]] = func_get_arg($i);
353 "url" => func_get_arg(0),
354 "type" => @func_get_arg(1),
355 "login" => @func_get_arg(2),
356 "pass" => @func_get_arg(3),
357 "post_query" => @func_get_arg(4),
358 "timeout" => @func_get_arg(5),
359 "timestamp" => @func_get_arg(6),
360 "useragent" => @func_get_arg(7)
364 $url = $options["url"];
365 $type = isset($options["type"]) ?
$options["type"] : false;
366 $login = isset($options["login"]) ?
$options["login"] : false;
367 $pass = isset($options["pass"]) ?
$options["pass"] : false;
368 $post_query = isset($options["post_query"]) ?
$options["post_query"] : false;
369 $timeout = isset($options["timeout"]) ?
$options["timeout"] : false;
370 $last_modified = isset($options["last_modified"]) ?
$options["last_modified"] : "";
371 $useragent = isset($options["useragent"]) ?
$options["useragent"] : false;
372 $followlocation = isset($options["followlocation"]) ?
$options["followlocation"] : true;
374 $url = ltrim($url, ' ');
375 $url = str_replace(' ', '%20', $url);
377 if (strpos($url, "//") === 0)
378 $url = 'http:' . $url;
380 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
382 $fetch_curl_used = true;
384 $ch = curl_init($url);
386 if ($last_modified && !$post_query) {
387 curl_setopt($ch, CURLOPT_HTTPHEADER
,
388 array("If-Modified-Since: $last_modified"));
391 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
392 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
393 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
394 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
395 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
396 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
397 curl_setopt($ch, CURLOPT_HEADER
, true);
398 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
399 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
401 curl_setopt($ch, CURLOPT_ENCODING
, "");
402 //curl_setopt($ch, CURLOPT_REFERER, $url);
404 if (!ini_get("open_basedir")) {
405 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
408 if (defined('_HTTP_PROXY')) {
409 curl_setopt($ch, CURLOPT_PROXY
, _HTTP_PROXY
);
413 curl_setopt($ch, CURLOPT_POST
, true);
414 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
418 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
420 $ret = @curl_exec
($ch);
422 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE
);
423 $headers = explode("\r\n", substr($ret, 0, $headers_length));
424 $contents = substr($ret, $headers_length);
426 foreach ($headers as $header) {
427 if (strstr($header, ": ") !== FALSE) {
428 list ($key, $value) = explode(": ", $header);
430 if (strtolower($key) == "last-modified") {
431 $fetch_last_modified = $value;
435 if (substr(strtolower($header), 0, 7) == 'http/1.') {
436 $fetch_last_error_code = (int) substr($header, 9, 3);
437 $fetch_last_error = $header;
441 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
442 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
443 $contents = @curl_exec
($ch);
446 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
447 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
449 $fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL
);
451 $fetch_last_error_code = $http_code;
453 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
455 if (curl_errno($ch) != 0) {
456 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
459 $fetch_last_error_content = $contents;
465 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
475 $fetch_curl_used = false;
477 if ($login && $pass){
478 $url_parts = array();
480 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
482 $pass = urlencode($pass);
484 if ($url_parts[1] && $url_parts[2]) {
485 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
489 // TODO: should this support POST requests or not? idk
491 $context_options = array(
494 'ignore_errors' => true,
495 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
496 'protocol_version'=> 1.1)
499 if (!$post_query && $last_modified) {
500 $context_options['http']['header'] = "If-Modified-Since: $last_modified\r\n";
503 if (defined('_HTTP_PROXY')) {
504 $context_options['http']['request_fulluri'] = true;
505 $context_options['http']['proxy'] = _HTTP_PROXY
;
508 $context = stream_context_create($context_options);
510 $old_error = error_get_last();
512 $fetch_effective_url = $url;
514 $data = @file_get_contents
($url, false, $context);
516 if (isset($http_response_header) && is_array($http_response_header)) {
517 foreach ($http_response_header as $header) {
518 if (strstr($header, ": ") !== FALSE) {
519 list ($key, $value) = explode(": ", $header);
521 $key = strtolower($key);
523 if ($key == 'content-type') {
524 $fetch_last_content_type = $value;
525 // don't abort here b/c there might be more than one
526 // e.g. if we were being redirected -- last one is the right one
527 } else if ($key == 'last-modified') {
528 $fetch_last_modified = $value;
529 } else if ($key == 'location') {
530 $fetch_effective_url = $value;
534 if (substr(strtolower($header), 0, 7) == 'http/1.') {
535 $fetch_last_error_code = (int) substr($header, 9, 3);
536 $fetch_last_error = $header;
541 if ($fetch_last_error_code != 200) {
542 $error = error_get_last();
544 if ($error['message'] != $old_error['message']) {
545 $fetch_last_error .= "; " . $error["message"];
548 $fetch_last_error_content = $data;
558 * Try to determine the favicon URL for a feed.
559 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
560 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
562 * @param string $url A feed or page URL
564 * @return mixed The favicon URL, or false if none was found.
566 function get_favicon_url($url) {
568 $favicon_url = false;
570 if ($html = @fetch_file_contents
($url)) {
572 libxml_use_internal_errors(true);
574 $doc = new DOMDocument();
575 $doc->loadHTML($html);
576 $xpath = new DOMXPath($doc);
578 $base = $xpath->query('/html/head/base[@href]');
579 foreach ($base as $b) {
580 $url = rewrite_relative_url($url, $b->getAttribute("href"));
584 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
585 if (count($entries) > 0) {
586 foreach ($entries as $entry) {
587 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
594 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
597 } // function get_favicon_url
599 function initialize_user_prefs($uid, $profile = false) {
601 if (get_schema_version() < 63) $profile_qpart = "";
604 $in_nested_tr = false;
607 $pdo->beginTransaction();
608 } catch (Exception
$e) {
609 $in_nested_tr = true;
612 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
614 $profile = $profile ?
$profile : null;
616 $u_sth = $pdo->prepare("SELECT pref_name
617 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
618 (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
619 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
621 $active_prefs = array();
623 while ($line = $u_sth->fetch()) {
624 array_push($active_prefs, $line["pref_name"]);
627 while ($line = $sth->fetch()) {
628 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
629 // print "adding " . $line["pref_name"] . "<br>";
631 if (get_schema_version() < 63) {
632 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
633 (owner_uid,pref_name,value) VALUES
635 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
638 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
639 (owner_uid,pref_name,value, profile) VALUES
641 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
647 if (!$in_nested_tr) $pdo->commit();
651 function get_ssl_certificate_id() {
652 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
653 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
654 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
655 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
656 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
658 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
659 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
660 $_SERVER["SSL_CLIENT_V_START"] .
661 $_SERVER["SSL_CLIENT_V_END"] .
662 $_SERVER["SSL_CLIENT_S_DN"]);
667 function authenticate_user($login, $password, $check_only = false) {
669 if (!SINGLE_USER_MODE
) {
672 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
674 $user_id = (int) $plugin->authenticate($login, $password);
677 $_SESSION["auth_module"] = strtolower(get_class($plugin));
682 if ($user_id && !$check_only) {
685 $_SESSION["uid"] = $user_id;
686 $_SESSION["version"] = VERSION_STATIC
;
689 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
691 $sth->execute([$user_id]);
692 $row = $sth->fetch();
694 $_SESSION["name"] = $row["login"];
695 $_SESSION["access_level"] = $row["access_level"];
696 $_SESSION["csrf_token"] = uniqid_short();
698 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
699 $usth->execute([$user_id]);
701 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
702 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
703 $_SESSION["pwd_hash"] = $row["pwd_hash"];
705 $_SESSION["last_version_check"] = time();
707 initialize_user_prefs($_SESSION["uid"]);
716 $_SESSION["uid"] = 1;
717 $_SESSION["name"] = "admin";
718 $_SESSION["access_level"] = 10;
720 $_SESSION["hide_hello"] = true;
721 $_SESSION["hide_logout"] = true;
723 $_SESSION["auth_module"] = false;
725 if (!$_SESSION["csrf_token"]) {
726 $_SESSION["csrf_token"] = uniqid_short();
729 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
731 initialize_user_prefs($_SESSION["uid"]);
737 // this is used for user http parameters unless HTML code is actually needed
738 function clean($param) {
739 if (is_array($param)) {
740 return array_map("strip_tags", $param);
741 } else if (is_string($param)) {
742 return strip_tags($param);
748 function make_password($length = 8) {
751 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
755 while ($i < $length) {
756 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
758 if (!strstr($password, $char)) {
766 // this is called after user is created to initialize default feeds, labels
769 // user preferences are checked on every login, not here
771 function initialize_user($uid) {
775 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
776 values (?, 'Tiny Tiny RSS: Forum',
777 'http://tt-rss.org/forum/rss.php')");
778 $sth->execute([$uid]);
781 function logout_user() {
783 if (isset($_COOKIE[session_name()])) {
784 setcookie(session_name(), '', time()-42000, '/');
788 function validate_csrf($csrf_token) {
789 return $csrf_token == $_SESSION['csrf_token'];
792 function load_user_plugins($owner_uid, $pluginhost = false) {
794 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
796 if ($owner_uid && SCHEMA_VERSION
>= 100) {
797 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
799 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
801 if (get_schema_version() > 100) {
802 $pluginhost->load_data();
807 function login_sequence() {
810 if (SINGLE_USER_MODE
) {
812 authenticate_user("admin", null);
814 load_user_plugins($_SESSION["uid"]);
816 if (!validate_session()) $_SESSION["uid"] = false;
818 if (!$_SESSION["uid"]) {
820 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
821 $_SESSION["ref_schema_version"] = get_schema_version(true);
823 authenticate_user(null, null, true);
826 if (!$_SESSION["uid"]) {
828 setcookie(session_name(), '', time()-42000, '/');
835 /* bump login timestamp */
836 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
837 $sth->execute([$_SESSION['uid']]);
839 $_SESSION["last_login_update"] = time();
842 if ($_SESSION["uid"]) {
844 load_user_plugins($_SESSION["uid"]);
848 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
850 (SELECT COUNT(id) FROM ttrss_feeds WHERE
851 ttrss_feeds.id = feed_id) = 0");
853 $sth->execute([$_SESSION['uid']]);
855 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
857 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
858 ttrss_feed_categories.id = feed_id) = 0");
860 $sth->execute([$_SESSION['uid']]);
866 function truncate_string($str, $max_len, $suffix = '…') {
867 if (mb_strlen($str, "utf-8") > $max_len) {
868 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
875 function truncate_middle($str, $max_len, $suffix = '…') {
876 if (strlen($str) > $max_len) {
877 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
883 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
886 $source_tz = new DateTimeZone($source_tz);
887 } catch (Exception
$e) {
888 $source_tz = new DateTimeZone('UTC');
892 $dest_tz = new DateTimeZone($dest_tz);
893 } catch (Exception
$e) {
894 $dest_tz = new DateTimeZone('UTC');
897 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
898 return $dt->format('U') +
$dest_tz->getOffset($dt);
901 function make_local_datetime($timestamp, $long, $owner_uid = false,
902 $no_smart_dt = false, $eta_min = false) {
904 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
905 if (!$timestamp) $timestamp = '1970-01-01 0:00';
910 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
912 $timestamp = substr($timestamp, 0, 19);
914 # We store date in UTC internally
915 $dt = new DateTime($timestamp, $utc_tz);
917 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
919 if ($user_tz_string != 'Automatic') {
922 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
923 } catch (Exception
$e) {
927 $tz_offset = $user_tz->getOffset($dt);
929 $tz_offset = (int) -$_SESSION["clientTzOffset"];
932 $user_timestamp = $dt->format('U') +
$tz_offset;
935 return smart_date_time($user_timestamp,
936 $tz_offset, $owner_uid, $eta_min);
939 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
941 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
943 return date($format, $user_timestamp);
947 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
948 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
950 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
951 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
952 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
953 return date("G:i", $timestamp);
954 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
955 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
956 return date($format, $timestamp);
958 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
959 return date($format, $timestamp);
963 function sql_bool_to_bool($s) {
964 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
967 function bool_to_sql_bool($s) {
971 // Session caching removed due to causing wrong redirects to upgrade
972 // script when get_schema_version() is called on an obsolete session
973 // created on a previous schema version.
974 function get_schema_version($nocache = false) {
975 global $schema_version;
979 if (!$schema_version && !$nocache) {
980 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
981 $version = $row["schema_version"];
982 $schema_version = $version;
985 return $schema_version;
989 function sanity_check() {
990 require_once 'errors.php';
994 $schema_version = get_schema_version(true);
996 if ($schema_version != SCHEMA_VERSION
) {
1000 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1003 function file_is_locked($filename) {
1004 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
1005 if (function_exists('flock')) {
1006 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1008 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1009 flock($fp, LOCK_UN
);
1019 return true; // consider the file always locked and skip the test
1026 function make_lockfile($filename) {
1027 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1029 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1030 $stat_h = fstat($fp);
1031 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1033 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1034 if ($stat_h["ino"] != $stat_f["ino"] ||
1035 $stat_h["dev"] != $stat_f["dev"]) {
1041 if (function_exists('posix_getpid')) {
1042 fwrite($fp, posix_getpid() . "\n");
1050 function make_stampfile($filename) {
1051 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1053 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1054 fwrite($fp, time() . "\n");
1055 flock($fp, LOCK_UN
);
1063 function sql_random_function() {
1064 if (DB_TYPE
== "mysql") {
1071 function getFeedUnread($feed, $is_cat = false) {
1072 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1075 function checkbox_to_sql_bool($val) {
1076 return ($val == "on") ?
1 : 0;
1079 function uniqid_short() {
1080 return uniqid(base_convert(rand(), 10, 36));
1083 function make_init_params() {
1086 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1087 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1088 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1089 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1091 $params[strtolower($param)] = (int) get_pref($param);
1094 $params["icons_url"] = ICONS_URL
;
1095 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1096 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1097 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1098 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1099 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1100 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1101 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1103 $theme = get_pref( "USER_CSS_THEME", false, false);
1104 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1106 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1108 $params["php_platform"] = PHP_OS
;
1109 $params["php_version"] = PHP_VERSION
;
1111 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1115 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1116 ttrss_feeds WHERE owner_uid = ?");
1117 $sth->execute([$_SESSION['uid']]);
1118 $row = $sth->fetch();
1120 $max_feed_id = $row["mid"];
1121 $num_feeds = $row["nf"];
1123 $params["max_feed_id"] = (int) $max_feed_id;
1124 $params["num_feeds"] = (int) $num_feeds;
1126 $params["hotkeys"] = get_hotkeys_map();
1128 $params["csrf_token"] = $_SESSION["csrf_token"];
1129 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1131 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1133 $params["icon_alert"] = base64_img("images/alert.png");
1134 $params["icon_information"] = base64_img("images/information.png");
1135 $params["icon_cross"] = base64_img("images/cross.png");
1136 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1138 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1143 function get_hotkeys_info() {
1145 __("Navigation") => array(
1146 "next_feed" => __("Open next feed"),
1147 "prev_feed" => __("Open previous feed"),
1148 "next_article" => __("Open next article"),
1149 "prev_article" => __("Open previous article"),
1150 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1151 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1152 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1153 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1154 "search_dialog" => __("Show search dialog")),
1155 __("Article") => array(
1156 "toggle_mark" => __("Toggle starred"),
1157 "toggle_publ" => __("Toggle published"),
1158 "toggle_unread" => __("Toggle unread"),
1159 "edit_tags" => __("Edit tags"),
1160 "open_in_new_window" => __("Open in new window"),
1161 "catchup_below" => __("Mark below as read"),
1162 "catchup_above" => __("Mark above as read"),
1163 "article_scroll_down" => __("Scroll down"),
1164 "article_scroll_up" => __("Scroll up"),
1165 "select_article_cursor" => __("Select article under cursor"),
1166 "email_article" => __("Email article"),
1167 "close_article" => __("Close/collapse article"),
1168 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1169 "toggle_widescreen" => __("Toggle widescreen mode"),
1170 "toggle_embed_original" => __("Toggle embed original")),
1171 __("Article selection") => array(
1172 "select_all" => __("Select all articles"),
1173 "select_unread" => __("Select unread"),
1174 "select_marked" => __("Select starred"),
1175 "select_published" => __("Select published"),
1176 "select_invert" => __("Invert selection"),
1177 "select_none" => __("Deselect everything")),
1178 __("Feed") => array(
1179 "feed_refresh" => __("Refresh current feed"),
1180 "feed_unhide_read" => __("Un/hide read feeds"),
1181 "feed_subscribe" => __("Subscribe to feed"),
1182 "feed_edit" => __("Edit feed"),
1183 "feed_catchup" => __("Mark as read"),
1184 "feed_reverse" => __("Reverse headlines"),
1185 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1186 "feed_debug_update" => __("Debug feed update"),
1187 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1188 "catchup_all" => __("Mark all feeds as read"),
1189 "cat_toggle_collapse" => __("Un/collapse current category"),
1190 "toggle_combined_mode" => __("Toggle combined mode"),
1191 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1192 __("Go to") => array(
1193 "goto_all" => __("All articles"),
1194 "goto_fresh" => __("Fresh"),
1195 "goto_marked" => __("Starred"),
1196 "goto_published" => __("Published"),
1197 "goto_tagcloud" => __("Tag cloud"),
1198 "goto_prefs" => __("Preferences")),
1199 __("Other") => array(
1200 "create_label" => __("Create label"),
1201 "create_filter" => __("Create filter"),
1202 "collapse_sidebar" => __("Un/collapse sidebar"),
1203 "help_dialog" => __("Show help dialog"))
1206 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1207 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1213 function get_hotkeys_map() {
1215 // "navigation" => array(
1218 "n" => "next_article",
1219 "p" => "prev_article",
1220 "(38)|up" => "prev_article",
1221 "(40)|down" => "next_article",
1222 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1223 // "^(40)|Ctrl-down" => "next_article_noscroll",
1224 "(191)|/" => "search_dialog",
1225 // "article" => array(
1226 "s" => "toggle_mark",
1227 "*s" => "toggle_publ",
1228 "u" => "toggle_unread",
1229 "*t" => "edit_tags",
1230 "o" => "open_in_new_window",
1231 "c p" => "catchup_below",
1232 "c n" => "catchup_above",
1233 "*n" => "article_scroll_down",
1234 "*p" => "article_scroll_up",
1235 "*(38)|Shift+up" => "article_scroll_up",
1236 "*(40)|Shift+down" => "article_scroll_down",
1237 "a *w" => "toggle_widescreen",
1238 "a e" => "toggle_embed_original",
1239 "e" => "email_article",
1240 "a q" => "close_article",
1241 // "article_selection" => array(
1242 "a a" => "select_all",
1243 "a u" => "select_unread",
1244 "a *u" => "select_marked",
1245 "a p" => "select_published",
1246 "a i" => "select_invert",
1247 "a n" => "select_none",
1249 "f r" => "feed_refresh",
1250 "f a" => "feed_unhide_read",
1251 "f s" => "feed_subscribe",
1252 "f e" => "feed_edit",
1253 "f q" => "feed_catchup",
1254 "f x" => "feed_reverse",
1255 "f g" => "feed_toggle_vgroup",
1256 "f *d" => "feed_debug_update",
1257 "f *g" => "feed_debug_viewfeed",
1258 "f *c" => "toggle_combined_mode",
1259 "f c" => "toggle_cdm_expanded",
1260 "*q" => "catchup_all",
1261 "x" => "cat_toggle_collapse",
1263 "g a" => "goto_all",
1264 "g f" => "goto_fresh",
1265 "g s" => "goto_marked",
1266 "g p" => "goto_published",
1267 "g t" => "goto_tagcloud",
1268 "g *p" => "goto_prefs",
1269 // "other" => array(
1270 "(9)|Tab" => "select_article_cursor", // tab
1271 "c l" => "create_label",
1272 "c f" => "create_filter",
1273 "c s" => "collapse_sidebar",
1274 "^(191)|Ctrl+/" => "help_dialog",
1277 if (get_pref('COMBINED_DISPLAY_MODE')) {
1278 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1279 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1282 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1283 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1286 $prefixes = array();
1288 foreach (array_keys($hotkeys) as $hotkey) {
1289 $pair = explode(" ", $hotkey, 2);
1291 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1292 array_push($prefixes, $pair[0]);
1296 return array($prefixes, $hotkeys);
1299 function check_for_update() {
1300 if (defined("GIT_VERSION_TIMESTAMP")) {
1301 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1304 $content = json_decode($content, true);
1306 if ($content && isset($content["changeset"])) {
1307 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1308 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1310 return $content["changeset"]["id"];
1319 function make_runtime_info($disable_update_check = false) {
1324 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1325 ttrss_feeds WHERE owner_uid = ?");
1326 $sth->execute([$_SESSION['uid']]);
1327 $row = $sth->fetch();
1329 $max_feed_id = $row['mid'];
1330 $num_feeds = $row['nf'];
1332 $data["max_feed_id"] = (int) $max_feed_id;
1333 $data["num_feeds"] = (int) $num_feeds;
1335 $data['last_article_id'] = Article
::getLastArticleId();
1336 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1338 $data['dep_ts'] = calculate_dep_timestamp();
1339 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1341 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1343 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1344 $update_result = @check_for_update
();
1346 $data["update_result"] = $update_result;
1348 $_SESSION["last_version_check"] = time();
1351 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1353 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1355 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1357 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1360 $stamp_delta = time() - $stamp;
1362 if ($stamp_delta > 1800) {
1366 $_SESSION["daemon_stamp_check"] = time();
1369 $data['daemon_stamp_ok'] = $stamp_check;
1371 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1373 $data['daemon_stamp'] = $stamp_fmt;
1381 function search_to_sql($search, $search_language) {
1383 $keywords = str_getcsv(trim($search), " ");
1384 $query_keywords = array();
1385 $search_words = array();
1386 $search_query_leftover = array();
1390 if ($search_language)
1391 $search_language = $pdo->quote(mb_strtolower($search_language));
1393 $search_language = $pdo->quote("english");
1395 foreach ($keywords as $k) {
1396 if (strpos($k, "-") === 0) {
1403 $commandpair = explode(":", mb_strtolower($k), 2);
1405 switch ($commandpair[0]) {
1407 if ($commandpair[1]) {
1408 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1409 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1411 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1412 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1413 array_push($search_words, $k);
1417 if ($commandpair[1]) {
1418 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1419 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1421 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1422 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1423 array_push($search_words, $k);
1427 if ($commandpair[1]) {
1428 if ($commandpair[1] == "true")
1429 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1430 else if ($commandpair[1] == "false")
1431 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1433 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1434 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1436 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1437 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1438 if (!$not) array_push($search_words, $k);
1443 if ($commandpair[1]) {
1444 if ($commandpair[1] == "true")
1445 array_push($query_keywords, "($not (marked = true))");
1447 array_push($query_keywords, "($not (marked = false))");
1449 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1450 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1451 if (!$not) array_push($search_words, $k);
1455 if ($commandpair[1]) {
1456 if ($commandpair[1] == "true")
1457 array_push($query_keywords, "($not (published = true))");
1459 array_push($query_keywords, "($not (published = false))");
1462 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1463 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1464 if (!$not) array_push($search_words, $k);
1468 if ($commandpair[1]) {
1469 if ($commandpair[1] == "true")
1470 array_push($query_keywords, "($not (unread = true))");
1472 array_push($query_keywords, "($not (unread = false))");
1475 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1476 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1477 if (!$not) array_push($search_words, $k);
1481 if (strpos($k, "@") === 0) {
1483 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1484 $orig_ts = strtotime(substr($k, 1));
1485 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1487 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1489 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1492 if (DB_TYPE
== "pgsql") {
1493 $k = mb_strtolower($k);
1494 array_push($search_query_leftover, $not ?
"!$k" : $k);
1496 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1497 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1500 if (!$not) array_push($search_words, $k);
1505 if (count($search_query_leftover) > 0) {
1506 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1508 if (DB_TYPE
== "pgsql") {
1509 array_push($query_keywords,
1510 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1515 $search_query_part = implode("AND", $query_keywords);
1517 return array($search_query_part, $search_words);
1520 function iframe_whitelisted($entry) {
1521 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1523 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1526 foreach ($whitelist as $w) {
1527 if ($src == $w ||
$src == "www.$w")
1535 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1536 if (!$owner) $owner = $_SESSION["uid"];
1538 $res = trim($str); if (!$res) return '';
1540 $charset_hack = '<head>
1541 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1544 $res = trim($res); if (!$res) return '';
1546 libxml_use_internal_errors(true);
1548 $doc = new DOMDocument();
1549 $doc->loadHTML($charset_hack . $res);
1550 $xpath = new DOMXPath($doc);
1552 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1554 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1556 foreach ($entries as $entry) {
1558 if ($entry->hasAttribute('href')) {
1559 $entry->setAttribute('href',
1560 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1562 $entry->setAttribute('rel', 'noopener noreferrer');
1565 if ($entry->hasAttribute('src')) {
1566 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1567 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1569 if (file_exists($cached_filename)) {
1571 // this is strictly cosmetic
1572 if ($entry->tagName
== 'img') {
1574 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1576 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1582 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1584 if ($entry->hasAttribute('srcset')) {
1585 $entry->removeAttribute('srcset');
1588 if ($entry->hasAttribute('sizes')) {
1589 $entry->removeAttribute('sizes');
1593 $entry->setAttribute('src', $src);
1596 if ($entry->nodeName
== 'img') {
1597 $entry->setAttribute('referrerpolicy', 'no-referrer');
1599 if ($entry->hasAttribute('src')) {
1600 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1602 if (is_prefix_https() && !$is_https_url) {
1604 if ($entry->hasAttribute('srcset')) {
1605 $entry->removeAttribute('srcset');
1608 if ($entry->hasAttribute('sizes')) {
1609 $entry->removeAttribute('sizes');
1614 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1615 $force_remove_images ||
$_SESSION["bw_limit"]) {
1617 $p = $doc->createElement('p');
1619 $a = $doc->createElement('a');
1620 $a->setAttribute('href', $entry->getAttribute('src'));
1622 $a->appendChild(new DOMText($entry->getAttribute('src')));
1623 $a->setAttribute('target', '_blank');
1624 $a->setAttribute('rel', 'noopener noreferrer');
1626 $p->appendChild($a);
1628 $entry->parentNode
->replaceChild($p, $entry);
1632 if (strtolower($entry->nodeName
) == "a") {
1633 $entry->setAttribute("target", "_blank");
1634 $entry->setAttribute("rel", "noopener noreferrer");
1638 $entries = $xpath->query('//iframe');
1639 foreach ($entries as $entry) {
1640 if (!iframe_whitelisted($entry)) {
1641 $entry->setAttribute('sandbox', 'allow-scripts');
1643 if (is_prefix_https()) {
1644 $entry->setAttribute("src",
1645 str_replace("http://", "https://",
1646 $entry->getAttribute("src")));
1651 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1652 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1653 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1654 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1655 'dt', 'em', 'footer', 'figure', 'figcaption',
1656 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1657 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1658 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1659 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1660 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1661 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1663 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1665 $disallowed_attributes = array('id', 'style', 'class');
1667 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1668 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1669 if (is_array($retval)) {
1671 $allowed_elements = $retval[1];
1672 $disallowed_attributes = $retval[2];
1678 $doc->removeChild($doc->firstChild
); //remove doctype
1679 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1681 if ($highlight_words) {
1682 foreach ($highlight_words as $word) {
1684 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1686 $elements = $xpath->query("//*/text()");
1688 foreach ($elements as $child) {
1690 $fragment = $doc->createDocumentFragment();
1691 $text = $child->textContent
;
1693 while (($pos = mb_stripos($text, $word)) !== false) {
1694 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1695 $word = mb_substr($text, $pos, mb_strlen($word));
1696 $highlight = $doc->createElement('span');
1697 $highlight->appendChild(new DomText($word));
1698 $highlight->setAttribute('class', 'highlight');
1699 $fragment->appendChild($highlight);
1700 $text = mb_substr($text, $pos +
mb_strlen($word));
1703 if (!empty($text)) $fragment->appendChild(new DomText($text));
1705 $child->parentNode
->replaceChild($fragment, $child);
1710 $res = $doc->saveHTML();
1712 /* strip everything outside of <body>...</body> */
1714 $res_frag = array();
1715 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1716 return $res_frag[1];
1722 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1723 $xpath = new DOMXPath($doc);
1724 $entries = $xpath->query('//*');
1726 foreach ($entries as $entry) {
1727 if (!in_array($entry->nodeName
, $allowed_elements)) {
1728 $entry->parentNode
->removeChild($entry);
1731 if ($entry->hasAttributes()) {
1732 $attrs_to_remove = array();
1734 foreach ($entry->attributes
as $attr) {
1736 if (strpos($attr->nodeName
, 'on') === 0) {
1737 array_push($attrs_to_remove, $attr);
1740 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1741 array_push($attrs_to_remove, $attr);
1744 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1745 array_push($attrs_to_remove, $attr);
1749 foreach ($attrs_to_remove as $attr) {
1750 $entry->removeAttributeNode($attr);
1758 function trim_array($array) {
1760 array_walk($tmp, 'trim');
1764 function tag_is_valid($tag) {
1765 if ($tag == '') return false;
1766 if (is_numeric($tag)) return false;
1767 if (mb_strlen($tag) > 250) return false;
1769 if (!$tag) return false;
1774 function render_login_form() {
1775 header('Cache-Control: public');
1777 require_once "login_form.php";
1781 function T_sprintf() {
1782 $args = func_get_args();
1783 return vsprintf(__(array_shift($args)), $args);
1786 function print_checkpoint($n, $s) {
1787 $ts = microtime(true);
1788 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1792 function sanitize_tag($tag) {
1795 $tag = mb_strtolower($tag, 'utf-8');
1797 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1799 if (DB_TYPE
== "mysql") {
1800 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1806 function is_server_https() {
1807 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1810 function is_prefix_https() {
1811 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1814 // this returns SELF_URL_PATH sans ending slash
1815 function get_self_url_prefix() {
1816 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1817 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1819 return SELF_URL_PATH
;
1823 function encrypt_password($pass, $salt = '', $mode2 = false) {
1824 if ($salt && $mode2) {
1825 return "MODE2:" . hash('sha256', $salt . $pass);
1827 return "SHA1X:" . sha1("$salt:$pass");
1829 return "SHA1:" . sha1($pass);
1831 } // function encrypt_password
1833 function load_filters($feed_id, $owner_uid) {
1836 $feed_id = (int) $feed_id;
1837 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1840 $null_cat_qpart = "cat_id IS NULL OR";
1842 $null_cat_qpart = "";
1846 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1847 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1848 $sth->execute([$owner_uid]);
1850 $check_cats = array_merge(
1851 Feeds
::getParentCategories($cat_id, $owner_uid),
1854 $check_cats_str = join(",", $check_cats);
1855 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1857 while ($line = $sth->fetch()) {
1858 $filter_id = $line["id"];
1860 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1862 $sth2 = $pdo->prepare("SELECT
1863 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1864 FROM ttrss_filters2_rules AS r,
1865 ttrss_filter_types AS t
1867 (match_on IS NOT NULL OR
1868 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1869 (feed_id IS NULL OR feed_id = ?))) AND
1870 filter_type = t.id AND filter_id = ?");
1871 $sth2->execute([$feed_id, $filter_id]);
1876 while ($rule_line = $sth2->fetch()) {
1877 # print_r($rule_line);
1879 if ($rule_line["match_on"]) {
1880 $match_on = json_decode($rule_line["match_on"], true);
1882 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1885 $rule["reg_exp"] = $rule_line["reg_exp"];
1886 $rule["type"] = $rule_line["type_name"];
1887 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1889 array_push($rules, $rule);
1890 } else if (!$match_any_rule) {
1891 // this filter contains a rule that doesn't match to this feed/category combination
1892 // thus filter has to be rejected
1901 $rule["reg_exp"] = $rule_line["reg_exp"];
1902 $rule["type"] = $rule_line["type_name"];
1903 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1905 array_push($rules, $rule);
1909 if (count($rules) > 0) {
1910 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1911 FROM ttrss_filters2_actions AS a,
1912 ttrss_filter_actions AS t
1914 action_id = t.id AND filter_id = ?");
1915 $sth2->execute([$filter_id]);
1917 while ($action_line = $sth2->fetch()) {
1918 # print_r($action_line);
1921 $action["type"] = $action_line["type_name"];
1922 $action["param"] = $action_line["action_param"];
1924 array_push($actions, $action);
1929 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1930 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1931 $filter["rules"] = $rules;
1932 $filter["actions"] = $actions;
1934 if (count($rules) > 0 && count($actions) > 0) {
1935 array_push($filters, $filter);
1942 function get_score_pic($score) {
1944 return "score_high.png";
1945 } else if ($score > 0) {
1946 return "score_half_high.png";
1947 } else if ($score < -100) {
1948 return "score_low.png";
1949 } else if ($score < 0) {
1950 return "score_half_low.png";
1952 return "score_neutral.png";
1956 function init_plugins() {
1957 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1962 function add_feed_category($feed_cat, $parent_cat_id = false) {
1964 if (!$feed_cat) return false;
1966 $feed_cat = mb_substr($feed_cat, 0, 250);
1967 if (!$parent_cat_id) $parent_cat_id = null;
1970 $tr_in_progress = false;
1973 $pdo->beginTransaction();
1974 } catch (Exception
$e) {
1975 $tr_in_progress = true;
1978 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1979 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
1980 AND title = :title AND owner_uid = :uid");
1981 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
1983 if (!$sth->fetch()) {
1985 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1987 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
1989 if (!$tr_in_progress) $pdo->commit();
2000 * Fixes incomplete URLs by prepending "http://".
2001 * Also replaces feed:// with http://, and
2002 * prepends a trailing slash if the url is a domain name only.
2004 * @param string $url Possibly incomplete URL
2006 * @return string Fixed URL.
2008 function fix_url($url) {
2010 // support schema-less urls
2011 if (strpos($url, '//') === 0) {
2012 $url = 'https:' . $url;
2015 if (strpos($url, '://') === false) {
2016 $url = 'http://' . $url;
2017 } else if (substr($url, 0, 5) == 'feed:') {
2018 $url = 'http:' . substr($url, 5);
2021 //prepend slash if the URL has no slash in it
2022 // "http://www.example" -> "http://www.example/"
2023 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2027 //convert IDNA hostname to punycode if possible
2028 if (function_exists("idn_to_ascii")) {
2029 $parts = parse_url($url);
2030 if (mb_detect_encoding($parts['host']) != 'ASCII')
2032 $parts['host'] = idn_to_ascii($parts['host']);
2033 $url = build_url($parts);
2037 if ($url != "http:///")
2043 function validate_feed_url($url) {
2044 $parts = parse_url($url);
2046 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2050 /* function save_email_address($email) {
2051 // FIXME: implement persistent storage of emails
2053 if (!$_SESSION['stored_emails'])
2054 $_SESSION['stored_emails'] = array();
2056 if (!in_array($email, $_SESSION['stored_emails']))
2057 array_push($_SESSION['stored_emails'], $email);
2061 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2063 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2065 $is_cat = bool_to_sql_bool($is_cat);
2069 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2070 WHERE feed_id = ? AND is_cat = ?
2071 AND owner_uid = ?");
2072 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2074 if ($row = $sth->fetch()) {
2075 return $row["access_key"];
2077 $key = uniqid_short();
2079 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2080 (access_key, feed_id, is_cat, owner_uid)
2081 VALUES (?, ?, ?, ?)");
2083 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2089 function get_feeds_from_html($url, $content)
2091 $url = fix_url($url);
2092 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2094 libxml_use_internal_errors(true);
2096 $doc = new DOMDocument();
2097 $doc->loadHTML($content);
2098 $xpath = new DOMXPath($doc);
2099 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2100 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2101 $feedUrls = array();
2102 foreach ($entries as $entry) {
2103 if ($entry->hasAttribute('href')) {
2104 $title = $entry->getAttribute('title');
2106 $title = $entry->getAttribute('type');
2108 $feedUrl = rewrite_relative_url(
2109 $baseUrl, $entry->getAttribute('href')
2111 $feedUrls[$feedUrl] = $title;
2117 function is_html($content) {
2118 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2121 function url_is_html($url, $login = false, $pass = false) {
2122 return is_html(fetch_file_contents($url, false, $login, $pass));
2125 function build_url($parts) {
2126 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2129 function cleanup_url_path($path) {
2130 $path = str_replace("/./", "/", $path);
2131 $path = str_replace("//", "/", $path);
2137 * Converts a (possibly) relative URL to a absolute one.
2139 * @param string $url Base URL (i.e. from where the document is)
2140 * @param string $rel_url Possibly relative URL in the document
2142 * @return string Absolute URL
2144 function rewrite_relative_url($url, $rel_url) {
2145 if (strpos($rel_url, "://") !== false) {
2147 } else if (strpos($rel_url, "//") === 0) {
2148 # protocol-relative URL (rare but they exist)
2150 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2151 # magnet:, feed:, etc
2153 } else if (strpos($rel_url, "/") === 0) {
2154 $parts = parse_url($url);
2155 $parts['path'] = $rel_url;
2156 $parts['path'] = cleanup_url_path($parts['path']);
2158 return build_url($parts);
2161 $parts = parse_url($url);
2162 if (!isset($parts['path'])) {
2163 $parts['path'] = '/';
2165 $dir = $parts['path'];
2166 if (substr($dir, -1) !== '/') {
2167 $dir = dirname($parts['path']);
2168 $dir !== '/' && $dir .= '/';
2170 $parts['path'] = $dir . $rel_url;
2171 $parts['path'] = cleanup_url_path($parts['path']);
2173 return build_url($parts);
2177 function cleanup_tags($days = 14, $limit = 1000) {
2179 $days = (int) $days;
2181 if (DB_TYPE
== "pgsql") {
2182 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2183 } else if (DB_TYPE
== "mysql") {
2184 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2191 while ($limit > 0) {
2194 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2195 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2196 WHERE post_int_id = int_id AND $interval_query AND
2197 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2198 $sth->execute([$limit]);
2202 while ($line = $sth->fetch()) {
2203 array_push($ids, $line['id']);
2206 if (count($ids) > 0) {
2207 $ids = join(",", $ids);
2209 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2210 $tags_deleted = $usth->rowCount();
2215 $limit -= $limit_part;
2218 return $tags_deleted;
2221 function print_user_stylesheet() {
2222 $value = get_pref('USER_STYLESHEET');
2225 print "<style type=\"text/css\">";
2226 print str_replace("<br/>", "\n", $value);
2232 function filter_to_sql($filter, $owner_uid) {
2237 if (DB_TYPE
== "pgsql")
2240 $reg_qpart = "REGEXP";
2242 foreach ($filter["rules"] AS $rule) {
2243 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2244 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2245 $rule['reg_exp']) !== FALSE;
2247 if ($regexp_valid) {
2249 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2251 switch ($rule["type"]) {
2253 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2254 $rule['reg_exp'] . "')";
2257 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2258 $rule['reg_exp'] . "')";
2261 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2262 $rule['reg_exp'] . "') OR LOWER(" .
2263 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2266 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2267 $rule['reg_exp'] . "')";
2270 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2271 $rule['reg_exp'] . "')";
2274 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2275 $rule['reg_exp'] . "')";
2279 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2281 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2282 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2285 if (isset($rule["cat_id"])) {
2287 if ($rule["cat_id"] > 0) {
2288 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2289 array_push($children, $rule["cat_id"]);
2290 $children = array_map("intval", $children);
2292 $children = join(",", $children);
2294 $cat_qpart = "cat_id IN ($children)";
2296 $cat_qpart = "cat_id IS NULL";
2299 $qpart .= " AND $cat_qpart";
2302 $qpart .= " AND feed_id IS NOT NULL";
2304 array_push($query, "($qpart)");
2309 if (count($query) > 0) {
2310 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2312 $fullquery = "(false)";
2315 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2320 if (!function_exists('gzdecode')) {
2321 function gzdecode($string) { // no support for 2nd argument
2322 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2323 base64_encode($string));
2327 function get_random_bytes($length) {
2328 if (function_exists('openssl_random_pseudo_bytes')) {
2329 return openssl_random_pseudo_bytes($length);
2333 for ($i = 0; $i < $length; $i++
)
2334 $output .= chr(mt_rand(0, 255));
2340 function read_stdin() {
2341 $fp = fopen("php://stdin", "r");
2344 $line = trim(fgets($fp));
2352 function implements_interface($class, $interface) {
2353 return in_array($interface, class_implements($class));
2356 function get_minified_js($files) {
2357 require_once 'lib/jshrink/Minifier.php';
2361 foreach ($files as $js) {
2362 if (!isset($_GET['debug'])) {
2363 $cached_file = CACHE_DIR
. "/js/".basename($js);
2365 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2367 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2369 if ($header && $contents) {
2370 list($htag, $hversion) = explode(":", $header);
2372 if ($htag == "tt-rss" && $hversion == VERSION
) {
2379 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2380 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2384 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2391 function calculate_dep_timestamp() {
2392 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2396 foreach ($files as $file) {
2397 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2403 function T_js_decl($s1, $s2) {
2405 $s1 = preg_replace("/\n/", "", $s1);
2406 $s2 = preg_replace("/\n/", "", $s2);
2408 $s1 = preg_replace("/\"/", "\\\"", $s1);
2409 $s2 = preg_replace("/\"/", "\\\"", $s2);
2411 return "T_messages[\"$s1\"] = \"$s2\";\n";
2415 function init_js_translations() {
2417 print 'var T_messages = new Object();
2420 if (T_messages[msg]) {
2421 return T_messages[msg];
2427 function ngettext(msg1, msg2, n) {
2428 return __((parseInt(n) > 1) ? msg2 : msg1);
2431 $l10n = _get_reader();
2433 for ($i = 0; $i < $l10n->total
; $i++
) {
2434 $orig = $l10n->get_original_string($i);
2435 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2436 $key = explode(chr(0), $orig);
2437 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2438 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2440 $translation = __($orig);
2441 print T_js_decl($orig, $translation);
2446 function get_theme_path($theme) {
2447 if ($theme == "default.php")
2448 return "css/default.css";
2450 $check = "themes/$theme";
2451 if (file_exists($check)) return $check;
2453 $check = "themes.local/$theme";
2454 if (file_exists($check)) return $check;
2457 function theme_valid($theme) {
2458 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2460 if (in_array($theme, $bundled_themes)) return true;
2462 $file = "themes/" . basename($theme);
2464 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2466 if (file_exists($file) && is_readable($file)) {
2467 $fh = fopen($file, "r");
2470 $header = fgets($fh);
2473 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2481 * @SuppressWarnings(unused)
2483 function error_json($code) {
2484 require_once "errors.php";
2486 @$message = $ERRORS[$code];
2488 return json_encode(array("error" =>
2489 array("code" => $code, "message" => $message)));
2493 /*function abs_to_rel_path($dir) {
2494 $tmp = str_replace(dirname(__DIR__), "", $dir);
2496 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2501 function get_upload_error_message($code) {
2504 0 => __('There is no error, the file uploaded with success'),
2505 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2506 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2507 3 => __('The uploaded file was only partially uploaded'),
2508 4 => __('No file was uploaded'),
2509 6 => __('Missing a temporary folder'),
2510 7 => __('Failed to write file to disk.'),
2511 8 => __('A PHP extension stopped the file upload.'),
2514 return $errors[$code];
2517 function base64_img($filename) {
2518 if (file_exists($filename)) {
2519 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2521 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2527 /* this is essentially a wrapper for readfile() which allows plugins to hook
2528 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2530 hook function should return true if request was handled (or at least attempted to)
2532 note that this can be called without user context so the plugin to handle this
2533 should be loaded systemwide in config.php */
2534 function send_local_file($filename) {
2535 if (file_exists($filename)) {
2536 $tmppluginhost = new PluginHost();
2538 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2539 $tmppluginhost->load_data();
2541 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2542 if ($plugin->hook_send_local_file($filename)) return true;
2545 $mimetype = mime_content_type($filename);
2546 header("Content-type: $mimetype");
2548 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2549 header("Last-Modified: $stamp", true);
2551 return readfile($filename);
2557 function check_mysql_tables() {
2560 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2561 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2562 $sth->execute([DB_NAME
]);
2566 while ($line = $sth->fetch()) {
2567 array_push($bad_tables, $line);
2573 function validate_field($string, $allowed, $default = "") {
2574 if (in_array($string, $allowed))
2580 function arr_qmarks($arr) {
2581 return str_repeat('?,', count($arr) - 1) . '?';