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_curl_used = false;
15 $suppress_debugging = false;
17 libxml_disable_entity_loader(true);
19 // separate test because this is included before sanity checks
20 if (function_exists("mb_internal_encoding")) mb_internal_encoding("UTF-8");
22 date_default_timezone_set('UTC');
23 if (defined('E_DEPRECATED')) {
24 error_reporting(E_ALL
& ~E_NOTICE
& ~E_DEPRECATED
);
26 error_reporting(E_ALL
& ~E_NOTICE
);
29 require_once 'config.php';
32 * Define a constant if not already defined
34 function define_default($name, $value) {
35 defined($name) or define($name, $value);
38 /* Some tunables you can override in config.php using define(): */
40 define_default('FEED_FETCH_TIMEOUT', 45);
41 // How may seconds to wait for response when requesting feed from a site
42 define_default('FEED_FETCH_NO_CACHE_TIMEOUT', 15);
43 // How may seconds to wait for response when requesting feed from a
44 // site when that feed wasn't cached before
45 define_default('FILE_FETCH_TIMEOUT', 45);
46 // Default timeout when fetching files from remote sites
47 define_default('FILE_FETCH_CONNECT_TIMEOUT', 15);
48 // How many seconds to wait for initial response from website when
49 // fetching files from remote sites
50 define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
51 // stop updating feeds if users haven't logged in for X days
52 define_default('DAEMON_FEED_LIMIT', 500);
53 // feed limit for one update batch
54 define_default('DAEMON_SLEEP_INTERVAL', 120);
55 // default sleep interval between feed updates (sec)
56 define_default('MIN_CACHE_FILE_SIZE', 1024);
57 // do not cache files smaller than that (bytes)
58 define_default('CACHE_MAX_DAYS', 7);
59 // max age in days for various automatically cached (temporary) files
60 define_default('MAX_CONDITIONAL_INTERVAL', 3600*6);
61 // max interval between forced unconditional updates for servers
62 // not complying with http if-modified-since (seconds)
64 /* tunables end here */
66 if (DB_TYPE
== "pgsql") {
67 define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
69 define('SUBSTRING_FOR_DATE', 'SUBSTRING');
73 * Return available translations names.
76 * @return array A array of available translations.
78 function get_translations() {
80 "auto" => "Detect automatically",
81 "ar_SA" => "العربيّة (Arabic)",
82 "bg_BG" => "Bulgarian",
87 "el_GR" => "Ελληνικά",
88 "es_ES" => "Español (España)",
91 "fr_FR" => "Français",
92 "hu_HU" => "Magyar (Hungarian)",
93 "it_IT" => "Italiano",
94 "ja_JP" => "日本語 (Japanese)",
95 "lv_LV" => "Latviešu",
96 "nb_NO" => "Norwegian bokmål",
100 "pt_BR" => "Portuguese/Brazil",
101 "pt_PT" => "Portuguese/Portugal",
102 "zh_CN" => "Simplified Chinese",
103 "zh_TW" => "Traditional Chinese",
104 "sv_SE" => "Svenska",
106 "tr_TR" => "Türkçe");
111 require_once "lib/accept-to-gettext.php";
112 require_once "lib/gettext/gettext.inc";
114 function startup_gettext() {
116 # Get locale from Accept-Language header
117 $lang = al2gt(array_keys(get_translations()), "text/html");
119 if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
120 $lang = _TRANSLATION_OVERRIDE_DEFAULT
;
123 if ($_SESSION["uid"] && get_schema_version() >= 120) {
124 $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
126 if ($pref_lang && $pref_lang != 'auto') {
132 if (defined('LC_MESSAGES')) {
133 _setlocale(LC_MESSAGES
, $lang);
134 } else if (defined('LC_ALL')) {
135 _setlocale(LC_ALL
, $lang);
138 _bindtextdomain("messages", "locale");
140 _textdomain("messages");
141 _bind_textdomain_codeset("messages", "UTF-8");
145 require_once 'db-prefs.php';
146 require_once 'version.php';
147 require_once 'controls.php';
149 define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION
. ' (http://tt-rss.org/)');
150 ini_set('user_agent', SELF_USER_AGENT
);
152 $schema_version = false;
154 function _debug_suppress($suppress) {
155 global $suppress_debugging;
157 $suppress_debugging = $suppress;
161 * Print a timestamped debug message.
163 * @param string $msg The debug message.
166 function _debug($msg, $show = true) {
167 global $suppress_debugging;
169 //echo "[$suppress_debugging] $msg $show\n";
171 if ($suppress_debugging) return false;
173 $ts = strftime("%H:%M:%S", time());
174 if (function_exists('posix_getpid')) {
175 $ts = "$ts/" . posix_getpid();
178 if ($show && !(defined('QUIET') && QUIET
)) {
179 print "[$ts] $msg\n";
182 if (defined('LOGFILE')) {
183 $fp = fopen(LOGFILE
, 'a+');
188 if (function_exists("flock")) {
191 // try to lock logfile for writing
192 while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB
)) {
203 fputs($fp, "[$ts] $msg\n");
205 if (function_exists("flock")) {
216 * Purge a feed old posts.
218 * @param mixed $link A database connection.
219 * @param mixed $feed_id The id of the purged feed.
220 * @param mixed $purge_interval Olderness of purged posts.
221 * @param boolean $debug Set to True to enable the debug. False by default.
225 function purge_feed($feed_id, $purge_interval, $debug = false) {
227 if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
232 "SELECT owner_uid FROM ttrss_feeds WHERE id = '$feed_id'");
236 if (db_num_rows($result) == 1) {
237 $owner_uid = db_fetch_result($result, 0, "owner_uid");
240 if ($purge_interval == -1 ||
!$purge_interval) {
242 CCache
::update($feed_id, $owner_uid);
247 if (!$owner_uid) return;
249 if (FORCE_ARTICLE_PURGE
== 0) {
250 $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
253 $purge_unread = true;
254 $purge_interval = FORCE_ARTICLE_PURGE
;
257 if (!$purge_unread) $query_limit = " unread = false AND ";
259 if (DB_TYPE
== "pgsql") {
260 $result = db_query("DELETE FROM ttrss_user_entries
262 WHERE ttrss_entries.id = ref_id AND
264 feed_id = '$feed_id' AND
266 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
270 /* $result = db_query("DELETE FROM ttrss_user_entries WHERE
271 marked = false AND feed_id = '$feed_id' AND
272 (SELECT date_updated FROM ttrss_entries WHERE
273 id = ref_id) < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)"); */
275 $result = db_query("DELETE FROM ttrss_user_entries
276 USING ttrss_user_entries, ttrss_entries
277 WHERE ttrss_entries.id = ref_id AND
279 feed_id = '$feed_id' AND
281 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
284 $rows = db_affected_rows($result);
286 CCache
::update($feed_id, $owner_uid);
289 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
293 } // function purge_feed
295 function feed_purge_interval($feed_id) {
297 $result = db_query("SELECT purge_interval, owner_uid FROM ttrss_feeds
298 WHERE id = '$feed_id'");
300 if (db_num_rows($result) == 1) {
301 $purge_interval = db_fetch_result($result, 0, "purge_interval");
302 $owner_uid = db_fetch_result($result, 0, "owner_uid");
304 if ($purge_interval == 0) $purge_interval = get_pref(
305 'PURGE_OLD_DAYS', $owner_uid);
307 return $purge_interval;
314 /*function get_feed_update_interval($feed_id) {
315 $result = db_query("SELECT owner_uid, update_interval FROM
316 ttrss_feeds WHERE id = '$feed_id'");
318 if (db_num_rows($result) == 1) {
319 $update_interval = db_fetch_result($result, 0, "update_interval");
320 $owner_uid = db_fetch_result($result, 0, "owner_uid");
322 if ($update_interval != 0) {
323 return $update_interval;
325 return get_pref('DEFAULT_UPDATE_INTERVAL', $owner_uid, false);
333 // TODO: multiple-argument way is deprecated, first parameter is a hash now
334 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
335 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
337 global $fetch_last_error;
338 global $fetch_last_error_code;
339 global $fetch_last_error_content;
340 global $fetch_last_content_type;
341 global $fetch_last_modified;
342 global $fetch_curl_used;
344 $fetch_last_error = false;
345 $fetch_last_error_code = -1;
346 $fetch_last_error_content = "";
347 $fetch_last_content_type = "";
348 $fetch_curl_used = false;
349 $fetch_last_modified = "";
351 if (!is_array($options)) {
353 // falling back on compatibility shim
354 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
357 for ($i = 0; $i < func_num_args(); $i++
) {
358 $tmp[$option_names[$i]] = func_get_arg($i);
364 "url" => func_get_arg(0),
365 "type" => @func_get_arg(1),
366 "login" => @func_get_arg(2),
367 "pass" => @func_get_arg(3),
368 "post_query" => @func_get_arg(4),
369 "timeout" => @func_get_arg(5),
370 "timestamp" => @func_get_arg(6),
371 "useragent" => @func_get_arg(7)
375 $url = $options["url"];
376 $type = isset($options["type"]) ?
$options["type"] : false;
377 $login = isset($options["login"]) ?
$options["login"] : false;
378 $pass = isset($options["pass"]) ?
$options["pass"] : false;
379 $post_query = isset($options["post_query"]) ?
$options["post_query"] : false;
380 $timeout = isset($options["timeout"]) ?
$options["timeout"] : false;
381 $last_modified = isset($options["last_modified"]) ?
$options["last_modified"] : "";
382 $useragent = isset($options["useragent"]) ?
$options["useragent"] : false;
383 $followlocation = isset($options["followlocation"]) ?
$options["followlocation"] : true;
385 $url = ltrim($url, ' ');
386 $url = str_replace(' ', '%20', $url);
388 if (strpos($url, "//") === 0)
389 $url = 'http:' . $url;
391 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
393 $fetch_curl_used = true;
395 $ch = curl_init($url);
397 if ($last_modified && !$post_query) {
398 curl_setopt($ch, CURLOPT_HTTPHEADER
,
399 array("If-Modified-Since: $last_modified"));
402 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
403 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
404 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
405 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
406 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
407 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
408 curl_setopt($ch, CURLOPT_HEADER
, true);
409 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
410 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
412 curl_setopt($ch, CURLOPT_ENCODING
, "");
413 //curl_setopt($ch, CURLOPT_REFERER, $url);
415 if (!ini_get("open_basedir")) {
416 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
419 if (defined('_CURL_HTTP_PROXY')) {
420 curl_setopt($ch, CURLOPT_PROXY
, _CURL_HTTP_PROXY
);
424 curl_setopt($ch, CURLOPT_POST
, true);
425 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
429 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
431 $ret = @curl_exec
($ch);
433 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE
);
434 $headers = explode("\r\n", substr($ret, 0, $headers_length));
435 $contents = substr($ret, $headers_length);
437 foreach ($headers as $header) {
438 if (strstr($header, ": ") !== FALSE) {
439 list ($key, $value) = explode(": ", $header);
441 if (strtolower($key) == "last-modified") {
442 $fetch_last_modified = $value;
446 if (substr(strtolower($header), 0, 7) == 'http/1.') {
447 $fetch_last_error_code = (int) substr($header, 9, 3);
448 $fetch_last_error = $header;
452 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
453 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
454 $contents = @curl_exec
($ch);
457 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
458 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
460 $fetch_last_error_code = $http_code;
462 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
464 if (curl_errno($ch) != 0) {
465 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
468 $fetch_last_error_content = $contents;
474 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
484 $fetch_curl_used = false;
486 if ($login && $pass){
487 $url_parts = array();
489 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
491 $pass = urlencode($pass);
493 if ($url_parts[1] && $url_parts[2]) {
494 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
498 // TODO: should this support POST requests or not? idk
500 if (!$post_query && $last_modified) {
501 $context = stream_context_create(array(
504 'ignore_errors' => true,
505 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
506 'protocol_version'=> 1.1,
507 'header' => "If-Modified-Since: $last_modified\r\n")
510 $context = stream_context_create(array(
513 'ignore_errors' => true,
514 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
515 'protocol_version'=> 1.1
519 $old_error = error_get_last();
521 $data = @file_get_contents
($url, false, $context);
523 if (isset($http_response_header) && is_array($http_response_header)) {
524 foreach ($http_response_header as $header) {
525 if (strstr($header, ": ") !== FALSE) {
526 list ($key, $value) = explode(": ", $header);
528 $key = strtolower($key);
530 if ($key == 'content-type') {
531 $fetch_last_content_type = $value;
532 // don't abort here b/c there might be more than one
533 // e.g. if we were being redirected -- last one is the right one
534 } else if ($key == 'last-modified') {
535 $fetch_last_modified = $value;
539 if (substr(strtolower($header), 0, 7) == 'http/1.') {
540 $fetch_last_error_code = (int) substr($header, 9, 3);
541 $fetch_last_error = $header;
546 if ($fetch_last_error_code != 200) {
547 $error = error_get_last();
549 if ($error['message'] != $old_error['message']) {
550 $fetch_last_error .= "; " . $error["message"];
553 $fetch_last_error_content = $data;
563 * Try to determine the favicon URL for a feed.
564 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
565 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
567 * @param string $url A feed or page URL
569 * @return mixed The favicon URL, or false if none was found.
571 function get_favicon_url($url) {
573 $favicon_url = false;
575 if ($html = @fetch_file_contents
($url)) {
577 libxml_use_internal_errors(true);
579 $doc = new DOMDocument();
580 $doc->loadHTML($html);
581 $xpath = new DOMXPath($doc);
583 $base = $xpath->query('/html/head/base[@href]');
584 foreach ($base as $b) {
585 $url = rewrite_relative_url($url, $b->getAttribute("href"));
589 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
590 if (count($entries) > 0) {
591 foreach ($entries as $entry) {
592 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
599 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
602 } // function get_favicon_url
604 function initialize_user_prefs($uid, $profile = false) {
606 $uid = db_escape_string($uid);
610 $profile_qpart = "AND profile IS NULL";
612 $profile_qpart = "AND profile = '$profile'";
615 if (get_schema_version() < 63) $profile_qpart = "";
619 $result = db_query("SELECT pref_name,def_value FROM ttrss_prefs");
621 $u_result = db_query("SELECT pref_name
622 FROM ttrss_user_prefs WHERE owner_uid = '$uid' $profile_qpart");
624 $active_prefs = array();
626 while ($line = db_fetch_assoc($u_result)) {
627 array_push($active_prefs, $line["pref_name"]);
630 while ($line = db_fetch_assoc($result)) {
631 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
632 // print "adding " . $line["pref_name"] . "<br>";
634 $line["def_value"] = db_escape_string($line["def_value"]);
635 $line["pref_name"] = db_escape_string($line["pref_name"]);
637 if (get_schema_version() < 63) {
638 db_query("INSERT INTO ttrss_user_prefs
639 (owner_uid,pref_name,value) VALUES
640 ('$uid', '".$line["pref_name"]."','".$line["def_value"]."')");
643 db_query("INSERT INTO ttrss_user_prefs
644 (owner_uid,pref_name,value, profile) VALUES
645 ('$uid', '".$line["pref_name"]."','".$line["def_value"]."', $profile)");
655 function get_ssl_certificate_id() {
656 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
657 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
658 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
659 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
660 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
662 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
663 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
664 $_SERVER["SSL_CLIENT_V_START"] .
665 $_SERVER["SSL_CLIENT_V_END"] .
666 $_SERVER["SSL_CLIENT_S_DN"]);
671 function authenticate_user($login, $password, $check_only = false) {
673 if (!SINGLE_USER_MODE
) {
676 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
678 $user_id = (int) $plugin->authenticate($login, $password);
681 $_SESSION["auth_module"] = strtolower(get_class($plugin));
686 if ($user_id && !$check_only) {
689 $_SESSION["uid"] = $user_id;
690 $_SESSION["version"] = VERSION_STATIC
;
692 $result = db_query("SELECT login,access_level,pwd_hash FROM ttrss_users
693 WHERE id = '$user_id'");
695 $_SESSION["name"] = db_fetch_result($result, 0, "login");
696 $_SESSION["access_level"] = db_fetch_result($result, 0, "access_level");
697 $_SESSION["csrf_token"] = uniqid_short();
699 db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
702 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
703 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
704 $_SESSION["pwd_hash"] = db_fetch_result($result, 0, "pwd_hash");
706 $_SESSION["last_version_check"] = time();
708 initialize_user_prefs($_SESSION["uid"]);
717 $_SESSION["uid"] = 1;
718 $_SESSION["name"] = "admin";
719 $_SESSION["access_level"] = 10;
721 $_SESSION["hide_hello"] = true;
722 $_SESSION["hide_logout"] = true;
724 $_SESSION["auth_module"] = false;
726 if (!$_SESSION["csrf_token"]) {
727 $_SESSION["csrf_token"] = uniqid_short();
730 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
732 initialize_user_prefs($_SESSION["uid"]);
738 function make_password($length = 8) {
741 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
745 while ($i < $length) {
746 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
748 if (!strstr($password, $char)) {
756 // this is called after user is created to initialize default feeds, labels
759 // user preferences are checked on every login, not here
761 function initialize_user($uid) {
763 db_query("insert into ttrss_feeds (owner_uid,title,feed_url)
764 values ('$uid', 'Tiny Tiny RSS: Forum',
765 'http://tt-rss.org/forum/rss.php')");
768 function logout_user() {
770 if (isset($_COOKIE[session_name()])) {
771 setcookie(session_name(), '', time()-42000, '/');
775 function validate_csrf($csrf_token) {
776 return $csrf_token == $_SESSION['csrf_token'];
779 function load_user_plugins($owner_uid, $pluginhost = false) {
781 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
783 if ($owner_uid && SCHEMA_VERSION
>= 100) {
784 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
786 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
788 if (get_schema_version() > 100) {
789 $pluginhost->load_data();
794 function login_sequence() {
795 if (SINGLE_USER_MODE
) {
797 authenticate_user("admin", null);
799 load_user_plugins($_SESSION["uid"]);
801 if (!validate_session()) $_SESSION["uid"] = false;
803 if (!$_SESSION["uid"]) {
805 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
806 $_SESSION["ref_schema_version"] = get_schema_version(true);
808 authenticate_user(null, null, true);
811 if (!$_SESSION["uid"]) {
813 setcookie(session_name(), '', time()-42000, '/');
820 /* bump login timestamp */
821 db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
823 $_SESSION["last_login_update"] = time();
826 if ($_SESSION["uid"]) {
828 load_user_plugins($_SESSION["uid"]);
832 db_query("DELETE FROM ttrss_counters_cache WHERE owner_uid = ".
833 $_SESSION["uid"] . " AND
834 (SELECT COUNT(id) FROM ttrss_feeds WHERE
835 ttrss_feeds.id = feed_id) = 0");
837 db_query("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ".
838 $_SESSION["uid"] . " AND
839 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
840 ttrss_feed_categories.id = feed_id) = 0");
847 function truncate_string($str, $max_len, $suffix = '…') {
848 if (mb_strlen($str, "utf-8") > $max_len) {
849 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
856 function truncate_middle($str, $max_len, $suffix = '…') {
857 if (strlen($str) > $max_len) {
858 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
864 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
867 $source_tz = new DateTimeZone($source_tz);
868 } catch (Exception
$e) {
869 $source_tz = new DateTimeZone('UTC');
873 $dest_tz = new DateTimeZone($dest_tz);
874 } catch (Exception
$e) {
875 $dest_tz = new DateTimeZone('UTC');
878 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
879 return $dt->format('U') +
$dest_tz->getOffset($dt);
882 function make_local_datetime($timestamp, $long, $owner_uid = false,
883 $no_smart_dt = false, $eta_min = false) {
885 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
886 if (!$timestamp) $timestamp = '1970-01-01 0:00';
891 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
893 $timestamp = substr($timestamp, 0, 19);
895 # We store date in UTC internally
896 $dt = new DateTime($timestamp, $utc_tz);
898 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
900 if ($user_tz_string != 'Automatic') {
903 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
904 } catch (Exception
$e) {
908 $tz_offset = $user_tz->getOffset($dt);
910 $tz_offset = (int) -$_SESSION["clientTzOffset"];
913 $user_timestamp = $dt->format('U') +
$tz_offset;
916 return smart_date_time($user_timestamp,
917 $tz_offset, $owner_uid, $eta_min);
920 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
922 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
924 return date($format, $user_timestamp);
928 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
929 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
931 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
932 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
933 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
934 return date("G:i", $timestamp);
935 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
936 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
937 return date($format, $timestamp);
939 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
940 return date($format, $timestamp);
944 function sql_bool_to_bool($s) {
945 if ($s == "t" ||
$s == "1" ||
strtolower($s) == "true") {
952 function bool_to_sql_bool($s) {
960 // Session caching removed due to causing wrong redirects to upgrade
961 // script when get_schema_version() is called on an obsolete session
962 // created on a previous schema version.
963 function get_schema_version($nocache = false) {
964 global $schema_version;
966 if (!$schema_version && !$nocache) {
967 $result = db_query("SELECT schema_version FROM ttrss_version");
968 $version = db_fetch_result($result, 0, "schema_version");
969 $schema_version = $version;
972 return $schema_version;
976 function sanity_check() {
977 require_once 'errors.php';
981 $schema_version = get_schema_version(true);
983 if ($schema_version != SCHEMA_VERSION
) {
987 if (DB_TYPE
== "mysql") {
988 $result = db_query("SELECT true", false);
989 if (db_num_rows($result) != 1) {
994 if (db_escape_string("testTEST") != "testTEST") {
998 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
1001 function file_is_locked($filename) {
1002 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
1003 if (function_exists('flock')) {
1004 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1006 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1007 flock($fp, LOCK_UN
);
1017 return true; // consider the file always locked and skip the test
1024 function make_lockfile($filename) {
1025 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1027 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1028 $stat_h = fstat($fp);
1029 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1031 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1032 if ($stat_h["ino"] != $stat_f["ino"] ||
1033 $stat_h["dev"] != $stat_f["dev"]) {
1039 if (function_exists('posix_getpid')) {
1040 fwrite($fp, posix_getpid() . "\n");
1048 function make_stampfile($filename) {
1049 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1051 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1052 fwrite($fp, time() . "\n");
1053 flock($fp, LOCK_UN
);
1061 function sql_random_function() {
1062 if (DB_TYPE
== "mysql") {
1069 function getFeedUnread($feed, $is_cat = false) {
1070 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1074 /*function get_pgsql_version() {
1075 $result = db_query("SELECT version() AS version");
1076 $version = explode(" ", db_fetch_result($result, 0, "version"));
1080 function checkbox_to_sql_bool($val) {
1081 return ($val == "on") ?
"true" : "false";
1084 /*function getFeedCatTitle($id) {
1086 return __("Special");
1087 } else if ($id < LABEL_BASE_INDEX) {
1088 return __("Labels");
1089 } else if ($id > 0) {
1090 $result = db_query("SELECT ttrss_feed_categories.title
1091 FROM ttrss_feeds, ttrss_feed_categories WHERE ttrss_feeds.id = '$id' AND
1092 cat_id = ttrss_feed_categories.id");
1093 if (db_num_rows($result) == 1) {
1094 return db_fetch_result($result, 0, "title");
1096 return __("Uncategorized");
1099 return "getFeedCatTitle($id) failed";
1104 function uniqid_short() {
1105 return uniqid(base_convert(rand(), 10, 36));
1108 function make_init_params() {
1111 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1112 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1113 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1114 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1116 $params[strtolower($param)] = (int) get_pref($param);
1119 $params["icons_url"] = ICONS_URL
;
1120 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1121 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1122 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1123 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1124 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1125 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1127 $theme = get_pref( "USER_CSS_THEME", false, false);
1128 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1130 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1132 $params["php_platform"] = PHP_OS
;
1133 $params["php_version"] = PHP_VERSION
;
1135 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1137 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1138 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
1140 $max_feed_id = db_fetch_result($result, 0, "mid");
1141 $num_feeds = db_fetch_result($result, 0, "nf");
1143 $params["max_feed_id"] = (int) $max_feed_id;
1144 $params["num_feeds"] = (int) $num_feeds;
1146 $params["hotkeys"] = get_hotkeys_map();
1148 $params["csrf_token"] = $_SESSION["csrf_token"];
1149 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1151 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1153 $params["icon_alert"] = base64_img("images/alert.png");
1154 $params["icon_information"] = base64_img("images/information.png");
1155 $params["icon_cross"] = base64_img("images/cross.png");
1156 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1158 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1163 function get_hotkeys_info() {
1165 __("Navigation") => array(
1166 "next_feed" => __("Open next feed"),
1167 "prev_feed" => __("Open previous feed"),
1168 "next_article" => __("Open next article"),
1169 "prev_article" => __("Open previous article"),
1170 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1171 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1172 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1173 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1174 "search_dialog" => __("Show search dialog")),
1175 __("Article") => array(
1176 "toggle_mark" => __("Toggle starred"),
1177 "toggle_publ" => __("Toggle published"),
1178 "toggle_unread" => __("Toggle unread"),
1179 "edit_tags" => __("Edit tags"),
1180 "open_in_new_window" => __("Open in new window"),
1181 "catchup_below" => __("Mark below as read"),
1182 "catchup_above" => __("Mark above as read"),
1183 "article_scroll_down" => __("Scroll down"),
1184 "article_scroll_up" => __("Scroll up"),
1185 "select_article_cursor" => __("Select article under cursor"),
1186 "email_article" => __("Email article"),
1187 "close_article" => __("Close/collapse article"),
1188 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1189 "toggle_widescreen" => __("Toggle widescreen mode"),
1190 "toggle_embed_original" => __("Toggle embed original")),
1191 __("Article selection") => array(
1192 "select_all" => __("Select all articles"),
1193 "select_unread" => __("Select unread"),
1194 "select_marked" => __("Select starred"),
1195 "select_published" => __("Select published"),
1196 "select_invert" => __("Invert selection"),
1197 "select_none" => __("Deselect everything")),
1198 __("Feed") => array(
1199 "feed_refresh" => __("Refresh current feed"),
1200 "feed_unhide_read" => __("Un/hide read feeds"),
1201 "feed_subscribe" => __("Subscribe to feed"),
1202 "feed_edit" => __("Edit feed"),
1203 "feed_catchup" => __("Mark as read"),
1204 "feed_reverse" => __("Reverse headlines"),
1205 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1206 "feed_debug_update" => __("Debug feed update"),
1207 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1208 "catchup_all" => __("Mark all feeds as read"),
1209 "cat_toggle_collapse" => __("Un/collapse current category"),
1210 "toggle_combined_mode" => __("Toggle combined mode"),
1211 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1212 __("Go to") => array(
1213 "goto_all" => __("All articles"),
1214 "goto_fresh" => __("Fresh"),
1215 "goto_marked" => __("Starred"),
1216 "goto_published" => __("Published"),
1217 "goto_tagcloud" => __("Tag cloud"),
1218 "goto_prefs" => __("Preferences")),
1219 __("Other") => array(
1220 "create_label" => __("Create label"),
1221 "create_filter" => __("Create filter"),
1222 "collapse_sidebar" => __("Un/collapse sidebar"),
1223 "help_dialog" => __("Show help dialog"))
1226 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1227 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1233 function get_hotkeys_map() {
1235 // "navigation" => array(
1238 "n" => "next_article",
1239 "p" => "prev_article",
1240 "(38)|up" => "prev_article",
1241 "(40)|down" => "next_article",
1242 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1243 // "^(40)|Ctrl-down" => "next_article_noscroll",
1244 "(191)|/" => "search_dialog",
1245 // "article" => array(
1246 "s" => "toggle_mark",
1247 "*s" => "toggle_publ",
1248 "u" => "toggle_unread",
1249 "*t" => "edit_tags",
1250 "o" => "open_in_new_window",
1251 "c p" => "catchup_below",
1252 "c n" => "catchup_above",
1253 "*n" => "article_scroll_down",
1254 "*p" => "article_scroll_up",
1255 "*(38)|Shift+up" => "article_scroll_up",
1256 "*(40)|Shift+down" => "article_scroll_down",
1257 "a *w" => "toggle_widescreen",
1258 "a e" => "toggle_embed_original",
1259 "e" => "email_article",
1260 "a q" => "close_article",
1261 // "article_selection" => array(
1262 "a a" => "select_all",
1263 "a u" => "select_unread",
1264 "a *u" => "select_marked",
1265 "a p" => "select_published",
1266 "a i" => "select_invert",
1267 "a n" => "select_none",
1269 "f r" => "feed_refresh",
1270 "f a" => "feed_unhide_read",
1271 "f s" => "feed_subscribe",
1272 "f e" => "feed_edit",
1273 "f q" => "feed_catchup",
1274 "f x" => "feed_reverse",
1275 "f g" => "feed_toggle_vgroup",
1276 "f *d" => "feed_debug_update",
1277 "f *g" => "feed_debug_viewfeed",
1278 "f *c" => "toggle_combined_mode",
1279 "f c" => "toggle_cdm_expanded",
1280 "*q" => "catchup_all",
1281 "x" => "cat_toggle_collapse",
1283 "g a" => "goto_all",
1284 "g f" => "goto_fresh",
1285 "g s" => "goto_marked",
1286 "g p" => "goto_published",
1287 "g t" => "goto_tagcloud",
1288 "g *p" => "goto_prefs",
1289 // "other" => array(
1290 "(9)|Tab" => "select_article_cursor", // tab
1291 "c l" => "create_label",
1292 "c f" => "create_filter",
1293 "c s" => "collapse_sidebar",
1294 "^(191)|Ctrl+/" => "help_dialog",
1297 if (get_pref('COMBINED_DISPLAY_MODE')) {
1298 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1299 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1302 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1303 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1306 $prefixes = array();
1308 foreach (array_keys($hotkeys) as $hotkey) {
1309 $pair = explode(" ", $hotkey, 2);
1311 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1312 array_push($prefixes, $pair[0]);
1316 return array($prefixes, $hotkeys);
1319 function check_for_update() {
1320 if (defined("GIT_VERSION_TIMESTAMP")) {
1321 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1324 $content = json_decode($content, true);
1326 if ($content && isset($content["changeset"])) {
1327 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1328 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1330 return $content["changeset"]["id"];
1339 function make_runtime_info($disable_update_check = false) {
1342 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1343 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
1345 $max_feed_id = db_fetch_result($result, 0, "mid");
1346 $num_feeds = db_fetch_result($result, 0, "nf");
1348 $data["max_feed_id"] = (int) $max_feed_id;
1349 $data["num_feeds"] = (int) $num_feeds;
1351 $data['last_article_id'] = Article
::getLastArticleId();
1352 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1354 $data['dep_ts'] = calculate_dep_timestamp();
1355 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1357 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1359 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1360 $update_result = @check_for_update
();
1362 $data["update_result"] = $update_result;
1364 $_SESSION["last_version_check"] = time();
1367 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1369 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1371 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1373 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1376 $stamp_delta = time() - $stamp;
1378 if ($stamp_delta > 1800) {
1382 $_SESSION["daemon_stamp_check"] = time();
1385 $data['daemon_stamp_ok'] = $stamp_check;
1387 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1389 $data['daemon_stamp'] = $stamp_fmt;
1397 function search_to_sql($search, $search_language) {
1399 $keywords = str_getcsv(trim($search), " ");
1400 $query_keywords = array();
1401 $search_words = array();
1402 $search_query_leftover = array();
1404 if ($search_language)
1405 $search_language = db_escape_string(mb_strtolower($search_language));
1407 $search_language = "english";
1409 foreach ($keywords as $k) {
1410 if (strpos($k, "-") === 0) {
1417 $commandpair = explode(":", mb_strtolower($k), 2);
1419 switch ($commandpair[0]) {
1421 if ($commandpair[1]) {
1422 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
1423 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1425 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1426 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1427 array_push($search_words, $k);
1431 if ($commandpair[1]) {
1432 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
1433 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1435 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1436 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1437 array_push($search_words, $k);
1441 if ($commandpair[1]) {
1442 if ($commandpair[1] == "true")
1443 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1444 else if ($commandpair[1] == "false")
1445 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1447 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
1448 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1450 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1451 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1452 if (!$not) array_push($search_words, $k);
1457 if ($commandpair[1]) {
1458 if ($commandpair[1] == "true")
1459 array_push($query_keywords, "($not (marked = true))");
1461 array_push($query_keywords, "($not (marked = false))");
1463 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1464 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1465 if (!$not) array_push($search_words, $k);
1469 if ($commandpair[1]) {
1470 if ($commandpair[1] == "true")
1471 array_push($query_keywords, "($not (published = true))");
1473 array_push($query_keywords, "($not (published = false))");
1476 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1477 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1478 if (!$not) array_push($search_words, $k);
1482 if ($commandpair[1]) {
1483 if ($commandpair[1] == "true")
1484 array_push($query_keywords, "($not (unread = true))");
1486 array_push($query_keywords, "($not (unread = false))");
1489 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1490 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1491 if (!$not) array_push($search_words, $k);
1495 if (strpos($k, "@") === 0) {
1497 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1498 $orig_ts = strtotime(substr($k, 1));
1499 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1501 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1503 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1506 if (DB_TYPE
== "pgsql") {
1507 $k = mb_strtolower($k);
1508 array_push($search_query_leftover, $not ?
"!$k" : $k);
1510 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1511 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1514 if (!$not) array_push($search_words, $k);
1519 if (count($search_query_leftover) > 0) {
1520 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
1522 if (DB_TYPE
== "pgsql") {
1523 array_push($query_keywords,
1524 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
1529 $search_query_part = implode("AND", $query_keywords);
1531 return array($search_query_part, $search_words);
1534 function iframe_whitelisted($entry) {
1535 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1537 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1540 foreach ($whitelist as $w) {
1541 if ($src == $w ||
$src == "www.$w")
1549 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1550 if (!$owner) $owner = $_SESSION["uid"];
1552 $res = trim($str); if (!$res) return '';
1554 $charset_hack = '<head>
1555 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1558 $res = trim($res); if (!$res) return '';
1560 libxml_use_internal_errors(true);
1562 $doc = new DOMDocument();
1563 $doc->loadHTML($charset_hack . $res);
1564 $xpath = new DOMXPath($doc);
1566 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1568 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1570 foreach ($entries as $entry) {
1572 if ($entry->hasAttribute('href')) {
1573 $entry->setAttribute('href',
1574 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1576 $entry->setAttribute('rel', 'noopener noreferrer');
1579 if ($entry->hasAttribute('src')) {
1580 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1581 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1583 if (file_exists($cached_filename)) {
1585 // this is strictly cosmetic
1586 if ($entry->tagName
== 'img') {
1588 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1590 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1596 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1598 if ($entry->hasAttribute('srcset')) {
1599 $entry->removeAttribute('srcset');
1602 if ($entry->hasAttribute('sizes')) {
1603 $entry->removeAttribute('sizes');
1607 $entry->setAttribute('src', $src);
1610 if ($entry->nodeName
== 'img') {
1612 if ($entry->hasAttribute('src')) {
1613 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1615 if (is_prefix_https() && !$is_https_url) {
1617 if ($entry->hasAttribute('srcset')) {
1618 $entry->removeAttribute('srcset');
1621 if ($entry->hasAttribute('sizes')) {
1622 $entry->removeAttribute('sizes');
1627 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1628 $force_remove_images ||
$_SESSION["bw_limit"]) {
1630 $p = $doc->createElement('p');
1632 $a = $doc->createElement('a');
1633 $a->setAttribute('href', $entry->getAttribute('src'));
1635 $a->appendChild(new DOMText($entry->getAttribute('src')));
1636 $a->setAttribute('target', '_blank');
1637 $a->setAttribute('rel', 'noopener noreferrer');
1639 $p->appendChild($a);
1641 $entry->parentNode
->replaceChild($p, $entry);
1645 if (strtolower($entry->nodeName
) == "a") {
1646 $entry->setAttribute("target", "_blank");
1647 $entry->setAttribute("rel", "noopener noreferrer");
1651 $entries = $xpath->query('//iframe');
1652 foreach ($entries as $entry) {
1653 if (!iframe_whitelisted($entry)) {
1654 $entry->setAttribute('sandbox', 'allow-scripts');
1656 if (is_prefix_https()) {
1657 $entry->setAttribute("src",
1658 str_replace("http://", "https://",
1659 $entry->getAttribute("src")));
1664 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1665 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1666 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1667 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1668 'dt', 'em', 'footer', 'figure', 'figcaption',
1669 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1670 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1671 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1672 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1673 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1674 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1676 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1678 $disallowed_attributes = array('id', 'style', 'class');
1680 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1681 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1682 if (is_array($retval)) {
1684 $allowed_elements = $retval[1];
1685 $disallowed_attributes = $retval[2];
1691 $doc->removeChild($doc->firstChild
); //remove doctype
1692 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1694 if ($highlight_words) {
1695 foreach ($highlight_words as $word) {
1697 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1699 $elements = $xpath->query("//*/text()");
1701 foreach ($elements as $child) {
1703 $fragment = $doc->createDocumentFragment();
1704 $text = $child->textContent
;
1706 while (($pos = mb_stripos($text, $word)) !== false) {
1707 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1708 $word = mb_substr($text, $pos, mb_strlen($word));
1709 $highlight = $doc->createElement('span');
1710 $highlight->appendChild(new DomText($word));
1711 $highlight->setAttribute('class', 'highlight');
1712 $fragment->appendChild($highlight);
1713 $text = mb_substr($text, $pos +
mb_strlen($word));
1716 if (!empty($text)) $fragment->appendChild(new DomText($text));
1718 $child->parentNode
->replaceChild($fragment, $child);
1723 $res = $doc->saveHTML();
1725 /* strip everything outside of <body>...</body> */
1727 $res_frag = array();
1728 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1729 return $res_frag[1];
1735 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1736 $xpath = new DOMXPath($doc);
1737 $entries = $xpath->query('//*');
1739 foreach ($entries as $entry) {
1740 if (!in_array($entry->nodeName
, $allowed_elements)) {
1741 $entry->parentNode
->removeChild($entry);
1744 if ($entry->hasAttributes()) {
1745 $attrs_to_remove = array();
1747 foreach ($entry->attributes
as $attr) {
1749 if (strpos($attr->nodeName
, 'on') === 0) {
1750 array_push($attrs_to_remove, $attr);
1753 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1754 array_push($attrs_to_remove, $attr);
1757 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1758 array_push($attrs_to_remove, $attr);
1762 foreach ($attrs_to_remove as $attr) {
1763 $entry->removeAttributeNode($attr);
1771 function trim_array($array) {
1773 array_walk($tmp, 'trim');
1777 function tag_is_valid($tag) {
1778 if ($tag == '') return false;
1779 if (is_numeric($tag)) return false;
1780 if (mb_strlen($tag) > 250) return false;
1782 if (!$tag) return false;
1787 function render_login_form() {
1788 header('Cache-Control: public');
1790 require_once "login_form.php";
1794 function T_sprintf() {
1795 $args = func_get_args();
1796 return vsprintf(__(array_shift($args)), $args);
1799 function print_checkpoint($n, $s) {
1800 $ts = microtime(true);
1801 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1805 function sanitize_tag($tag) {
1808 $tag = mb_strtolower($tag, 'utf-8');
1810 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1812 if (DB_TYPE
== "mysql") {
1813 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1819 function is_server_https() {
1820 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1823 function is_prefix_https() {
1824 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1827 // this returns SELF_URL_PATH sans ending slash
1828 function get_self_url_prefix() {
1829 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1830 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1832 return SELF_URL_PATH
;
1836 function encrypt_password($pass, $salt = '', $mode2 = false) {
1837 if ($salt && $mode2) {
1838 return "MODE2:" . hash('sha256', $salt . $pass);
1840 return "SHA1X:" . sha1("$salt:$pass");
1842 return "SHA1:" . sha1($pass);
1844 } // function encrypt_password
1846 function load_filters($feed_id, $owner_uid) {
1849 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1852 $null_cat_qpart = "cat_id IS NULL OR";
1854 $null_cat_qpart = "";
1856 $result = db_query("SELECT * FROM ttrss_filters2 WHERE
1857 owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title");
1859 $check_cats = array_merge(
1860 Feeds
::getParentCategories($cat_id, $owner_uid),
1863 $check_cats_str = join(",", $check_cats);
1864 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1866 while ($line = db_fetch_assoc($result)) {
1867 $filter_id = $line["id"];
1869 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1871 $result2 = db_query("SELECT
1872 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1873 FROM ttrss_filters2_rules AS r,
1874 ttrss_filter_types AS t
1876 (match_on IS NOT NULL OR
1877 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1878 (feed_id IS NULL OR feed_id = '$feed_id'))) AND
1879 filter_type = t.id AND filter_id = '$filter_id'");
1884 while ($rule_line = db_fetch_assoc($result2)) {
1885 # print_r($rule_line);
1887 if ($rule_line["match_on"]) {
1888 $match_on = json_decode($rule_line["match_on"], true);
1890 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1893 $rule["reg_exp"] = $rule_line["reg_exp"];
1894 $rule["type"] = $rule_line["type_name"];
1895 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1897 array_push($rules, $rule);
1898 } else if (!$match_any_rule) {
1899 // this filter contains a rule that doesn't match to this feed/category combination
1900 // thus filter has to be rejected
1909 $rule["reg_exp"] = $rule_line["reg_exp"];
1910 $rule["type"] = $rule_line["type_name"];
1911 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1913 array_push($rules, $rule);
1917 if (count($rules) > 0) {
1918 $result2 = db_query("SELECT a.action_param,t.name AS type_name
1919 FROM ttrss_filters2_actions AS a,
1920 ttrss_filter_actions AS t
1922 action_id = t.id AND filter_id = '$filter_id'");
1924 while ($action_line = db_fetch_assoc($result2)) {
1925 # print_r($action_line);
1928 $action["type"] = $action_line["type_name"];
1929 $action["param"] = $action_line["action_param"];
1931 array_push($actions, $action);
1936 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1937 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1938 $filter["rules"] = $rules;
1939 $filter["actions"] = $actions;
1941 if (count($rules) > 0 && count($actions) > 0) {
1942 array_push($filters, $filter);
1949 function get_score_pic($score) {
1951 return "score_high.png";
1952 } else if ($score > 0) {
1953 return "score_half_high.png";
1954 } else if ($score < -100) {
1955 return "score_low.png";
1956 } else if ($score < 0) {
1957 return "score_half_low.png";
1959 return "score_neutral.png";
1963 function feed_has_icon($id) {
1964 return is_file(ICONS_DIR
. "/$id.ico") && filesize(ICONS_DIR
. "/$id.ico") > 0;
1967 function init_plugins() {
1968 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1973 function add_feed_category($feed_cat, $parent_cat_id = false) {
1975 if (!$feed_cat) return false;
1979 if ($parent_cat_id) {
1980 $parent_qpart = "parent_cat = '$parent_cat_id'";
1981 $parent_insert = "'$parent_cat_id'";
1983 $parent_qpart = "parent_cat IS NULL";
1984 $parent_insert = "NULL";
1987 $feed_cat = mb_substr($feed_cat, 0, 250);
1990 "SELECT id FROM ttrss_feed_categories
1991 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1993 if (db_num_rows($result) == 0) {
1996 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1997 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
2008 * Fixes incomplete URLs by prepending "http://".
2009 * Also replaces feed:// with http://, and
2010 * prepends a trailing slash if the url is a domain name only.
2012 * @param string $url Possibly incomplete URL
2014 * @return string Fixed URL.
2016 function fix_url($url) {
2018 // support schema-less urls
2019 if (strpos($url, '//') === 0) {
2020 $url = 'https:' . $url;
2023 if (strpos($url, '://') === false) {
2024 $url = 'http://' . $url;
2025 } else if (substr($url, 0, 5) == 'feed:') {
2026 $url = 'http:' . substr($url, 5);
2029 //prepend slash if the URL has no slash in it
2030 // "http://www.example" -> "http://www.example/"
2031 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2035 //convert IDNA hostname to punycode if possible
2036 if (function_exists("idn_to_ascii")) {
2037 $parts = parse_url($url);
2038 if (mb_detect_encoding($parts['host']) != 'ASCII')
2040 $parts['host'] = idn_to_ascii($parts['host']);
2041 $url = build_url($parts);
2045 if ($url != "http:///")
2051 function validate_feed_url($url) {
2052 $parts = parse_url($url);
2054 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2058 /* function save_email_address($email) {
2059 // FIXME: implement persistent storage of emails
2061 if (!$_SESSION['stored_emails'])
2062 $_SESSION['stored_emails'] = array();
2064 if (!in_array($email, $_SESSION['stored_emails']))
2065 array_push($_SESSION['stored_emails'], $email);
2069 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2071 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2073 $sql_is_cat = bool_to_sql_bool($is_cat);
2075 $result = db_query("SELECT access_key FROM ttrss_access_keys
2076 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
2077 AND owner_uid = " . $owner_uid);
2079 if (db_num_rows($result) == 1) {
2080 return db_fetch_result($result, 0, "access_key");
2082 $key = db_escape_string(uniqid_short());
2084 $result = db_query("INSERT INTO ttrss_access_keys
2085 (access_key, feed_id, is_cat, owner_uid)
2086 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
2093 function get_feeds_from_html($url, $content)
2095 $url = fix_url($url);
2096 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2098 libxml_use_internal_errors(true);
2100 $doc = new DOMDocument();
2101 $doc->loadHTML($content);
2102 $xpath = new DOMXPath($doc);
2103 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2104 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2105 $feedUrls = array();
2106 foreach ($entries as $entry) {
2107 if ($entry->hasAttribute('href')) {
2108 $title = $entry->getAttribute('title');
2110 $title = $entry->getAttribute('type');
2112 $feedUrl = rewrite_relative_url(
2113 $baseUrl, $entry->getAttribute('href')
2115 $feedUrls[$feedUrl] = $title;
2121 function is_html($content) {
2122 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2125 function url_is_html($url, $login = false, $pass = false) {
2126 return is_html(fetch_file_contents($url, false, $login, $pass));
2129 function build_url($parts) {
2130 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2133 function cleanup_url_path($path) {
2134 $path = str_replace("/./", "/", $path);
2135 $path = str_replace("//", "/", $path);
2141 * Converts a (possibly) relative URL to a absolute one.
2143 * @param string $url Base URL (i.e. from where the document is)
2144 * @param string $rel_url Possibly relative URL in the document
2146 * @return string Absolute URL
2148 function rewrite_relative_url($url, $rel_url) {
2149 if (strpos($rel_url, "://") !== false) {
2151 } else if (strpos($rel_url, "//") === 0) {
2152 # protocol-relative URL (rare but they exist)
2154 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2155 # magnet:, feed:, etc
2157 } else if (strpos($rel_url, "/") === 0) {
2158 $parts = parse_url($url);
2159 $parts['path'] = $rel_url;
2160 $parts['path'] = cleanup_url_path($parts['path']);
2162 return build_url($parts);
2165 $parts = parse_url($url);
2166 if (!isset($parts['path'])) {
2167 $parts['path'] = '/';
2169 $dir = $parts['path'];
2170 if (substr($dir, -1) !== '/') {
2171 $dir = dirname($parts['path']);
2172 $dir !== '/' && $dir .= '/';
2174 $parts['path'] = $dir . $rel_url;
2175 $parts['path'] = cleanup_url_path($parts['path']);
2177 return build_url($parts);
2181 function cleanup_tags($days = 14, $limit = 1000) {
2183 if (DB_TYPE
== "pgsql") {
2184 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2185 } else if (DB_TYPE
== "mysql") {
2186 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2191 while ($limit > 0) {
2194 $query = "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 $limit_part";
2199 $result = db_query($query);
2203 while ($line = db_fetch_assoc($result)) {
2204 array_push($ids, $line['id']);
2207 if (count($ids) > 0) {
2208 $ids = join(",", $ids);
2210 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2211 $tags_deleted +
= db_affected_rows($tmp_result);
2216 $limit -= $limit_part;
2219 return $tags_deleted;
2222 function print_user_stylesheet() {
2223 $value = get_pref('USER_STYLESHEET');
2226 print "<style type=\"text/css\">";
2227 print str_replace("<br/>", "\n", $value);
2233 function filter_to_sql($filter, $owner_uid) {
2236 if (DB_TYPE
== "pgsql")
2239 $reg_qpart = "REGEXP";
2241 foreach ($filter["rules"] AS $rule) {
2242 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2243 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2244 $rule['reg_exp']) !== FALSE;
2246 if ($regexp_valid) {
2248 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2250 switch ($rule["type"]) {
2252 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2253 $rule['reg_exp'] . "')";
2256 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2257 $rule['reg_exp'] . "')";
2260 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2261 $rule['reg_exp'] . "') OR LOWER(" .
2262 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2265 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2266 $rule['reg_exp'] . "')";
2269 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2270 $rule['reg_exp'] . "')";
2273 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2274 $rule['reg_exp'] . "')";
2278 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2280 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2281 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2284 if (isset($rule["cat_id"])) {
2286 if ($rule["cat_id"] > 0) {
2287 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2288 array_push($children, $rule["cat_id"]);
2290 $children = join(",", $children);
2292 $cat_qpart = "cat_id IN ($children)";
2294 $cat_qpart = "cat_id IS NULL";
2297 $qpart .= " AND $cat_qpart";
2300 $qpart .= " AND feed_id IS NOT NULL";
2302 array_push($query, "($qpart)");
2307 if (count($query) > 0) {
2308 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2310 $fullquery = "(false)";
2313 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2318 if (!function_exists('gzdecode')) {
2319 function gzdecode($string) { // no support for 2nd argument
2320 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2321 base64_encode($string));
2325 function get_random_bytes($length) {
2326 if (function_exists('openssl_random_pseudo_bytes')) {
2327 return openssl_random_pseudo_bytes($length);
2331 for ($i = 0; $i < $length; $i++
)
2332 $output .= chr(mt_rand(0, 255));
2338 function read_stdin() {
2339 $fp = fopen("php://stdin", "r");
2342 $line = trim(fgets($fp));
2350 function implements_interface($class, $interface) {
2351 return in_array($interface, class_implements($class));
2354 function get_minified_js($files) {
2355 require_once 'lib/jshrink/Minifier.php';
2359 foreach ($files as $js) {
2360 if (!isset($_GET['debug'])) {
2361 $cached_file = CACHE_DIR
. "/js/".basename($js).".js";
2363 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2365 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2367 if ($header && $contents) {
2368 list($htag, $hversion) = explode(":", $header);
2370 if ($htag == "tt-rss" && $hversion == VERSION
) {
2377 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js.js"));
2378 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2382 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2389 function calculate_dep_timestamp() {
2390 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2394 foreach ($files as $file) {
2395 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2401 function T_js_decl($s1, $s2) {
2403 $s1 = preg_replace("/\n/", "", $s1);
2404 $s2 = preg_replace("/\n/", "", $s2);
2406 $s1 = preg_replace("/\"/", "\\\"", $s1);
2407 $s2 = preg_replace("/\"/", "\\\"", $s2);
2409 return "T_messages[\"$s1\"] = \"$s2\";\n";
2413 function init_js_translations() {
2415 print 'var T_messages = new Object();
2418 if (T_messages[msg]) {
2419 return T_messages[msg];
2425 function ngettext(msg1, msg2, n) {
2426 return __((parseInt(n) > 1) ? msg2 : msg1);
2429 $l10n = _get_reader();
2431 for ($i = 0; $i < $l10n->total
; $i++
) {
2432 $orig = $l10n->get_original_string($i);
2433 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2434 $key = explode(chr(0), $orig);
2435 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2436 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2438 $translation = __($orig);
2439 print T_js_decl($orig, $translation);
2444 function get_theme_path($theme) {
2445 $check = "themes/$theme";
2446 if (file_exists($check)) return $check;
2448 $check = "themes.local/$theme";
2449 if (file_exists($check)) return $check;
2452 function theme_valid($theme) {
2453 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2455 if (in_array($theme, $bundled_themes)) return true;
2457 $file = "themes/" . basename($theme);
2459 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2461 if (file_exists($file) && is_readable($file)) {
2462 $fh = fopen($file, "r");
2465 $header = fgets($fh);
2468 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2476 * @SuppressWarnings(unused)
2478 function error_json($code) {
2479 require_once "errors.php";
2481 @$message = $ERRORS[$code];
2483 return json_encode(array("error" =>
2484 array("code" => $code, "message" => $message)));
2488 /*function abs_to_rel_path($dir) {
2489 $tmp = str_replace(dirname(__DIR__), "", $dir);
2491 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2496 function get_upload_error_message($code) {
2499 0 => __('There is no error, the file uploaded with success'),
2500 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2501 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2502 3 => __('The uploaded file was only partially uploaded'),
2503 4 => __('No file was uploaded'),
2504 6 => __('Missing a temporary folder'),
2505 7 => __('Failed to write file to disk.'),
2506 8 => __('A PHP extension stopped the file upload.'),
2509 return $errors[$code];
2512 function base64_img($filename) {
2513 if (file_exists($filename)) {
2514 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2516 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2522 /* this is essentially a wrapper for readfile() which allows plugins to hook
2523 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2525 hook function should return true if request was handled (or at least attempted to)
2527 note that this can be called without user context so the plugin to handle this
2528 should be loaded systemwide in config.php */
2529 function send_local_file($filename) {
2530 if (file_exists($filename)) {
2531 $tmppluginhost = new PluginHost();
2533 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2534 $tmppluginhost->load_data();
2536 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2537 if ($plugin->hook_send_local_file($filename)) return true;
2540 $mimetype = mime_content_type($filename);
2541 header("Content-type: $mimetype");
2543 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2544 header("Last-Modified: $stamp", true);
2546 return readfile($filename);
2552 function check_mysql_tables() {
2553 $schema = db_escape_string(DB_NAME
);
2555 $result = db_query("SELECT engine, table_name FROM information_schema.tables WHERE
2556 table_schema = '$schema' AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2560 while ($line = db_fetch_assoc($result)) {
2561 array_push($bad_tables, $line);