2 define('EXPECTED_CONFIG_VERSION', 26);
3 define('SCHEMA_VERSION', 132);
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
61 /* tunables end here */
63 if (DB_TYPE
== "pgsql") {
64 define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
66 define('SUBSTRING_FOR_DATE', 'SUBSTRING');
70 * Return available translations names.
73 * @return array A array of available translations.
75 function get_translations() {
77 "auto" => "Detect automatically",
78 "ar_SA" => "العربيّة (Arabic)",
79 "bg_BG" => "Bulgarian",
84 "el_GR" => "Ελληνικά",
85 "es_ES" => "Español (España)",
88 "fr_FR" => "Français",
89 "hu_HU" => "Magyar (Hungarian)",
90 "it_IT" => "Italiano",
91 "ja_JP" => "日本語 (Japanese)",
92 "lv_LV" => "Latviešu",
93 "nb_NO" => "Norwegian bokmål",
97 "pt_BR" => "Portuguese/Brazil",
98 "pt_PT" => "Portuguese/Portugal",
99 "zh_CN" => "Simplified Chinese",
100 "zh_TW" => "Traditional Chinese",
101 "sv_SE" => "Svenska",
103 "tr_TR" => "Türkçe");
108 require_once "lib/accept-to-gettext.php";
109 require_once "lib/gettext/gettext.inc";
111 function startup_gettext() {
113 # Get locale from Accept-Language header
114 $lang = al2gt(array_keys(get_translations()), "text/html");
116 if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
117 $lang = _TRANSLATION_OVERRIDE_DEFAULT
;
120 if ($_SESSION["uid"] && get_schema_version() >= 120) {
121 $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
123 if ($pref_lang && $pref_lang != 'auto') {
129 if (defined('LC_MESSAGES')) {
130 _setlocale(LC_MESSAGES
, $lang);
131 } else if (defined('LC_ALL')) {
132 _setlocale(LC_ALL
, $lang);
135 _bindtextdomain("messages", "locale");
137 _textdomain("messages");
138 _bind_textdomain_codeset("messages", "UTF-8");
142 require_once 'db-prefs.php';
143 require_once 'version.php';
144 require_once 'controls.php';
146 define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION
. ' (http://tt-rss.org/)');
147 ini_set('user_agent', SELF_USER_AGENT
);
149 $schema_version = false;
151 function _debug_suppress($suppress) {
152 global $suppress_debugging;
154 $suppress_debugging = $suppress;
158 * Print a timestamped debug message.
160 * @param string $msg The debug message.
163 function _debug($msg, $show = true) {
164 global $suppress_debugging;
166 //echo "[$suppress_debugging] $msg $show\n";
168 if ($suppress_debugging) return false;
170 $ts = strftime("%H:%M:%S", time());
171 if (function_exists('posix_getpid')) {
172 $ts = "$ts/" . posix_getpid();
175 if ($show && !(defined('QUIET') && QUIET
)) {
176 print "[$ts] $msg\n";
179 if (defined('LOGFILE')) {
180 $fp = fopen(LOGFILE
, 'a+');
185 if (function_exists("flock")) {
188 // try to lock logfile for writing
189 while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB
)) {
200 fputs($fp, "[$ts] $msg\n");
202 if (function_exists("flock")) {
213 * Purge a feed old posts.
215 * @param mixed $link A database connection.
216 * @param mixed $feed_id The id of the purged feed.
217 * @param mixed $purge_interval Olderness of purged posts.
218 * @param boolean $debug Set to True to enable the debug. False by default.
222 function purge_feed($feed_id, $purge_interval, $debug = false) {
224 if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
228 $sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
229 $sth->execute($feed_id);
233 if ($row = $sth->fetch()) {
234 $owner_uid = $row["owner_uid"];
237 if ($purge_interval == -1 ||
!$purge_interval) {
239 CCache
::update($feed_id, $owner_uid);
244 if (!$owner_uid) return;
246 if (FORCE_ARTICLE_PURGE
== 0) {
247 $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
250 $purge_unread = true;
251 $purge_interval = FORCE_ARTICLE_PURGE
;
255 $query_limit = " unread = false AND ";
259 if (DB_TYPE
== "pgsql") {
260 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
262 WHERE ttrss_entries.id = ref_id AND
266 ttrss_entries.date_updated < NOW() - INTERVAL ?");
267 $sth->execute([$feed_id, "$purge_interval days"]);
270 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
271 USING ttrss_user_entries, ttrss_entries
272 WHERE ttrss_entries.id = ref_id AND
276 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL ? DAY)");
277 $sth->execute([$feed_id, $purge_interval]);
281 $rows = $sth->rowCount();
283 CCache
::update($feed_id, $owner_uid);
286 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
290 } // function purge_feed
292 function feed_purge_interval($feed_id) {
296 $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
298 $sth->execute([$feed_id]);
300 if ($row = $sth->fetch()) {
301 $purge_interval = $row["purge_interval"];
302 $owner_uid = $row["owner_uid"];
304 if ($purge_interval == 0) $purge_interval = get_pref(
305 'PURGE_OLD_DAYS', $owner_uid);
307 return $purge_interval;
314 // TODO: multiple-argument way is deprecated, first parameter is a hash now
315 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
316 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
318 global $fetch_last_error;
319 global $fetch_last_error_code;
320 global $fetch_last_error_content;
321 global $fetch_last_content_type;
322 global $fetch_last_modified;
323 global $fetch_curl_used;
325 $fetch_last_error = false;
326 $fetch_last_error_code = -1;
327 $fetch_last_error_content = "";
328 $fetch_last_content_type = "";
329 $fetch_curl_used = false;
330 $fetch_last_modified = "";
332 if (!is_array($options)) {
334 // falling back on compatibility shim
335 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
338 for ($i = 0; $i < func_num_args(); $i++
) {
339 $tmp[$option_names[$i]] = func_get_arg($i);
345 "url" => func_get_arg(0),
346 "type" => @func_get_arg(1),
347 "login" => @func_get_arg(2),
348 "pass" => @func_get_arg(3),
349 "post_query" => @func_get_arg(4),
350 "timeout" => @func_get_arg(5),
351 "timestamp" => @func_get_arg(6),
352 "useragent" => @func_get_arg(7)
356 $url = $options["url"];
357 $type = isset($options["type"]) ?
$options["type"] : false;
358 $login = isset($options["login"]) ?
$options["login"] : false;
359 $pass = isset($options["pass"]) ?
$options["pass"] : false;
360 $post_query = isset($options["post_query"]) ?
$options["post_query"] : false;
361 $timeout = isset($options["timeout"]) ?
$options["timeout"] : false;
362 $last_modified = isset($options["last_modified"]) ?
$options["last_modified"] : "";
363 $useragent = isset($options["useragent"]) ?
$options["useragent"] : false;
364 $followlocation = isset($options["followlocation"]) ?
$options["followlocation"] : true;
366 $url = ltrim($url, ' ');
367 $url = str_replace(' ', '%20', $url);
369 if (strpos($url, "//") === 0)
370 $url = 'http:' . $url;
372 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
374 $fetch_curl_used = true;
376 $ch = curl_init($url);
378 if ($last_modified && !$post_query) {
379 curl_setopt($ch, CURLOPT_HTTPHEADER
,
380 array("If-Modified-Since: $last_modified"));
383 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
384 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
385 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
386 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
387 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
388 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
389 curl_setopt($ch, CURLOPT_HEADER
, true);
390 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
391 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
393 curl_setopt($ch, CURLOPT_ENCODING
, "");
394 //curl_setopt($ch, CURLOPT_REFERER, $url);
396 if (!ini_get("open_basedir")) {
397 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
400 if (defined('_CURL_HTTP_PROXY')) {
401 curl_setopt($ch, CURLOPT_PROXY
, _CURL_HTTP_PROXY
);
405 curl_setopt($ch, CURLOPT_POST
, true);
406 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
410 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
412 $ret = @curl_exec
($ch);
414 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE
);
415 $headers = explode("\r\n", substr($ret, 0, $headers_length));
416 $contents = substr($ret, $headers_length);
418 foreach ($headers as $header) {
419 if (strstr($header, ": ") !== FALSE) {
420 list ($key, $value) = explode(": ", $header);
422 if (strtolower($key) == "last-modified") {
423 $fetch_last_modified = $value;
427 if (substr(strtolower($header), 0, 7) == 'http/1.') {
428 $fetch_last_error_code = (int) substr($header, 9, 3);
429 $fetch_last_error = $header;
433 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
434 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
435 $contents = @curl_exec
($ch);
438 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
439 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
441 $fetch_last_error_code = $http_code;
443 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
445 if (curl_errno($ch) != 0) {
446 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
449 $fetch_last_error_content = $contents;
455 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
465 $fetch_curl_used = false;
467 if ($login && $pass){
468 $url_parts = array();
470 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
472 $pass = urlencode($pass);
474 if ($url_parts[1] && $url_parts[2]) {
475 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
479 // TODO: should this support POST requests or not? idk
481 if (!$post_query && $last_modified) {
482 $context = stream_context_create(array(
485 'ignore_errors' => true,
486 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
487 'protocol_version'=> 1.1,
488 'header' => "If-Modified-Since: $last_modified\r\n")
491 $context = stream_context_create(array(
494 'ignore_errors' => true,
495 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
496 'protocol_version'=> 1.1
500 $old_error = error_get_last();
502 $data = @file_get_contents
($url, false, $context);
504 if (isset($http_response_header) && is_array($http_response_header)) {
505 foreach ($http_response_header as $header) {
506 if (strstr($header, ": ") !== FALSE) {
507 list ($key, $value) = explode(": ", $header);
509 $key = strtolower($key);
511 if ($key == 'content-type') {
512 $fetch_last_content_type = $value;
513 // don't abort here b/c there might be more than one
514 // e.g. if we were being redirected -- last one is the right one
515 } else if ($key == 'last-modified') {
516 $fetch_last_modified = $value;
520 if (substr(strtolower($header), 0, 7) == 'http/1.') {
521 $fetch_last_error_code = (int) substr($header, 9, 3);
522 $fetch_last_error = $header;
527 if ($fetch_last_error_code != 200) {
528 $error = error_get_last();
530 if ($error['message'] != $old_error['message']) {
531 $fetch_last_error .= "; " . $error["message"];
534 $fetch_last_error_content = $data;
544 * Try to determine the favicon URL for a feed.
545 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
546 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
548 * @param string $url A feed or page URL
550 * @return mixed The favicon URL, or false if none was found.
552 function get_favicon_url($url) {
554 $favicon_url = false;
556 if ($html = @fetch_file_contents
($url)) {
558 libxml_use_internal_errors(true);
560 $doc = new DOMDocument();
561 $doc->loadHTML($html);
562 $xpath = new DOMXPath($doc);
564 $base = $xpath->query('/html/head/base[@href]');
565 foreach ($base as $b) {
566 $url = rewrite_relative_url($url, $b->getAttribute("href"));
570 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
571 if (count($entries) > 0) {
572 foreach ($entries as $entry) {
573 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
580 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
583 } // function get_favicon_url
585 function initialize_user_prefs($uid, $profile = false) {
587 $uid = db_escape_string($uid);
589 if (get_schema_version() < 63) $profile_qpart = "";
591 ////db_query("BEGIN");
595 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
597 $profile = $profile ?
$profile : null;
599 $u_sth = $pdo->prepare("SELECT pref_name
600 FROM ttrss_user_prefs WHERE owner_uid = ? AND profile = ?");
601 $u_sth->execute([$uid, $profile]);
603 $active_prefs = array();
605 while ($line = $u_sth->fetch()) {
606 array_push($active_prefs, $line["pref_name"]);
609 while ($line = $sth->fetch()) {
610 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
611 // print "adding " . $line["pref_name"] . "<br>";
613 $line["def_value"] = db_escape_string($line["def_value"]);
614 $line["pref_name"] = db_escape_string($line["pref_name"]);
616 if (get_schema_version() < 63) {
617 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
618 (owner_uid,pref_name,value) VALUES
620 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
623 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
624 (owner_uid,pref_name,value, profile) VALUES
626 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
632 ////db_query("COMMIT");
636 function get_ssl_certificate_id() {
637 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
638 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
639 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
640 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
641 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
643 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
644 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
645 $_SERVER["SSL_CLIENT_V_START"] .
646 $_SERVER["SSL_CLIENT_V_END"] .
647 $_SERVER["SSL_CLIENT_S_DN"]);
652 function authenticate_user($login, $password, $check_only = false) {
654 if (!SINGLE_USER_MODE
) {
657 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
659 $user_id = (int) $plugin->authenticate($login, $password);
662 $_SESSION["auth_module"] = strtolower(get_class($plugin));
667 if ($user_id && !$check_only) {
670 $_SESSION["uid"] = $user_id;
671 $_SESSION["version"] = VERSION_STATIC
;
674 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
676 $sth->execute([$user_id]);
677 $row = $sth->fetch();
679 $_SESSION["name"] = $row["login"];
680 $_SESSION["access_level"] = $row["access_level"];
681 $_SESSION["csrf_token"] = uniqid_short();
683 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
684 $usth->execute([$user_id]);
686 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
687 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
688 $_SESSION["pwd_hash"] = $row["pwd_hash"];
690 $_SESSION["last_version_check"] = time();
692 initialize_user_prefs($_SESSION["uid"]);
701 $_SESSION["uid"] = 1;
702 $_SESSION["name"] = "admin";
703 $_SESSION["access_level"] = 10;
705 $_SESSION["hide_hello"] = true;
706 $_SESSION["hide_logout"] = true;
708 $_SESSION["auth_module"] = false;
710 if (!$_SESSION["csrf_token"]) {
711 $_SESSION["csrf_token"] = uniqid_short();
714 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
716 initialize_user_prefs($_SESSION["uid"]);
722 function make_password($length = 8) {
725 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
729 while ($i < $length) {
730 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
732 if (!strstr($password, $char)) {
740 // this is called after user is created to initialize default feeds, labels
743 // user preferences are checked on every login, not here
745 function initialize_user($uid) {
749 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
750 values (?, 'Tiny Tiny RSS: Forum',
751 'http://tt-rss.org/forum/rss.php')");
752 $sth->execute([$uid]);
755 function logout_user() {
757 if (isset($_COOKIE[session_name()])) {
758 setcookie(session_name(), '', time()-42000, '/');
762 function validate_csrf($csrf_token) {
763 return $csrf_token == $_SESSION['csrf_token'];
766 function load_user_plugins($owner_uid, $pluginhost = false) {
768 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
770 if ($owner_uid && SCHEMA_VERSION
>= 100) {
771 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
773 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
775 if (get_schema_version() > 100) {
776 $pluginhost->load_data();
781 function login_sequence() {
784 if (SINGLE_USER_MODE
) {
786 authenticate_user("admin", null);
788 load_user_plugins($_SESSION["uid"]);
790 if (!validate_session()) $_SESSION["uid"] = false;
792 if (!$_SESSION["uid"]) {
794 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
795 $_SESSION["ref_schema_version"] = get_schema_version(true);
797 authenticate_user(null, null, true);
800 if (!$_SESSION["uid"]) {
802 setcookie(session_name(), '', time()-42000, '/');
809 /* bump login timestamp */
810 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
811 $sth->execute([$_SESSION['uid']]);
813 $_SESSION["last_login_update"] = time();
816 if ($_SESSION["uid"]) {
818 load_user_plugins($_SESSION["uid"]);
822 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
824 (SELECT COUNT(id) FROM ttrss_feeds WHERE
825 ttrss_feeds.id = feed_id) = 0");
827 $sth->execute($_SESSION['uid']);
829 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
831 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
832 ttrss_feed_categories.id = feed_id) = 0");
834 $sth->execute($_SESSION['uid']);
840 function truncate_string($str, $max_len, $suffix = '…') {
841 if (mb_strlen($str, "utf-8") > $max_len) {
842 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
849 function truncate_middle($str, $max_len, $suffix = '…') {
850 if (strlen($str) > $max_len) {
851 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
857 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
860 $source_tz = new DateTimeZone($source_tz);
861 } catch (Exception
$e) {
862 $source_tz = new DateTimeZone('UTC');
866 $dest_tz = new DateTimeZone($dest_tz);
867 } catch (Exception
$e) {
868 $dest_tz = new DateTimeZone('UTC');
871 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
872 return $dt->format('U') +
$dest_tz->getOffset($dt);
875 function make_local_datetime($timestamp, $long, $owner_uid = false,
876 $no_smart_dt = false, $eta_min = false) {
878 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
879 if (!$timestamp) $timestamp = '1970-01-01 0:00';
884 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
886 $timestamp = substr($timestamp, 0, 19);
888 # We store date in UTC internally
889 $dt = new DateTime($timestamp, $utc_tz);
891 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
893 if ($user_tz_string != 'Automatic') {
896 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
897 } catch (Exception
$e) {
901 $tz_offset = $user_tz->getOffset($dt);
903 $tz_offset = (int) -$_SESSION["clientTzOffset"];
906 $user_timestamp = $dt->format('U') +
$tz_offset;
909 return smart_date_time($user_timestamp,
910 $tz_offset, $owner_uid, $eta_min);
913 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
915 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
917 return date($format, $user_timestamp);
921 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
922 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
924 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
925 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
926 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
927 return date("G:i", $timestamp);
928 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
929 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
930 return date($format, $timestamp);
932 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
933 return date($format, $timestamp);
937 function sql_bool_to_bool($s) {
938 if ($s == "t" ||
$s == "1" ||
strtolower($s) == "true") {
945 function bool_to_sql_bool($s) {
953 // Session caching removed due to causing wrong redirects to upgrade
954 // script when get_schema_version() is called on an obsolete session
955 // created on a previous schema version.
956 function get_schema_version($nocache = false) {
957 global $schema_version;
961 if (!$schema_version && !$nocache) {
962 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
963 $version = $row["schema_version"];
964 $schema_version = $version;
967 return $schema_version;
971 function sanity_check() {
972 require_once 'errors.php';
976 $schema_version = get_schema_version(true);
978 if ($schema_version != SCHEMA_VERSION
) {
982 if (db_escape_string("testTEST") != "testTEST") {
986 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
989 function file_is_locked($filename) {
990 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
991 if (function_exists('flock')) {
992 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
994 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1005 return true; // consider the file always locked and skip the test
1012 function make_lockfile($filename) {
1013 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1015 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1016 $stat_h = fstat($fp);
1017 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1019 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1020 if ($stat_h["ino"] != $stat_f["ino"] ||
1021 $stat_h["dev"] != $stat_f["dev"]) {
1027 if (function_exists('posix_getpid')) {
1028 fwrite($fp, posix_getpid() . "\n");
1036 function make_stampfile($filename) {
1037 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1039 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1040 fwrite($fp, time() . "\n");
1041 flock($fp, LOCK_UN
);
1049 function sql_random_function() {
1050 if (DB_TYPE
== "mysql") {
1057 function getFeedUnread($feed, $is_cat = false) {
1058 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1061 function checkbox_to_sql_bool($val) {
1062 return ($val == "on") ?
"true" : "false";
1065 function uniqid_short() {
1066 return uniqid(base_convert(rand(), 10, 36));
1069 function make_init_params() {
1072 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1073 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1074 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1075 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1077 $params[strtolower($param)] = (int) get_pref($param);
1080 $params["icons_url"] = ICONS_URL
;
1081 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1082 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1083 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1084 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1085 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1086 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1088 $theme = get_pref( "USER_CSS_THEME", false, false);
1089 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1091 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1093 $params["php_platform"] = PHP_OS
;
1094 $params["php_version"] = PHP_VERSION
;
1096 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1100 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1101 ttrss_feeds WHERE owner_uid = ?");
1102 $sth->execute([$_SESSION['uid']]);
1103 $row = $sth->fetch();
1105 $max_feed_id = $row["mid"];
1106 $num_feeds = $row["nf"];
1108 $params["max_feed_id"] = (int) $max_feed_id;
1109 $params["num_feeds"] = (int) $num_feeds;
1111 $params["hotkeys"] = get_hotkeys_map();
1113 $params["csrf_token"] = $_SESSION["csrf_token"];
1114 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1116 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1118 $params["icon_alert"] = base64_img("images/alert.png");
1119 $params["icon_information"] = base64_img("images/information.png");
1120 $params["icon_cross"] = base64_img("images/cross.png");
1121 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1123 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1128 function get_hotkeys_info() {
1130 __("Navigation") => array(
1131 "next_feed" => __("Open next feed"),
1132 "prev_feed" => __("Open previous feed"),
1133 "next_article" => __("Open next article"),
1134 "prev_article" => __("Open previous article"),
1135 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1136 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1137 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1138 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1139 "search_dialog" => __("Show search dialog")),
1140 __("Article") => array(
1141 "toggle_mark" => __("Toggle starred"),
1142 "toggle_publ" => __("Toggle published"),
1143 "toggle_unread" => __("Toggle unread"),
1144 "edit_tags" => __("Edit tags"),
1145 "open_in_new_window" => __("Open in new window"),
1146 "catchup_below" => __("Mark below as read"),
1147 "catchup_above" => __("Mark above as read"),
1148 "article_scroll_down" => __("Scroll down"),
1149 "article_scroll_up" => __("Scroll up"),
1150 "select_article_cursor" => __("Select article under cursor"),
1151 "email_article" => __("Email article"),
1152 "close_article" => __("Close/collapse article"),
1153 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1154 "toggle_widescreen" => __("Toggle widescreen mode"),
1155 "toggle_embed_original" => __("Toggle embed original")),
1156 __("Article selection") => array(
1157 "select_all" => __("Select all articles"),
1158 "select_unread" => __("Select unread"),
1159 "select_marked" => __("Select starred"),
1160 "select_published" => __("Select published"),
1161 "select_invert" => __("Invert selection"),
1162 "select_none" => __("Deselect everything")),
1163 __("Feed") => array(
1164 "feed_refresh" => __("Refresh current feed"),
1165 "feed_unhide_read" => __("Un/hide read feeds"),
1166 "feed_subscribe" => __("Subscribe to feed"),
1167 "feed_edit" => __("Edit feed"),
1168 "feed_catchup" => __("Mark as read"),
1169 "feed_reverse" => __("Reverse headlines"),
1170 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1171 "feed_debug_update" => __("Debug feed update"),
1172 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1173 "catchup_all" => __("Mark all feeds as read"),
1174 "cat_toggle_collapse" => __("Un/collapse current category"),
1175 "toggle_combined_mode" => __("Toggle combined mode"),
1176 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1177 __("Go to") => array(
1178 "goto_all" => __("All articles"),
1179 "goto_fresh" => __("Fresh"),
1180 "goto_marked" => __("Starred"),
1181 "goto_published" => __("Published"),
1182 "goto_tagcloud" => __("Tag cloud"),
1183 "goto_prefs" => __("Preferences")),
1184 __("Other") => array(
1185 "create_label" => __("Create label"),
1186 "create_filter" => __("Create filter"),
1187 "collapse_sidebar" => __("Un/collapse sidebar"),
1188 "help_dialog" => __("Show help dialog"))
1191 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1192 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1198 function get_hotkeys_map() {
1200 // "navigation" => array(
1203 "n" => "next_article",
1204 "p" => "prev_article",
1205 "(38)|up" => "prev_article",
1206 "(40)|down" => "next_article",
1207 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1208 // "^(40)|Ctrl-down" => "next_article_noscroll",
1209 "(191)|/" => "search_dialog",
1210 // "article" => array(
1211 "s" => "toggle_mark",
1212 "*s" => "toggle_publ",
1213 "u" => "toggle_unread",
1214 "*t" => "edit_tags",
1215 "o" => "open_in_new_window",
1216 "c p" => "catchup_below",
1217 "c n" => "catchup_above",
1218 "*n" => "article_scroll_down",
1219 "*p" => "article_scroll_up",
1220 "*(38)|Shift+up" => "article_scroll_up",
1221 "*(40)|Shift+down" => "article_scroll_down",
1222 "a *w" => "toggle_widescreen",
1223 "a e" => "toggle_embed_original",
1224 "e" => "email_article",
1225 "a q" => "close_article",
1226 // "article_selection" => array(
1227 "a a" => "select_all",
1228 "a u" => "select_unread",
1229 "a *u" => "select_marked",
1230 "a p" => "select_published",
1231 "a i" => "select_invert",
1232 "a n" => "select_none",
1234 "f r" => "feed_refresh",
1235 "f a" => "feed_unhide_read",
1236 "f s" => "feed_subscribe",
1237 "f e" => "feed_edit",
1238 "f q" => "feed_catchup",
1239 "f x" => "feed_reverse",
1240 "f g" => "feed_toggle_vgroup",
1241 "f *d" => "feed_debug_update",
1242 "f *g" => "feed_debug_viewfeed",
1243 "f *c" => "toggle_combined_mode",
1244 "f c" => "toggle_cdm_expanded",
1245 "*q" => "catchup_all",
1246 "x" => "cat_toggle_collapse",
1248 "g a" => "goto_all",
1249 "g f" => "goto_fresh",
1250 "g s" => "goto_marked",
1251 "g p" => "goto_published",
1252 "g t" => "goto_tagcloud",
1253 "g *p" => "goto_prefs",
1254 // "other" => array(
1255 "(9)|Tab" => "select_article_cursor", // tab
1256 "c l" => "create_label",
1257 "c f" => "create_filter",
1258 "c s" => "collapse_sidebar",
1259 "^(191)|Ctrl+/" => "help_dialog",
1262 if (get_pref('COMBINED_DISPLAY_MODE')) {
1263 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1264 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1267 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1268 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1271 $prefixes = array();
1273 foreach (array_keys($hotkeys) as $hotkey) {
1274 $pair = explode(" ", $hotkey, 2);
1276 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1277 array_push($prefixes, $pair[0]);
1281 return array($prefixes, $hotkeys);
1284 function check_for_update() {
1285 if (defined("GIT_VERSION_TIMESTAMP")) {
1286 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1289 $content = json_decode($content, true);
1291 if ($content && isset($content["changeset"])) {
1292 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1293 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1295 return $content["changeset"]["id"];
1304 function make_runtime_info($disable_update_check = false) {
1309 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1310 ttrss_feeds WHERE owner_uid = ?");
1311 $sth->execute([$_SESSION['uid']]);
1312 $row = $sth->fetch();
1314 $max_feed_id = $row['mid'];
1315 $num_feeds = $row['nf'];
1317 $data["max_feed_id"] = (int) $max_feed_id;
1318 $data["num_feeds"] = (int) $num_feeds;
1320 $data['last_article_id'] = Article
::getLastArticleId();
1321 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1323 $data['dep_ts'] = calculate_dep_timestamp();
1324 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1326 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1328 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1329 $update_result = @check_for_update
();
1331 $data["update_result"] = $update_result;
1333 $_SESSION["last_version_check"] = time();
1336 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1338 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1340 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1342 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1345 $stamp_delta = time() - $stamp;
1347 if ($stamp_delta > 1800) {
1351 $_SESSION["daemon_stamp_check"] = time();
1354 $data['daemon_stamp_ok'] = $stamp_check;
1356 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1358 $data['daemon_stamp'] = $stamp_fmt;
1366 function search_to_sql($search, $search_language) {
1368 $keywords = str_getcsv(trim($search), " ");
1369 $query_keywords = array();
1370 $search_words = array();
1371 $search_query_leftover = array();
1373 if ($search_language)
1374 $search_language = db_escape_string(mb_strtolower($search_language));
1376 $search_language = "english";
1378 foreach ($keywords as $k) {
1379 if (strpos($k, "-") === 0) {
1386 $commandpair = explode(":", mb_strtolower($k), 2);
1388 switch ($commandpair[0]) {
1390 if ($commandpair[1]) {
1391 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
1392 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1394 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1395 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1396 array_push($search_words, $k);
1400 if ($commandpair[1]) {
1401 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
1402 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1404 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1405 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1406 array_push($search_words, $k);
1410 if ($commandpair[1]) {
1411 if ($commandpair[1] == "true")
1412 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1413 else if ($commandpair[1] == "false")
1414 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1416 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
1417 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1419 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1420 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1421 if (!$not) array_push($search_words, $k);
1426 if ($commandpair[1]) {
1427 if ($commandpair[1] == "true")
1428 array_push($query_keywords, "($not (marked = true))");
1430 array_push($query_keywords, "($not (marked = false))");
1432 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1433 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1434 if (!$not) array_push($search_words, $k);
1438 if ($commandpair[1]) {
1439 if ($commandpair[1] == "true")
1440 array_push($query_keywords, "($not (published = true))");
1442 array_push($query_keywords, "($not (published = false))");
1445 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1446 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1447 if (!$not) array_push($search_words, $k);
1451 if ($commandpair[1]) {
1452 if ($commandpair[1] == "true")
1453 array_push($query_keywords, "($not (unread = true))");
1455 array_push($query_keywords, "($not (unread = false))");
1458 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1459 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1460 if (!$not) array_push($search_words, $k);
1464 if (strpos($k, "@") === 0) {
1466 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1467 $orig_ts = strtotime(substr($k, 1));
1468 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1470 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1472 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1475 if (DB_TYPE
== "pgsql") {
1476 $k = mb_strtolower($k);
1477 array_push($search_query_leftover, $not ?
"!$k" : $k);
1479 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1480 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1483 if (!$not) array_push($search_words, $k);
1488 if (count($search_query_leftover) > 0) {
1489 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
1491 if (DB_TYPE
== "pgsql") {
1492 array_push($query_keywords,
1493 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
1498 $search_query_part = implode("AND", $query_keywords);
1500 return array($search_query_part, $search_words);
1503 function iframe_whitelisted($entry) {
1504 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1506 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1509 foreach ($whitelist as $w) {
1510 if ($src == $w ||
$src == "www.$w")
1518 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1519 if (!$owner) $owner = $_SESSION["uid"];
1521 $res = trim($str); if (!$res) return '';
1523 $charset_hack = '<head>
1524 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1527 $res = trim($res); if (!$res) return '';
1529 libxml_use_internal_errors(true);
1531 $doc = new DOMDocument();
1532 $doc->loadHTML($charset_hack . $res);
1533 $xpath = new DOMXPath($doc);
1535 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1537 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1539 foreach ($entries as $entry) {
1541 if ($entry->hasAttribute('href')) {
1542 $entry->setAttribute('href',
1543 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1545 $entry->setAttribute('rel', 'noopener noreferrer');
1548 if ($entry->hasAttribute('src')) {
1549 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1550 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1552 if (file_exists($cached_filename)) {
1554 // this is strictly cosmetic
1555 if ($entry->tagName
== 'img') {
1557 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1559 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1565 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1567 if ($entry->hasAttribute('srcset')) {
1568 $entry->removeAttribute('srcset');
1571 if ($entry->hasAttribute('sizes')) {
1572 $entry->removeAttribute('sizes');
1576 $entry->setAttribute('src', $src);
1579 if ($entry->nodeName
== 'img') {
1581 if ($entry->hasAttribute('src')) {
1582 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1584 if (is_prefix_https() && !$is_https_url) {
1586 if ($entry->hasAttribute('srcset')) {
1587 $entry->removeAttribute('srcset');
1590 if ($entry->hasAttribute('sizes')) {
1591 $entry->removeAttribute('sizes');
1596 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1597 $force_remove_images ||
$_SESSION["bw_limit"]) {
1599 $p = $doc->createElement('p');
1601 $a = $doc->createElement('a');
1602 $a->setAttribute('href', $entry->getAttribute('src'));
1604 $a->appendChild(new DOMText($entry->getAttribute('src')));
1605 $a->setAttribute('target', '_blank');
1606 $a->setAttribute('rel', 'noopener noreferrer');
1608 $p->appendChild($a);
1610 $entry->parentNode
->replaceChild($p, $entry);
1614 if (strtolower($entry->nodeName
) == "a") {
1615 $entry->setAttribute("target", "_blank");
1616 $entry->setAttribute("rel", "noopener noreferrer");
1620 $entries = $xpath->query('//iframe');
1621 foreach ($entries as $entry) {
1622 if (!iframe_whitelisted($entry)) {
1623 $entry->setAttribute('sandbox', 'allow-scripts');
1625 if (is_prefix_https()) {
1626 $entry->setAttribute("src",
1627 str_replace("http://", "https://",
1628 $entry->getAttribute("src")));
1633 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1634 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1635 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1636 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1637 'dt', 'em', 'footer', 'figure', 'figcaption',
1638 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1639 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1640 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1641 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1642 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1643 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1645 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1647 $disallowed_attributes = array('id', 'style', 'class');
1649 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1650 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1651 if (is_array($retval)) {
1653 $allowed_elements = $retval[1];
1654 $disallowed_attributes = $retval[2];
1660 $doc->removeChild($doc->firstChild
); //remove doctype
1661 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1663 if ($highlight_words) {
1664 foreach ($highlight_words as $word) {
1666 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1668 $elements = $xpath->query("//*/text()");
1670 foreach ($elements as $child) {
1672 $fragment = $doc->createDocumentFragment();
1673 $text = $child->textContent
;
1675 while (($pos = mb_stripos($text, $word)) !== false) {
1676 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1677 $word = mb_substr($text, $pos, mb_strlen($word));
1678 $highlight = $doc->createElement('span');
1679 $highlight->appendChild(new DomText($word));
1680 $highlight->setAttribute('class', 'highlight');
1681 $fragment->appendChild($highlight);
1682 $text = mb_substr($text, $pos +
mb_strlen($word));
1685 if (!empty($text)) $fragment->appendChild(new DomText($text));
1687 $child->parentNode
->replaceChild($fragment, $child);
1692 $res = $doc->saveHTML();
1694 /* strip everything outside of <body>...</body> */
1696 $res_frag = array();
1697 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1698 return $res_frag[1];
1704 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1705 $xpath = new DOMXPath($doc);
1706 $entries = $xpath->query('//*');
1708 foreach ($entries as $entry) {
1709 if (!in_array($entry->nodeName
, $allowed_elements)) {
1710 $entry->parentNode
->removeChild($entry);
1713 if ($entry->hasAttributes()) {
1714 $attrs_to_remove = array();
1716 foreach ($entry->attributes
as $attr) {
1718 if (strpos($attr->nodeName
, 'on') === 0) {
1719 array_push($attrs_to_remove, $attr);
1722 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1723 array_push($attrs_to_remove, $attr);
1726 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1727 array_push($attrs_to_remove, $attr);
1731 foreach ($attrs_to_remove as $attr) {
1732 $entry->removeAttributeNode($attr);
1740 function trim_array($array) {
1742 array_walk($tmp, 'trim');
1746 function tag_is_valid($tag) {
1747 if ($tag == '') return false;
1748 if (is_numeric($tag)) return false;
1749 if (mb_strlen($tag) > 250) return false;
1751 if (!$tag) return false;
1756 function render_login_form() {
1757 header('Cache-Control: public');
1759 require_once "login_form.php";
1763 function T_sprintf() {
1764 $args = func_get_args();
1765 return vsprintf(__(array_shift($args)), $args);
1768 function print_checkpoint($n, $s) {
1769 $ts = microtime(true);
1770 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1774 function sanitize_tag($tag) {
1777 $tag = mb_strtolower($tag, 'utf-8');
1779 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1781 if (DB_TYPE
== "mysql") {
1782 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1788 function is_server_https() {
1789 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1792 function is_prefix_https() {
1793 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1796 // this returns SELF_URL_PATH sans ending slash
1797 function get_self_url_prefix() {
1798 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1799 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1801 return SELF_URL_PATH
;
1805 function encrypt_password($pass, $salt = '', $mode2 = false) {
1806 if ($salt && $mode2) {
1807 return "MODE2:" . hash('sha256', $salt . $pass);
1809 return "SHA1X:" . sha1("$salt:$pass");
1811 return "SHA1:" . sha1($pass);
1813 } // function encrypt_password
1815 function load_filters($feed_id, $owner_uid) {
1818 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1821 $null_cat_qpart = "cat_id IS NULL OR";
1823 $null_cat_qpart = "";
1827 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1828 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1829 $sth->execute([$owner_uid]);
1831 $check_cats = array_merge(
1832 Feeds
::getParentCategories($cat_id, $owner_uid),
1835 $check_cats_str = join(",", $check_cats);
1836 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1838 while ($line = $sth->fetch()) {
1839 $filter_id = $line["id"];
1841 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1843 $sth2 = $pdo->prepare("SELECT
1844 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1845 FROM ttrss_filters2_rules AS r,
1846 ttrss_filter_types AS t
1848 (match_on IS NOT NULL OR
1849 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1850 (feed_id IS NULL OR feed_id = ?))) AND
1851 filter_type = t.id AND filter_id = ?");
1852 $sth2->execute([$feed_id, $filter_id]);
1857 while ($rule_line = $sth2->fetch()) {
1858 # print_r($rule_line);
1860 if ($rule_line["match_on"]) {
1861 $match_on = json_decode($rule_line["match_on"], true);
1863 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1866 $rule["reg_exp"] = $rule_line["reg_exp"];
1867 $rule["type"] = $rule_line["type_name"];
1868 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1870 array_push($rules, $rule);
1871 } else if (!$match_any_rule) {
1872 // this filter contains a rule that doesn't match to this feed/category combination
1873 // thus filter has to be rejected
1882 $rule["reg_exp"] = $rule_line["reg_exp"];
1883 $rule["type"] = $rule_line["type_name"];
1884 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1886 array_push($rules, $rule);
1890 if (count($rules) > 0) {
1891 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1892 FROM ttrss_filters2_actions AS a,
1893 ttrss_filter_actions AS t
1895 action_id = t.id AND filter_id = ?");
1896 $sth2->execute([$filter_id]);
1898 while ($action_line = $sth2->fetch()) {
1899 # print_r($action_line);
1902 $action["type"] = $action_line["type_name"];
1903 $action["param"] = $action_line["action_param"];
1905 array_push($actions, $action);
1910 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1911 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1912 $filter["rules"] = $rules;
1913 $filter["actions"] = $actions;
1915 if (count($rules) > 0 && count($actions) > 0) {
1916 array_push($filters, $filter);
1923 function get_score_pic($score) {
1925 return "score_high.png";
1926 } else if ($score > 0) {
1927 return "score_half_high.png";
1928 } else if ($score < -100) {
1929 return "score_low.png";
1930 } else if ($score < 0) {
1931 return "score_half_low.png";
1933 return "score_neutral.png";
1937 function feed_has_icon($id) {
1938 return is_file(ICONS_DIR
. "/$id.ico") && filesize(ICONS_DIR
. "/$id.ico") > 0;
1941 function init_plugins() {
1942 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1947 function add_feed_category($feed_cat, $parent_cat_id = false) {
1949 if (!$feed_cat) return false;
1951 ////db_query("BEGIN");
1953 if ($parent_cat_id) {
1954 $parent_qpart = "parent_cat = '$parent_cat_id'";
1955 $parent_insert = "'$parent_cat_id'";
1957 $parent_qpart = "parent_cat IS NULL";
1958 $parent_insert = "NULL";
1961 $feed_cat = mb_substr($feed_cat, 0, 250);
1965 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1966 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ?");
1968 if (db_num_rows($result) == 0) {
1971 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1972 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
1983 * Fixes incomplete URLs by prepending "http://".
1984 * Also replaces feed:// with http://, and
1985 * prepends a trailing slash if the url is a domain name only.
1987 * @param string $url Possibly incomplete URL
1989 * @return string Fixed URL.
1991 function fix_url($url) {
1993 // support schema-less urls
1994 if (strpos($url, '//') === 0) {
1995 $url = 'https:' . $url;
1998 if (strpos($url, '://') === false) {
1999 $url = 'http://' . $url;
2000 } else if (substr($url, 0, 5) == 'feed:') {
2001 $url = 'http:' . substr($url, 5);
2004 //prepend slash if the URL has no slash in it
2005 // "http://www.example" -> "http://www.example/"
2006 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2010 //convert IDNA hostname to punycode if possible
2011 if (function_exists("idn_to_ascii")) {
2012 $parts = parse_url($url);
2013 if (mb_detect_encoding($parts['host']) != 'ASCII')
2015 $parts['host'] = idn_to_ascii($parts['host']);
2016 $url = build_url($parts);
2020 if ($url != "http:///")
2026 function validate_feed_url($url) {
2027 $parts = parse_url($url);
2029 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2033 /* function save_email_address($email) {
2034 // FIXME: implement persistent storage of emails
2036 if (!$_SESSION['stored_emails'])
2037 $_SESSION['stored_emails'] = array();
2039 if (!in_array($email, $_SESSION['stored_emails']))
2040 array_push($_SESSION['stored_emails'], $email);
2044 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2046 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2048 $sql_is_cat = bool_to_sql_bool($is_cat);
2050 $result = db_query("SELECT access_key FROM ttrss_access_keys
2051 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
2052 AND owner_uid = " . $owner_uid);
2054 if (db_num_rows($result) == 1) {
2055 return db_fetch_result($result, 0, "access_key");
2057 $key = db_escape_string(uniqid_short());
2059 $result = db_query("INSERT INTO ttrss_access_keys
2060 (access_key, feed_id, is_cat, owner_uid)
2061 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
2068 function get_feeds_from_html($url, $content)
2070 $url = fix_url($url);
2071 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2073 libxml_use_internal_errors(true);
2075 $doc = new DOMDocument();
2076 $doc->loadHTML($content);
2077 $xpath = new DOMXPath($doc);
2078 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2079 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2080 $feedUrls = array();
2081 foreach ($entries as $entry) {
2082 if ($entry->hasAttribute('href')) {
2083 $title = $entry->getAttribute('title');
2085 $title = $entry->getAttribute('type');
2087 $feedUrl = rewrite_relative_url(
2088 $baseUrl, $entry->getAttribute('href')
2090 $feedUrls[$feedUrl] = $title;
2096 function is_html($content) {
2097 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2100 function url_is_html($url, $login = false, $pass = false) {
2101 return is_html(fetch_file_contents($url, false, $login, $pass));
2104 function build_url($parts) {
2105 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2108 function cleanup_url_path($path) {
2109 $path = str_replace("/./", "/", $path);
2110 $path = str_replace("//", "/", $path);
2116 * Converts a (possibly) relative URL to a absolute one.
2118 * @param string $url Base URL (i.e. from where the document is)
2119 * @param string $rel_url Possibly relative URL in the document
2121 * @return string Absolute URL
2123 function rewrite_relative_url($url, $rel_url) {
2124 if (strpos($rel_url, "://") !== false) {
2126 } else if (strpos($rel_url, "//") === 0) {
2127 # protocol-relative URL (rare but they exist)
2129 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2130 # magnet:, feed:, etc
2132 } else if (strpos($rel_url, "/") === 0) {
2133 $parts = parse_url($url);
2134 $parts['path'] = $rel_url;
2135 $parts['path'] = cleanup_url_path($parts['path']);
2137 return build_url($parts);
2140 $parts = parse_url($url);
2141 if (!isset($parts['path'])) {
2142 $parts['path'] = '/';
2144 $dir = $parts['path'];
2145 if (substr($dir, -1) !== '/') {
2146 $dir = dirname($parts['path']);
2147 $dir !== '/' && $dir .= '/';
2149 $parts['path'] = $dir . $rel_url;
2150 $parts['path'] = cleanup_url_path($parts['path']);
2152 return build_url($parts);
2156 function cleanup_tags($days = 14, $limit = 1000) {
2158 if (DB_TYPE
== "pgsql") {
2159 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2160 } else if (DB_TYPE
== "mysql") {
2161 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2166 while ($limit > 0) {
2169 $query = "SELECT ttrss_tags.id AS id
2170 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2171 WHERE post_int_id = int_id AND $interval_query AND
2172 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2174 $result = db_query($query);
2178 while ($line = db_fetch_assoc($result)) {
2179 array_push($ids, $line['id']);
2182 if (count($ids) > 0) {
2183 $ids = join(",", $ids);
2185 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2186 $tags_deleted +
= db_affected_rows($tmp_result);
2191 $limit -= $limit_part;
2194 return $tags_deleted;
2197 function print_user_stylesheet() {
2198 $value = get_pref('USER_STYLESHEET');
2201 print "<style type=\"text/css\">";
2202 print str_replace("<br/>", "\n", $value);
2208 function filter_to_sql($filter, $owner_uid) {
2211 if (DB_TYPE
== "pgsql")
2214 $reg_qpart = "REGEXP";
2216 foreach ($filter["rules"] AS $rule) {
2217 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2218 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2219 $rule['reg_exp']) !== FALSE;
2221 if ($regexp_valid) {
2223 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2225 switch ($rule["type"]) {
2227 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2228 $rule['reg_exp'] . "')";
2231 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2232 $rule['reg_exp'] . "')";
2235 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2236 $rule['reg_exp'] . "') OR LOWER(" .
2237 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2240 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2241 $rule['reg_exp'] . "')";
2244 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2245 $rule['reg_exp'] . "')";
2248 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2249 $rule['reg_exp'] . "')";
2253 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2255 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2256 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2259 if (isset($rule["cat_id"])) {
2261 if ($rule["cat_id"] > 0) {
2262 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2263 array_push($children, $rule["cat_id"]);
2265 $children = join(",", $children);
2267 $cat_qpart = "cat_id IN ($children)";
2269 $cat_qpart = "cat_id IS NULL";
2272 $qpart .= " AND $cat_qpart";
2275 $qpart .= " AND feed_id IS NOT NULL";
2277 array_push($query, "($qpart)");
2282 if (count($query) > 0) {
2283 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2285 $fullquery = "(false)";
2288 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2293 if (!function_exists('gzdecode')) {
2294 function gzdecode($string) { // no support for 2nd argument
2295 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2296 base64_encode($string));
2300 function get_random_bytes($length) {
2301 if (function_exists('openssl_random_pseudo_bytes')) {
2302 return openssl_random_pseudo_bytes($length);
2306 for ($i = 0; $i < $length; $i++
)
2307 $output .= chr(mt_rand(0, 255));
2313 function read_stdin() {
2314 $fp = fopen("php://stdin", "r");
2317 $line = trim(fgets($fp));
2325 function implements_interface($class, $interface) {
2326 return in_array($interface, class_implements($class));
2329 function get_minified_js($files) {
2330 require_once 'lib/jshrink/Minifier.php';
2334 foreach ($files as $js) {
2335 if (!isset($_GET['debug'])) {
2336 $cached_file = CACHE_DIR
. "/js/".basename($js).".js";
2338 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2340 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2342 if ($header && $contents) {
2343 list($htag, $hversion) = explode(":", $header);
2345 if ($htag == "tt-rss" && $hversion == VERSION
) {
2352 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js.js"));
2353 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2357 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2364 function calculate_dep_timestamp() {
2365 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2369 foreach ($files as $file) {
2370 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2376 function T_js_decl($s1, $s2) {
2378 $s1 = preg_replace("/\n/", "", $s1);
2379 $s2 = preg_replace("/\n/", "", $s2);
2381 $s1 = preg_replace("/\"/", "\\\"", $s1);
2382 $s2 = preg_replace("/\"/", "\\\"", $s2);
2384 return "T_messages[\"$s1\"] = \"$s2\";\n";
2388 function init_js_translations() {
2390 print 'var T_messages = new Object();
2393 if (T_messages[msg]) {
2394 return T_messages[msg];
2400 function ngettext(msg1, msg2, n) {
2401 return __((parseInt(n) > 1) ? msg2 : msg1);
2404 $l10n = _get_reader();
2406 for ($i = 0; $i < $l10n->total
; $i++
) {
2407 $orig = $l10n->get_original_string($i);
2408 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2409 $key = explode(chr(0), $orig);
2410 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2411 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2413 $translation = __($orig);
2414 print T_js_decl($orig, $translation);
2419 function get_theme_path($theme) {
2420 $check = "themes/$theme";
2421 if (file_exists($check)) return $check;
2423 $check = "themes.local/$theme";
2424 if (file_exists($check)) return $check;
2427 function theme_valid($theme) {
2428 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2430 if (in_array($theme, $bundled_themes)) return true;
2432 $file = "themes/" . basename($theme);
2434 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2436 if (file_exists($file) && is_readable($file)) {
2437 $fh = fopen($file, "r");
2440 $header = fgets($fh);
2443 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2451 * @SuppressWarnings(unused)
2453 function error_json($code) {
2454 require_once "errors.php";
2456 @$message = $ERRORS[$code];
2458 return json_encode(array("error" =>
2459 array("code" => $code, "message" => $message)));
2463 /*function abs_to_rel_path($dir) {
2464 $tmp = str_replace(dirname(__DIR__), "", $dir);
2466 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2471 function get_upload_error_message($code) {
2474 0 => __('There is no error, the file uploaded with success'),
2475 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2476 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2477 3 => __('The uploaded file was only partially uploaded'),
2478 4 => __('No file was uploaded'),
2479 6 => __('Missing a temporary folder'),
2480 7 => __('Failed to write file to disk.'),
2481 8 => __('A PHP extension stopped the file upload.'),
2484 return $errors[$code];
2487 function base64_img($filename) {
2488 if (file_exists($filename)) {
2489 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2491 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2497 /* this is essentially a wrapper for readfile() which allows plugins to hook
2498 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2500 hook function should return true if request was handled (or at least attempted to)
2502 note that this can be called without user context so the plugin to handle this
2503 should be loaded systemwide in config.php */
2504 function send_local_file($filename) {
2505 if (file_exists($filename)) {
2506 $tmppluginhost = new PluginHost();
2508 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2509 $tmppluginhost->load_data();
2511 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2512 if ($plugin->hook_send_local_file($filename)) return true;
2515 $mimetype = mime_content_type($filename);
2516 header("Content-type: $mimetype");
2518 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2519 header("Last-Modified: $stamp", true);
2521 return readfile($filename);
2527 function check_mysql_tables() {
2528 $schema = db_escape_string(DB_NAME
);
2530 $result = db_query("SELECT engine, table_name FROM information_schema.tables WHERE
2531 table_schema = '$schema' AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2535 while ($line = db_fetch_assoc($result)) {
2536 array_push($bad_tables, $line);