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(
497 'ignore_errors' => true,
498 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
499 'protocol_version'=> 1.1)
502 if (!$post_query && $last_modified) {
503 $context_options['http']['header'] = "If-Modified-Since: $last_modified\r\n";
506 if (defined('_HTTP_PROXY')) {
507 $context_options['http']['request_fulluri'] = true;
508 $context_options['http']['proxy'] = _HTTP_PROXY
;
511 $context = stream_context_create($context_options);
513 $old_error = error_get_last();
515 $fetch_effective_url = $url;
517 $data = @file_get_contents
($url, false, $context);
519 if (isset($http_response_header) && is_array($http_response_header)) {
520 foreach ($http_response_header as $header) {
521 if (strstr($header, ": ") !== FALSE) {
522 list ($key, $value) = explode(": ", $header);
524 $key = strtolower($key);
526 if ($key == 'content-type') {
527 $fetch_last_content_type = $value;
528 // don't abort here b/c there might be more than one
529 // e.g. if we were being redirected -- last one is the right one
530 } else if ($key == 'last-modified') {
531 $fetch_last_modified = $value;
532 } else if ($key == 'location') {
533 $fetch_effective_url = $value;
537 if (substr(strtolower($header), 0, 7) == 'http/1.') {
538 $fetch_last_error_code = (int) substr($header, 9, 3);
539 $fetch_last_error = $header;
544 if ($fetch_last_error_code != 200) {
545 $error = error_get_last();
547 if ($error['message'] != $old_error['message']) {
548 $fetch_last_error .= "; " . $error["message"];
551 $fetch_last_error_content = $data;
561 * Try to determine the favicon URL for a feed.
562 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
563 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
565 * @param string $url A feed or page URL
567 * @return mixed The favicon URL, or false if none was found.
569 function get_favicon_url($url) {
571 $favicon_url = false;
573 if ($html = @fetch_file_contents
($url)) {
575 libxml_use_internal_errors(true);
577 $doc = new DOMDocument();
578 $doc->loadHTML($html);
579 $xpath = new DOMXPath($doc);
581 $base = $xpath->query('/html/head/base[@href]');
582 foreach ($base as $b) {
583 $url = rewrite_relative_url($url, $b->getAttribute("href"));
587 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
588 if (count($entries) > 0) {
589 foreach ($entries as $entry) {
590 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
597 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
600 } // function get_favicon_url
602 function initialize_user_prefs($uid, $profile = false) {
604 if (get_schema_version() < 63) $profile_qpart = "";
607 $in_nested_tr = false;
610 $pdo->beginTransaction();
611 } catch (Exception
$e) {
612 $in_nested_tr = true;
615 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
617 $profile = $profile ?
$profile : null;
619 $u_sth = $pdo->prepare("SELECT pref_name
620 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
621 (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
622 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
624 $active_prefs = array();
626 while ($line = $u_sth->fetch()) {
627 array_push($active_prefs, $line["pref_name"]);
630 while ($line = $sth->fetch()) {
631 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
632 // print "adding " . $line["pref_name"] . "<br>";
634 if (get_schema_version() < 63) {
635 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
636 (owner_uid,pref_name,value) VALUES
638 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
641 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
642 (owner_uid,pref_name,value, profile) VALUES
644 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
650 if (!$in_nested_tr) $pdo->commit();
654 function get_ssl_certificate_id() {
655 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
656 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
657 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
658 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
659 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
661 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
662 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
663 $_SERVER["SSL_CLIENT_V_START"] .
664 $_SERVER["SSL_CLIENT_V_END"] .
665 $_SERVER["SSL_CLIENT_S_DN"]);
670 function authenticate_user($login, $password, $check_only = false) {
672 if (!SINGLE_USER_MODE
) {
675 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
677 $user_id = (int) $plugin->authenticate($login, $password);
680 $_SESSION["auth_module"] = strtolower(get_class($plugin));
685 if ($user_id && !$check_only) {
688 $_SESSION["uid"] = $user_id;
689 $_SESSION["version"] = VERSION_STATIC
;
692 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
694 $sth->execute([$user_id]);
695 $row = $sth->fetch();
697 $_SESSION["name"] = $row["login"];
698 $_SESSION["access_level"] = $row["access_level"];
699 $_SESSION["csrf_token"] = uniqid_short();
701 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
702 $usth->execute([$user_id]);
704 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
705 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
706 $_SESSION["pwd_hash"] = $row["pwd_hash"];
708 $_SESSION["last_version_check"] = time();
710 initialize_user_prefs($_SESSION["uid"]);
719 $_SESSION["uid"] = 1;
720 $_SESSION["name"] = "admin";
721 $_SESSION["access_level"] = 10;
723 $_SESSION["hide_hello"] = true;
724 $_SESSION["hide_logout"] = true;
726 $_SESSION["auth_module"] = false;
728 if (!$_SESSION["csrf_token"]) {
729 $_SESSION["csrf_token"] = uniqid_short();
732 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
734 initialize_user_prefs($_SESSION["uid"]);
740 // this is used for user http parameters unless HTML code is actually needed
741 function clean($param) {
742 if (is_array($param)) {
743 return array_map("strip_tags", $param);
744 } else if (is_string($param)) {
745 return strip_tags($param);
751 function make_password($length = 8) {
754 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
758 while ($i < $length) {
759 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
761 if (!strstr($password, $char)) {
769 // this is called after user is created to initialize default feeds, labels
772 // user preferences are checked on every login, not here
774 function initialize_user($uid) {
778 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
779 values (?, 'Tiny Tiny RSS: Forum',
780 'http://tt-rss.org/forum/rss.php')");
781 $sth->execute([$uid]);
784 function logout_user() {
786 if (isset($_COOKIE[session_name()])) {
787 setcookie(session_name(), '', time()-42000, '/');
791 function validate_csrf($csrf_token) {
792 return $csrf_token == $_SESSION['csrf_token'];
795 function load_user_plugins($owner_uid, $pluginhost = false) {
797 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
799 if ($owner_uid && SCHEMA_VERSION
>= 100) {
800 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
802 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
804 if (get_schema_version() > 100) {
805 $pluginhost->load_data();
810 function login_sequence() {
813 if (SINGLE_USER_MODE
) {
815 authenticate_user("admin", null);
817 load_user_plugins($_SESSION["uid"]);
819 if (!validate_session()) $_SESSION["uid"] = false;
821 if (!$_SESSION["uid"]) {
823 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
824 $_SESSION["ref_schema_version"] = get_schema_version(true);
826 authenticate_user(null, null, true);
829 if (!$_SESSION["uid"]) {
831 setcookie(session_name(), '', time()-42000, '/');
838 /* bump login timestamp */
839 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
840 $sth->execute([$_SESSION['uid']]);
842 $_SESSION["last_login_update"] = time();
845 if ($_SESSION["uid"]) {
847 load_user_plugins($_SESSION["uid"]);
851 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
853 (SELECT COUNT(id) FROM ttrss_feeds WHERE
854 ttrss_feeds.id = feed_id) = 0");
856 $sth->execute([$_SESSION['uid']]);
858 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
860 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
861 ttrss_feed_categories.id = feed_id) = 0");
863 $sth->execute([$_SESSION['uid']]);
869 function truncate_string($str, $max_len, $suffix = '…') {
870 if (mb_strlen($str, "utf-8") > $max_len) {
871 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
878 function truncate_middle($str, $max_len, $suffix = '…') {
879 if (strlen($str) > $max_len) {
880 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
886 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
889 $source_tz = new DateTimeZone($source_tz);
890 } catch (Exception
$e) {
891 $source_tz = new DateTimeZone('UTC');
895 $dest_tz = new DateTimeZone($dest_tz);
896 } catch (Exception
$e) {
897 $dest_tz = new DateTimeZone('UTC');
900 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
901 return $dt->format('U') +
$dest_tz->getOffset($dt);
904 function make_local_datetime($timestamp, $long, $owner_uid = false,
905 $no_smart_dt = false, $eta_min = false) {
907 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
908 if (!$timestamp) $timestamp = '1970-01-01 0:00';
913 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
915 $timestamp = substr($timestamp, 0, 19);
917 # We store date in UTC internally
918 $dt = new DateTime($timestamp, $utc_tz);
920 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
922 if ($user_tz_string != 'Automatic') {
925 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
926 } catch (Exception
$e) {
930 $tz_offset = $user_tz->getOffset($dt);
932 $tz_offset = (int) -$_SESSION["clientTzOffset"];
935 $user_timestamp = $dt->format('U') +
$tz_offset;
938 return smart_date_time($user_timestamp,
939 $tz_offset, $owner_uid, $eta_min);
942 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
944 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
946 return date($format, $user_timestamp);
950 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
951 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
953 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
954 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
955 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
956 return date("G:i", $timestamp);
957 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
958 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
959 return date($format, $timestamp);
961 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
962 return date($format, $timestamp);
966 function sql_bool_to_bool($s) {
967 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
970 function bool_to_sql_bool($s) {
974 // Session caching removed due to causing wrong redirects to upgrade
975 // script when get_schema_version() is called on an obsolete session
976 // created on a previous schema version.
977 function get_schema_version($nocache = false) {
978 global $schema_version;
982 if (!$schema_version && !$nocache) {
983 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
984 $version = $row["schema_version"];
985 $schema_version = $version;
988 return $schema_version;
992 function sanity_check() {
993 require_once 'errors.php';
997 $schema_version = get_schema_version(true);
999 if ($schema_version != SCHEMA_VERSION
) {
1003 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1006 function file_is_locked($filename) {
1007 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
1008 if (function_exists('flock')) {
1009 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1011 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1012 flock($fp, LOCK_UN
);
1022 return true; // consider the file always locked and skip the test
1029 function make_lockfile($filename) {
1030 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1032 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1033 $stat_h = fstat($fp);
1034 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1036 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1037 if ($stat_h["ino"] != $stat_f["ino"] ||
1038 $stat_h["dev"] != $stat_f["dev"]) {
1044 if (function_exists('posix_getpid')) {
1045 fwrite($fp, posix_getpid() . "\n");
1053 function make_stampfile($filename) {
1054 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1056 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1057 fwrite($fp, time() . "\n");
1058 flock($fp, LOCK_UN
);
1066 function sql_random_function() {
1067 if (DB_TYPE
== "mysql") {
1074 function getFeedUnread($feed, $is_cat = false) {
1075 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1078 function checkbox_to_sql_bool($val) {
1079 return ($val == "on") ?
1 : 0;
1082 function uniqid_short() {
1083 return uniqid(base_convert(rand(), 10, 36));
1086 function make_init_params() {
1089 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1090 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1091 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1092 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1094 $params[strtolower($param)] = (int) get_pref($param);
1097 $params["icons_url"] = ICONS_URL
;
1098 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1099 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1100 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1101 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1102 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1103 $params["is_default_pw"] = Pref_Prefs
::isdefaultpassword();
1104 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1106 $theme = get_pref( "USER_CSS_THEME", false, false);
1107 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1109 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1111 $params["php_platform"] = PHP_OS
;
1112 $params["php_version"] = PHP_VERSION
;
1114 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1118 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1119 ttrss_feeds WHERE owner_uid = ?");
1120 $sth->execute([$_SESSION['uid']]);
1121 $row = $sth->fetch();
1123 $max_feed_id = $row["mid"];
1124 $num_feeds = $row["nf"];
1126 $params["max_feed_id"] = (int) $max_feed_id;
1127 $params["num_feeds"] = (int) $num_feeds;
1129 $params["hotkeys"] = get_hotkeys_map();
1131 $params["csrf_token"] = $_SESSION["csrf_token"];
1132 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1134 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1136 $params["icon_alert"] = base64_img("images/alert.png");
1137 $params["icon_information"] = base64_img("images/information.png");
1138 $params["icon_cross"] = base64_img("images/cross.png");
1139 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1141 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1146 function get_hotkeys_info() {
1148 __("Navigation") => array(
1149 "next_feed" => __("Open next feed"),
1150 "prev_feed" => __("Open previous feed"),
1151 "next_article" => __("Open next article"),
1152 "prev_article" => __("Open previous article"),
1153 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1154 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1155 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1156 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1157 "search_dialog" => __("Show search dialog")),
1158 __("Article") => array(
1159 "toggle_mark" => __("Toggle starred"),
1160 "toggle_publ" => __("Toggle published"),
1161 "toggle_unread" => __("Toggle unread"),
1162 "edit_tags" => __("Edit tags"),
1163 "open_in_new_window" => __("Open in new window"),
1164 "catchup_below" => __("Mark below as read"),
1165 "catchup_above" => __("Mark above as read"),
1166 "article_scroll_down" => __("Scroll down"),
1167 "article_scroll_up" => __("Scroll up"),
1168 "select_article_cursor" => __("Select article under cursor"),
1169 "email_article" => __("Email article"),
1170 "close_article" => __("Close/collapse article"),
1171 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1172 "toggle_widescreen" => __("Toggle widescreen mode"),
1173 "toggle_embed_original" => __("Toggle embed original")),
1174 __("Article selection") => array(
1175 "select_all" => __("Select all articles"),
1176 "select_unread" => __("Select unread"),
1177 "select_marked" => __("Select starred"),
1178 "select_published" => __("Select published"),
1179 "select_invert" => __("Invert selection"),
1180 "select_none" => __("Deselect everything")),
1181 __("Feed") => array(
1182 "feed_refresh" => __("Refresh current feed"),
1183 "feed_unhide_read" => __("Un/hide read feeds"),
1184 "feed_subscribe" => __("Subscribe to feed"),
1185 "feed_edit" => __("Edit feed"),
1186 "feed_catchup" => __("Mark as read"),
1187 "feed_reverse" => __("Reverse headlines"),
1188 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1189 "feed_debug_update" => __("Debug feed update"),
1190 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1191 "catchup_all" => __("Mark all feeds as read"),
1192 "cat_toggle_collapse" => __("Un/collapse current category"),
1193 "toggle_combined_mode" => __("Toggle combined mode"),
1194 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1195 __("Go to") => array(
1196 "goto_all" => __("All articles"),
1197 "goto_fresh" => __("Fresh"),
1198 "goto_marked" => __("Starred"),
1199 "goto_published" => __("Published"),
1200 "goto_tagcloud" => __("Tag cloud"),
1201 "goto_prefs" => __("Preferences")),
1202 __("Other") => array(
1203 "create_label" => __("Create label"),
1204 "create_filter" => __("Create filter"),
1205 "collapse_sidebar" => __("Un/collapse sidebar"),
1206 "help_dialog" => __("Show help dialog"))
1209 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1210 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1216 function get_hotkeys_map() {
1218 // "navigation" => array(
1221 "n" => "next_article",
1222 "p" => "prev_article",
1223 "(38)|up" => "prev_article",
1224 "(40)|down" => "next_article",
1225 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1226 // "^(40)|Ctrl-down" => "next_article_noscroll",
1227 "(191)|/" => "search_dialog",
1228 // "article" => array(
1229 "s" => "toggle_mark",
1230 "*s" => "toggle_publ",
1231 "u" => "toggle_unread",
1232 "*t" => "edit_tags",
1233 "o" => "open_in_new_window",
1234 "c p" => "catchup_below",
1235 "c n" => "catchup_above",
1236 "*n" => "article_scroll_down",
1237 "*p" => "article_scroll_up",
1238 "*(38)|Shift+up" => "article_scroll_up",
1239 "*(40)|Shift+down" => "article_scroll_down",
1240 "a *w" => "toggle_widescreen",
1241 "a e" => "toggle_embed_original",
1242 "e" => "email_article",
1243 "a q" => "close_article",
1244 // "article_selection" => array(
1245 "a a" => "select_all",
1246 "a u" => "select_unread",
1247 "a *u" => "select_marked",
1248 "a p" => "select_published",
1249 "a i" => "select_invert",
1250 "a n" => "select_none",
1252 "f r" => "feed_refresh",
1253 "f a" => "feed_unhide_read",
1254 "f s" => "feed_subscribe",
1255 "f e" => "feed_edit",
1256 "f q" => "feed_catchup",
1257 "f x" => "feed_reverse",
1258 "f g" => "feed_toggle_vgroup",
1259 "f *d" => "feed_debug_update",
1260 "f *g" => "feed_debug_viewfeed",
1261 "f *c" => "toggle_combined_mode",
1262 "f c" => "toggle_cdm_expanded",
1263 "*q" => "catchup_all",
1264 "x" => "cat_toggle_collapse",
1266 "g a" => "goto_all",
1267 "g f" => "goto_fresh",
1268 "g s" => "goto_marked",
1269 "g p" => "goto_published",
1270 "g t" => "goto_tagcloud",
1271 "g *p" => "goto_prefs",
1272 // "other" => array(
1273 "(9)|Tab" => "select_article_cursor", // tab
1274 "c l" => "create_label",
1275 "c f" => "create_filter",
1276 "c s" => "collapse_sidebar",
1277 "^(191)|Ctrl+/" => "help_dialog",
1280 if (get_pref('COMBINED_DISPLAY_MODE')) {
1281 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1282 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1285 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1286 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1289 $prefixes = array();
1291 foreach (array_keys($hotkeys) as $hotkey) {
1292 $pair = explode(" ", $hotkey, 2);
1294 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1295 array_push($prefixes, $pair[0]);
1299 return array($prefixes, $hotkeys);
1302 function check_for_update() {
1303 if (defined("GIT_VERSION_TIMESTAMP")) {
1304 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1307 $content = json_decode($content, true);
1309 if ($content && isset($content["changeset"])) {
1310 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1311 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1313 return $content["changeset"]["id"];
1322 function make_runtime_info($disable_update_check = false) {
1327 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1328 ttrss_feeds WHERE owner_uid = ?");
1329 $sth->execute([$_SESSION['uid']]);
1330 $row = $sth->fetch();
1332 $max_feed_id = $row['mid'];
1333 $num_feeds = $row['nf'];
1335 $data["max_feed_id"] = (int) $max_feed_id;
1336 $data["num_feeds"] = (int) $num_feeds;
1338 $data['last_article_id'] = Article
::getLastArticleId();
1339 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1341 $data['dep_ts'] = calculate_dep_timestamp();
1342 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1344 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1346 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1347 $update_result = @check_for_update
();
1349 $data["update_result"] = $update_result;
1351 $_SESSION["last_version_check"] = time();
1354 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1356 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1358 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1360 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1363 $stamp_delta = time() - $stamp;
1365 if ($stamp_delta > 1800) {
1369 $_SESSION["daemon_stamp_check"] = time();
1372 $data['daemon_stamp_ok'] = $stamp_check;
1374 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1376 $data['daemon_stamp'] = $stamp_fmt;
1384 function search_to_sql($search, $search_language) {
1386 $keywords = str_getcsv(trim($search), " ");
1387 $query_keywords = array();
1388 $search_words = array();
1389 $search_query_leftover = array();
1393 if ($search_language)
1394 $search_language = $pdo->quote(mb_strtolower($search_language));
1396 $search_language = $pdo->quote("english");
1398 foreach ($keywords as $k) {
1399 if (strpos($k, "-") === 0) {
1406 $commandpair = explode(":", mb_strtolower($k), 2);
1408 switch ($commandpair[0]) {
1410 if ($commandpair[1]) {
1411 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1412 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1414 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1415 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1416 array_push($search_words, $k);
1420 if ($commandpair[1]) {
1421 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1422 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1424 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1425 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1426 array_push($search_words, $k);
1430 if ($commandpair[1]) {
1431 if ($commandpair[1] == "true")
1432 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1433 else if ($commandpair[1] == "false")
1434 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1436 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1437 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1439 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1440 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1441 if (!$not) array_push($search_words, $k);
1446 if ($commandpair[1]) {
1447 if ($commandpair[1] == "true")
1448 array_push($query_keywords, "($not (marked = true))");
1450 array_push($query_keywords, "($not (marked = false))");
1452 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1453 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1454 if (!$not) array_push($search_words, $k);
1458 if ($commandpair[1]) {
1459 if ($commandpair[1] == "true")
1460 array_push($query_keywords, "($not (published = true))");
1462 array_push($query_keywords, "($not (published = false))");
1465 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1466 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1467 if (!$not) array_push($search_words, $k);
1471 if ($commandpair[1]) {
1472 if ($commandpair[1] == "true")
1473 array_push($query_keywords, "($not (unread = true))");
1475 array_push($query_keywords, "($not (unread = false))");
1478 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1479 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1480 if (!$not) array_push($search_words, $k);
1484 if (strpos($k, "@") === 0) {
1486 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1487 $orig_ts = strtotime(substr($k, 1));
1488 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1490 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1492 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1495 if (DB_TYPE
== "pgsql") {
1496 $k = mb_strtolower($k);
1497 array_push($search_query_leftover, $not ?
"!$k" : $k);
1499 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1500 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1503 if (!$not) array_push($search_words, $k);
1508 if (count($search_query_leftover) > 0) {
1509 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1511 if (DB_TYPE
== "pgsql") {
1512 array_push($query_keywords,
1513 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1518 $search_query_part = implode("AND", $query_keywords);
1520 return array($search_query_part, $search_words);
1523 function iframe_whitelisted($entry) {
1524 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1526 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1529 foreach ($whitelist as $w) {
1530 if ($src == $w ||
$src == "www.$w")
1538 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1539 if (!$owner) $owner = $_SESSION["uid"];
1541 $res = trim($str); if (!$res) return '';
1543 $charset_hack = '<head>
1544 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1547 $res = trim($res); if (!$res) return '';
1549 libxml_use_internal_errors(true);
1551 $doc = new DOMDocument();
1552 $doc->loadHTML($charset_hack . $res);
1553 $xpath = new DOMXPath($doc);
1555 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1557 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1559 foreach ($entries as $entry) {
1561 if ($entry->hasAttribute('href')) {
1562 $entry->setAttribute('href',
1563 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1565 $entry->setAttribute('rel', 'noopener noreferrer');
1568 if ($entry->hasAttribute('src')) {
1569 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1570 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1572 if (file_exists($cached_filename)) {
1574 // this is strictly cosmetic
1575 if ($entry->tagName
== 'img') {
1577 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1579 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1585 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1587 if ($entry->hasAttribute('srcset')) {
1588 $entry->removeAttribute('srcset');
1591 if ($entry->hasAttribute('sizes')) {
1592 $entry->removeAttribute('sizes');
1596 $entry->setAttribute('src', $src);
1599 if ($entry->nodeName
== 'img') {
1600 $entry->setAttribute('referrerpolicy', 'no-referrer');
1602 $entry->removeAttribute('width');
1603 $entry->removeAttribute('height');
1605 if ($entry->hasAttribute('src')) {
1606 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1608 if (is_prefix_https() && !$is_https_url) {
1610 if ($entry->hasAttribute('srcset')) {
1611 $entry->removeAttribute('srcset');
1614 if ($entry->hasAttribute('sizes')) {
1615 $entry->removeAttribute('sizes');
1620 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1621 $force_remove_images ||
$_SESSION["bw_limit"]) {
1623 $p = $doc->createElement('p');
1625 $a = $doc->createElement('a');
1626 $a->setAttribute('href', $entry->getAttribute('src'));
1628 $a->appendChild(new DOMText($entry->getAttribute('src')));
1629 $a->setAttribute('target', '_blank');
1630 $a->setAttribute('rel', 'noopener noreferrer');
1632 $p->appendChild($a);
1634 $entry->parentNode
->replaceChild($p, $entry);
1638 if (strtolower($entry->nodeName
) == "a") {
1639 $entry->setAttribute("target", "_blank");
1640 $entry->setAttribute("rel", "noopener noreferrer");
1644 $entries = $xpath->query('//iframe');
1645 foreach ($entries as $entry) {
1646 if (!iframe_whitelisted($entry)) {
1647 $entry->setAttribute('sandbox', 'allow-scripts');
1649 if (is_prefix_https()) {
1650 $entry->setAttribute("src",
1651 str_replace("http://", "https://",
1652 $entry->getAttribute("src")));
1657 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1658 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1659 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1660 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1661 'dt', 'em', 'footer', 'figure', 'figcaption',
1662 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1663 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1664 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1665 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1666 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1667 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1669 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1671 $disallowed_attributes = array('id', 'style', 'class');
1673 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1674 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1675 if (is_array($retval)) {
1677 $allowed_elements = $retval[1];
1678 $disallowed_attributes = $retval[2];
1684 $doc->removeChild($doc->firstChild
); //remove doctype
1685 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1687 if ($highlight_words) {
1688 foreach ($highlight_words as $word) {
1690 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1692 $elements = $xpath->query("//*/text()");
1694 foreach ($elements as $child) {
1696 $fragment = $doc->createDocumentFragment();
1697 $text = $child->textContent
;
1699 while (($pos = mb_stripos($text, $word)) !== false) {
1700 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1701 $word = mb_substr($text, $pos, mb_strlen($word));
1702 $highlight = $doc->createElement('span');
1703 $highlight->appendChild(new DomText($word));
1704 $highlight->setAttribute('class', 'highlight');
1705 $fragment->appendChild($highlight);
1706 $text = mb_substr($text, $pos +
mb_strlen($word));
1709 if (!empty($text)) $fragment->appendChild(new DomText($text));
1711 $child->parentNode
->replaceChild($fragment, $child);
1716 $res = $doc->saveHTML();
1718 /* strip everything outside of <body>...</body> */
1720 $res_frag = array();
1721 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1722 return $res_frag[1];
1728 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1729 $xpath = new DOMXPath($doc);
1730 $entries = $xpath->query('//*');
1732 foreach ($entries as $entry) {
1733 if (!in_array($entry->nodeName
, $allowed_elements)) {
1734 $entry->parentNode
->removeChild($entry);
1737 if ($entry->hasAttributes()) {
1738 $attrs_to_remove = array();
1740 foreach ($entry->attributes
as $attr) {
1742 if (strpos($attr->nodeName
, 'on') === 0) {
1743 array_push($attrs_to_remove, $attr);
1746 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1747 array_push($attrs_to_remove, $attr);
1750 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1751 array_push($attrs_to_remove, $attr);
1755 foreach ($attrs_to_remove as $attr) {
1756 $entry->removeAttributeNode($attr);
1764 function trim_array($array) {
1766 array_walk($tmp, 'trim');
1770 function tag_is_valid($tag) {
1771 if (!$tag ||
is_numeric($tag) ||
mb_strlen($tag) > 250)
1777 function render_login_form() {
1778 header('Cache-Control: public');
1780 require_once "login_form.php";
1784 function T_sprintf() {
1785 $args = func_get_args();
1786 return vsprintf(__(array_shift($args)), $args);
1789 function print_checkpoint($n, $s) {
1790 $ts = microtime(true);
1791 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1795 function sanitize_tag($tag) {
1798 $tag = mb_strtolower($tag, 'utf-8');
1800 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1802 if (DB_TYPE
== "mysql") {
1803 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1809 function is_server_https() {
1810 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1813 function is_prefix_https() {
1814 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1817 // this returns SELF_URL_PATH sans ending slash
1818 function get_self_url_prefix() {
1819 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1820 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1822 return SELF_URL_PATH
;
1826 function encrypt_password($pass, $salt = '', $mode2 = false) {
1827 if ($salt && $mode2) {
1828 return "MODE2:" . hash('sha256', $salt . $pass);
1830 return "SHA1X:" . sha1("$salt:$pass");
1832 return "SHA1:" . sha1($pass);
1834 } // function encrypt_password
1836 function load_filters($feed_id, $owner_uid) {
1839 $feed_id = (int) $feed_id;
1840 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1843 $null_cat_qpart = "cat_id IS NULL OR";
1845 $null_cat_qpart = "";
1849 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1850 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1851 $sth->execute([$owner_uid]);
1853 $check_cats = array_merge(
1854 Feeds
::getParentCategories($cat_id, $owner_uid),
1857 $check_cats_str = join(",", $check_cats);
1858 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1860 while ($line = $sth->fetch()) {
1861 $filter_id = $line["id"];
1863 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1865 $sth2 = $pdo->prepare("SELECT
1866 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1867 FROM ttrss_filters2_rules AS r,
1868 ttrss_filter_types AS t
1870 (match_on IS NOT NULL OR
1871 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1872 (feed_id IS NULL OR feed_id = ?))) AND
1873 filter_type = t.id AND filter_id = ?");
1874 $sth2->execute([$feed_id, $filter_id]);
1879 while ($rule_line = $sth2->fetch()) {
1880 # print_r($rule_line);
1882 if ($rule_line["match_on"]) {
1883 $match_on = json_decode($rule_line["match_on"], true);
1885 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1888 $rule["reg_exp"] = $rule_line["reg_exp"];
1889 $rule["type"] = $rule_line["type_name"];
1890 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1892 array_push($rules, $rule);
1893 } else if (!$match_any_rule) {
1894 // this filter contains a rule that doesn't match to this feed/category combination
1895 // thus filter has to be rejected
1904 $rule["reg_exp"] = $rule_line["reg_exp"];
1905 $rule["type"] = $rule_line["type_name"];
1906 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1908 array_push($rules, $rule);
1912 if (count($rules) > 0) {
1913 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1914 FROM ttrss_filters2_actions AS a,
1915 ttrss_filter_actions AS t
1917 action_id = t.id AND filter_id = ?");
1918 $sth2->execute([$filter_id]);
1920 while ($action_line = $sth2->fetch()) {
1921 # print_r($action_line);
1924 $action["type"] = $action_line["type_name"];
1925 $action["param"] = $action_line["action_param"];
1927 array_push($actions, $action);
1932 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1933 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1934 $filter["rules"] = $rules;
1935 $filter["actions"] = $actions;
1937 if (count($rules) > 0 && count($actions) > 0) {
1938 array_push($filters, $filter);
1945 function get_score_pic($score) {
1947 return "score_high.png";
1948 } else if ($score > 0) {
1949 return "score_half_high.png";
1950 } else if ($score < -100) {
1951 return "score_low.png";
1952 } else if ($score < 0) {
1953 return "score_half_low.png";
1955 return "score_neutral.png";
1959 function init_plugins() {
1960 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1965 function add_feed_category($feed_cat, $parent_cat_id = false) {
1967 if (!$feed_cat) return false;
1969 $feed_cat = mb_substr($feed_cat, 0, 250);
1970 if (!$parent_cat_id) $parent_cat_id = null;
1973 $tr_in_progress = false;
1976 $pdo->beginTransaction();
1977 } catch (Exception
$e) {
1978 $tr_in_progress = true;
1981 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1982 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
1983 AND title = :title AND owner_uid = :uid");
1984 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
1986 if (!$sth->fetch()) {
1988 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1990 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
1992 if (!$tr_in_progress) $pdo->commit();
2003 * Fixes incomplete URLs by prepending "http://".
2004 * Also replaces feed:// with http://, and
2005 * prepends a trailing slash if the url is a domain name only.
2007 * @param string $url Possibly incomplete URL
2009 * @return string Fixed URL.
2011 function fix_url($url) {
2013 // support schema-less urls
2014 if (strpos($url, '//') === 0) {
2015 $url = 'https:' . $url;
2018 if (strpos($url, '://') === false) {
2019 $url = 'http://' . $url;
2020 } else if (substr($url, 0, 5) == 'feed:') {
2021 $url = 'http:' . substr($url, 5);
2024 //prepend slash if the URL has no slash in it
2025 // "http://www.example" -> "http://www.example/"
2026 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2030 //convert IDNA hostname to punycode if possible
2031 if (function_exists("idn_to_ascii")) {
2032 $parts = parse_url($url);
2033 if (mb_detect_encoding($parts['host']) != 'ASCII')
2035 $parts['host'] = idn_to_ascii($parts['host']);
2036 $url = build_url($parts);
2040 if ($url != "http:///")
2046 function validate_feed_url($url) {
2047 $parts = parse_url($url);
2049 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2053 /* function save_email_address($email) {
2054 // FIXME: implement persistent storage of emails
2056 if (!$_SESSION['stored_emails'])
2057 $_SESSION['stored_emails'] = array();
2059 if (!in_array($email, $_SESSION['stored_emails']))
2060 array_push($_SESSION['stored_emails'], $email);
2064 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2066 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2068 $is_cat = bool_to_sql_bool($is_cat);
2072 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2073 WHERE feed_id = ? AND is_cat = ?
2074 AND owner_uid = ?");
2075 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2077 if ($row = $sth->fetch()) {
2078 return $row["access_key"];
2080 $key = uniqid_short();
2082 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2083 (access_key, feed_id, is_cat, owner_uid)
2084 VALUES (?, ?, ?, ?)");
2086 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2092 function get_feeds_from_html($url, $content)
2094 $url = fix_url($url);
2095 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2097 libxml_use_internal_errors(true);
2099 $doc = new DOMDocument();
2100 $doc->loadHTML($content);
2101 $xpath = new DOMXPath($doc);
2102 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2103 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2104 $feedUrls = array();
2105 foreach ($entries as $entry) {
2106 if ($entry->hasAttribute('href')) {
2107 $title = $entry->getAttribute('title');
2109 $title = $entry->getAttribute('type');
2111 $feedUrl = rewrite_relative_url(
2112 $baseUrl, $entry->getAttribute('href')
2114 $feedUrls[$feedUrl] = $title;
2120 function is_html($content) {
2121 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2124 function url_is_html($url, $login = false, $pass = false) {
2125 return is_html(fetch_file_contents($url, false, $login, $pass));
2128 function build_url($parts) {
2129 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2132 function cleanup_url_path($path) {
2133 $path = str_replace("/./", "/", $path);
2134 $path = str_replace("//", "/", $path);
2140 * Converts a (possibly) relative URL to a absolute one.
2142 * @param string $url Base URL (i.e. from where the document is)
2143 * @param string $rel_url Possibly relative URL in the document
2145 * @return string Absolute URL
2147 function rewrite_relative_url($url, $rel_url) {
2148 if (strpos($rel_url, "://") !== false) {
2150 } else if (strpos($rel_url, "//") === 0) {
2151 # protocol-relative URL (rare but they exist)
2153 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2154 # magnet:, feed:, etc
2156 } else if (strpos($rel_url, "/") === 0) {
2157 $parts = parse_url($url);
2158 $parts['path'] = $rel_url;
2159 $parts['path'] = cleanup_url_path($parts['path']);
2161 return build_url($parts);
2164 $parts = parse_url($url);
2165 if (!isset($parts['path'])) {
2166 $parts['path'] = '/';
2168 $dir = $parts['path'];
2169 if (substr($dir, -1) !== '/') {
2170 $dir = dirname($parts['path']);
2171 $dir !== '/' && $dir .= '/';
2173 $parts['path'] = $dir . $rel_url;
2174 $parts['path'] = cleanup_url_path($parts['path']);
2176 return build_url($parts);
2180 function cleanup_tags($days = 14, $limit = 1000) {
2182 $days = (int) $days;
2184 if (DB_TYPE
== "pgsql") {
2185 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2186 } else if (DB_TYPE
== "mysql") {
2187 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2194 while ($limit > 0) {
2197 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2198 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2199 WHERE post_int_id = int_id AND $interval_query AND
2200 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2201 $sth->execute([$limit]);
2205 while ($line = $sth->fetch()) {
2206 array_push($ids, $line['id']);
2209 if (count($ids) > 0) {
2210 $ids = join(",", $ids);
2212 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2213 $tags_deleted = $usth->rowCount();
2218 $limit -= $limit_part;
2221 return $tags_deleted;
2224 function print_user_stylesheet() {
2225 $value = get_pref('USER_STYLESHEET');
2228 print "<style type=\"text/css\">";
2229 print str_replace("<br/>", "\n", $value);
2235 function filter_to_sql($filter, $owner_uid) {
2240 if (DB_TYPE
== "pgsql")
2243 $reg_qpart = "REGEXP";
2245 foreach ($filter["rules"] AS $rule) {
2246 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2247 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2248 $rule['reg_exp']) !== FALSE;
2250 if ($regexp_valid) {
2252 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2254 switch ($rule["type"]) {
2256 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2257 $rule['reg_exp'] . "')";
2260 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2261 $rule['reg_exp'] . "')";
2264 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2265 $rule['reg_exp'] . "') OR LOWER(" .
2266 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2269 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2270 $rule['reg_exp'] . "')";
2273 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2274 $rule['reg_exp'] . "')";
2277 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2278 $rule['reg_exp'] . "')";
2282 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2284 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2285 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2288 if (isset($rule["cat_id"])) {
2290 if ($rule["cat_id"] > 0) {
2291 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2292 array_push($children, $rule["cat_id"]);
2293 $children = array_map("intval", $children);
2295 $children = join(",", $children);
2297 $cat_qpart = "cat_id IN ($children)";
2299 $cat_qpart = "cat_id IS NULL";
2302 $qpart .= " AND $cat_qpart";
2305 $qpart .= " AND feed_id IS NOT NULL";
2307 array_push($query, "($qpart)");
2312 if (count($query) > 0) {
2313 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2315 $fullquery = "(false)";
2318 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2323 if (!function_exists('gzdecode')) {
2324 function gzdecode($string) { // no support for 2nd argument
2325 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2326 base64_encode($string));
2330 function get_random_bytes($length) {
2331 if (function_exists('openssl_random_pseudo_bytes')) {
2332 return openssl_random_pseudo_bytes($length);
2336 for ($i = 0; $i < $length; $i++
)
2337 $output .= chr(mt_rand(0, 255));
2343 function read_stdin() {
2344 $fp = fopen("php://stdin", "r");
2347 $line = trim(fgets($fp));
2355 function implements_interface($class, $interface) {
2356 return in_array($interface, class_implements($class));
2359 function get_minified_js($files) {
2360 require_once 'lib/jshrink/Minifier.php';
2364 foreach ($files as $js) {
2365 if (!isset($_GET['debug'])) {
2366 $cached_file = CACHE_DIR
. "/js/".basename($js);
2368 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2370 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2372 if ($header && $contents) {
2373 list($htag, $hversion) = explode(":", $header);
2375 if ($htag == "tt-rss" && $hversion == VERSION
) {
2382 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js"));
2383 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2387 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2394 function calculate_dep_timestamp() {
2395 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2399 foreach ($files as $file) {
2400 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2406 function T_js_decl($s1, $s2) {
2408 $s1 = preg_replace("/\n/", "", $s1);
2409 $s2 = preg_replace("/\n/", "", $s2);
2411 $s1 = preg_replace("/\"/", "\\\"", $s1);
2412 $s2 = preg_replace("/\"/", "\\\"", $s2);
2414 return "T_messages[\"$s1\"] = \"$s2\";\n";
2418 function init_js_translations() {
2420 print 'var T_messages = new Object();
2423 if (T_messages[msg]) {
2424 return T_messages[msg];
2430 function ngettext(msg1, msg2, n) {
2431 return __((parseInt(n) > 1) ? msg2 : msg1);
2434 $l10n = _get_reader();
2436 for ($i = 0; $i < $l10n->total
; $i++
) {
2437 $orig = $l10n->get_original_string($i);
2438 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2439 $key = explode(chr(0), $orig);
2440 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2441 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2443 $translation = __($orig);
2444 print T_js_decl($orig, $translation);
2449 function get_theme_path($theme) {
2450 if ($theme == "default.php")
2451 return "css/default.css";
2453 $check = "themes/$theme";
2454 if (file_exists($check)) return $check;
2456 $check = "themes.local/$theme";
2457 if (file_exists($check)) return $check;
2460 function theme_valid($theme) {
2461 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2463 if (in_array($theme, $bundled_themes)) return true;
2465 $file = "themes/" . basename($theme);
2467 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2469 if (file_exists($file) && is_readable($file)) {
2470 $fh = fopen($file, "r");
2473 $header = fgets($fh);
2476 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2484 * @SuppressWarnings(unused)
2486 function error_json($code) {
2487 require_once "errors.php";
2489 @$message = $ERRORS[$code];
2491 return json_encode(array("error" =>
2492 array("code" => $code, "message" => $message)));
2496 /*function abs_to_rel_path($dir) {
2497 $tmp = str_replace(dirname(__DIR__), "", $dir);
2499 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2504 function get_upload_error_message($code) {
2507 0 => __('There is no error, the file uploaded with success'),
2508 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2509 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2510 3 => __('The uploaded file was only partially uploaded'),
2511 4 => __('No file was uploaded'),
2512 6 => __('Missing a temporary folder'),
2513 7 => __('Failed to write file to disk.'),
2514 8 => __('A PHP extension stopped the file upload.'),
2517 return $errors[$code];
2520 function base64_img($filename) {
2521 if (file_exists($filename)) {
2522 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2524 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2530 /* this is essentially a wrapper for readfile() which allows plugins to hook
2531 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2533 hook function should return true if request was handled (or at least attempted to)
2535 note that this can be called without user context so the plugin to handle this
2536 should be loaded systemwide in config.php */
2537 function send_local_file($filename) {
2538 if (file_exists($filename)) {
2539 $tmppluginhost = new PluginHost();
2541 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2542 $tmppluginhost->load_data();
2544 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2545 if ($plugin->hook_send_local_file($filename)) return true;
2548 $mimetype = mime_content_type($filename);
2549 header("Content-type: $mimetype");
2551 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2552 header("Last-Modified: $stamp", true);
2554 return readfile($filename);
2560 function check_mysql_tables() {
2563 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2564 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2565 $sth->execute([DB_NAME
]);
2569 while ($line = $sth->fetch()) {
2570 array_push($bad_tables, $line);
2576 function validate_field($string, $allowed, $default = "") {
2577 if (in_array($string, $allowed))
2583 function arr_qmarks($arr) {
2584 return str_repeat('?,', count($arr) - 1) . '?';