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);
229 "SELECT owner_uid FROM ttrss_feeds WHERE id = '$feed_id'");
233 if (db_num_rows($result) == 1) {
234 $owner_uid = db_fetch_result($result, 0, "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
;
254 if (!$purge_unread) $query_limit = " unread = false AND ";
256 if (DB_TYPE
== "pgsql") {
257 $result = db_query("DELETE FROM ttrss_user_entries
259 WHERE ttrss_entries.id = ref_id AND
261 feed_id = '$feed_id' AND
263 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
267 /* $result = db_query("DELETE FROM ttrss_user_entries WHERE
268 marked = false AND feed_id = '$feed_id' AND
269 (SELECT date_updated FROM ttrss_entries WHERE
270 id = ref_id) < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)"); */
272 $result = db_query("DELETE FROM ttrss_user_entries
273 USING ttrss_user_entries, ttrss_entries
274 WHERE ttrss_entries.id = ref_id AND
276 feed_id = '$feed_id' AND
278 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
281 $rows = db_affected_rows($result);
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) {
294 $result = db_query("SELECT purge_interval, owner_uid FROM ttrss_feeds
295 WHERE id = '$feed_id'");
297 if (db_num_rows($result) == 1) {
298 $purge_interval = db_fetch_result($result, 0, "purge_interval");
299 $owner_uid = db_fetch_result($result, 0, "owner_uid");
301 if ($purge_interval == 0) $purge_interval = get_pref(
302 'PURGE_OLD_DAYS', $owner_uid);
304 return $purge_interval;
311 /*function get_feed_update_interval($feed_id) {
312 $result = db_query("SELECT owner_uid, update_interval FROM
313 ttrss_feeds WHERE id = '$feed_id'");
315 if (db_num_rows($result) == 1) {
316 $update_interval = db_fetch_result($result, 0, "update_interval");
317 $owner_uid = db_fetch_result($result, 0, "owner_uid");
319 if ($update_interval != 0) {
320 return $update_interval;
322 return get_pref('DEFAULT_UPDATE_INTERVAL', $owner_uid, false);
330 // TODO: multiple-argument way is deprecated, first parameter is a hash now
331 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
332 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
334 global $fetch_last_error;
335 global $fetch_last_error_code;
336 global $fetch_last_error_content;
337 global $fetch_last_content_type;
338 global $fetch_last_modified;
339 global $fetch_curl_used;
341 $fetch_last_error = false;
342 $fetch_last_error_code = -1;
343 $fetch_last_error_content = "";
344 $fetch_last_content_type = "";
345 $fetch_curl_used = false;
346 $fetch_last_modified = "";
348 if (!is_array($options)) {
350 // falling back on compatibility shim
351 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
354 for ($i = 0; $i < func_num_args(); $i++
) {
355 $tmp[$option_names[$i]] = func_get_arg($i);
361 "url" => func_get_arg(0),
362 "type" => @func_get_arg(1),
363 "login" => @func_get_arg(2),
364 "pass" => @func_get_arg(3),
365 "post_query" => @func_get_arg(4),
366 "timeout" => @func_get_arg(5),
367 "timestamp" => @func_get_arg(6),
368 "useragent" => @func_get_arg(7)
372 $url = $options["url"];
373 $type = isset($options["type"]) ?
$options["type"] : false;
374 $login = isset($options["login"]) ?
$options["login"] : false;
375 $pass = isset($options["pass"]) ?
$options["pass"] : false;
376 $post_query = isset($options["post_query"]) ?
$options["post_query"] : false;
377 $timeout = isset($options["timeout"]) ?
$options["timeout"] : false;
378 $last_modified = isset($options["last_modified"]) ?
$options["last_modified"] : "";
379 $useragent = isset($options["useragent"]) ?
$options["useragent"] : false;
380 $followlocation = isset($options["followlocation"]) ?
$options["followlocation"] : true;
382 $url = ltrim($url, ' ');
383 $url = str_replace(' ', '%20', $url);
385 if (strpos($url, "//") === 0)
386 $url = 'http:' . $url;
388 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
390 $fetch_curl_used = true;
392 $ch = curl_init($url);
394 if ($last_modified && !$post_query) {
395 curl_setopt($ch, CURLOPT_HTTPHEADER
,
396 array("If-Modified-Since: $last_modified"));
399 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
400 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
401 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
402 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
403 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
404 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
405 curl_setopt($ch, CURLOPT_HEADER
, true);
406 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
407 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
409 curl_setopt($ch, CURLOPT_ENCODING
, "");
410 //curl_setopt($ch, CURLOPT_REFERER, $url);
412 if (!ini_get("open_basedir")) {
413 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
416 if (defined('_CURL_HTTP_PROXY')) {
417 curl_setopt($ch, CURLOPT_PROXY
, _CURL_HTTP_PROXY
);
421 curl_setopt($ch, CURLOPT_POST
, true);
422 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
426 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
428 $ret = @curl_exec
($ch);
430 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE
);
431 $headers = explode("\r\n", substr($ret, 0, $headers_length));
432 $contents = substr($ret, $headers_length);
434 foreach ($headers as $header) {
435 list ($key, $value) = explode(": ", $header);
437 if (strtolower($key) == "last-modified") {
438 $fetch_last_modified = $value;
442 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
443 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
444 $contents = @curl_exec
($ch);
447 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
448 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
450 $fetch_last_error_code = $http_code;
452 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
453 if (curl_errno($ch) != 0) {
454 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
456 $fetch_last_error = "HTTP Code: $http_code";
458 $fetch_last_error_content = $contents;
464 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
469 /*$fetch_last_modified = curl_getinfo($ch, CURLINFO_FILETIME);
471 if ($fetch_last_modified != -1) {
472 echo date("Y-m-d H:i:s", $fetch_last_modified); die;
480 $fetch_curl_used = false;
482 if ($login && $pass){
483 $url_parts = array();
485 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
487 $pass = urlencode($pass);
489 if ($url_parts[1] && $url_parts[2]) {
490 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
494 // TODO: should this support POST requests or not? idk
496 if (!$post_query && $last_modified) {
497 $context = stream_context_create(array(
500 'ignore_errors' => true,
501 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
502 'protocol_version'=> 1.1,
503 'header' => "If-Modified-Since: $last_modified\r\n")
506 $context = stream_context_create(array(
509 'ignore_errors' => true,
510 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
511 'protocol_version'=> 1.1
515 $old_error = error_get_last();
517 $data = @file_get_contents
($url, false, $context);
519 if (isset($http_response_header) && is_array($http_response_header)) {
520 foreach ($http_response_header as $h) {
521 list ($key, $value) = explode(": ", $h);
523 $key = strtolower($key);
525 if ($key == 'content-type') {
526 $fetch_last_content_type = $value;
527 // don't abort here b/c there might be more than one
528 // e.g. if we were being redirected -- last one is the right one
529 } else if ($key == 'last-modified') {
530 $fetch_last_modified = $value;
533 if (substr(strtolower($h), 0, 7) == 'http/1.') {
534 $fetch_last_error_code = (int) substr($h, 9, 3);
539 if ($fetch_last_error_code != 200) {
540 $error = error_get_last();
542 if ($error['message'] != $old_error['message']) {
543 $fetch_last_error = $error["message"];
545 $fetch_last_error = "HTTP Code: $fetch_last_error_code";
548 $fetch_last_error_content = $data;
558 * Try to determine the favicon URL for a feed.
559 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
560 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
562 * @param string $url A feed or page URL
564 * @return mixed The favicon URL, or false if none was found.
566 function get_favicon_url($url) {
568 $favicon_url = false;
570 if ($html = @fetch_file_contents
($url)) {
572 libxml_use_internal_errors(true);
574 $doc = new DOMDocument();
575 $doc->loadHTML($html);
576 $xpath = new DOMXPath($doc);
578 $base = $xpath->query('/html/head/base[@href]');
579 foreach ($base as $b) {
580 $url = rewrite_relative_url($url, $b->getAttribute("href"));
584 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
585 if (count($entries) > 0) {
586 foreach ($entries as $entry) {
587 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
594 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
597 } // function get_favicon_url
599 function initialize_user_prefs($uid, $profile = false) {
601 $uid = db_escape_string($uid);
605 $profile_qpart = "AND profile IS NULL";
607 $profile_qpart = "AND profile = '$profile'";
610 if (get_schema_version() < 63) $profile_qpart = "";
614 $result = db_query("SELECT pref_name,def_value FROM ttrss_prefs");
616 $u_result = db_query("SELECT pref_name
617 FROM ttrss_user_prefs WHERE owner_uid = '$uid' $profile_qpart");
619 $active_prefs = array();
621 while ($line = db_fetch_assoc($u_result)) {
622 array_push($active_prefs, $line["pref_name"]);
625 while ($line = db_fetch_assoc($result)) {
626 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
627 // print "adding " . $line["pref_name"] . "<br>";
629 $line["def_value"] = db_escape_string($line["def_value"]);
630 $line["pref_name"] = db_escape_string($line["pref_name"]);
632 if (get_schema_version() < 63) {
633 db_query("INSERT INTO ttrss_user_prefs
634 (owner_uid,pref_name,value) VALUES
635 ('$uid', '".$line["pref_name"]."','".$line["def_value"]."')");
638 db_query("INSERT INTO ttrss_user_prefs
639 (owner_uid,pref_name,value, profile) VALUES
640 ('$uid', '".$line["pref_name"]."','".$line["def_value"]."', $profile)");
650 function get_ssl_certificate_id() {
651 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
652 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
653 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
654 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
655 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
657 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
658 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
659 $_SERVER["SSL_CLIENT_V_START"] .
660 $_SERVER["SSL_CLIENT_V_END"] .
661 $_SERVER["SSL_CLIENT_S_DN"]);
666 function authenticate_user($login, $password, $check_only = false) {
668 if (!SINGLE_USER_MODE
) {
671 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
673 $user_id = (int) $plugin->authenticate($login, $password);
676 $_SESSION["auth_module"] = strtolower(get_class($plugin));
681 if ($user_id && !$check_only) {
684 $_SESSION["uid"] = $user_id;
685 $_SESSION["version"] = VERSION_STATIC
;
687 $result = db_query("SELECT login,access_level,pwd_hash FROM ttrss_users
688 WHERE id = '$user_id'");
690 $_SESSION["name"] = db_fetch_result($result, 0, "login");
691 $_SESSION["access_level"] = db_fetch_result($result, 0, "access_level");
692 $_SESSION["csrf_token"] = uniqid_short();
694 db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
697 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
698 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
699 $_SESSION["pwd_hash"] = db_fetch_result($result, 0, "pwd_hash");
701 $_SESSION["last_version_check"] = time();
703 initialize_user_prefs($_SESSION["uid"]);
712 $_SESSION["uid"] = 1;
713 $_SESSION["name"] = "admin";
714 $_SESSION["access_level"] = 10;
716 $_SESSION["hide_hello"] = true;
717 $_SESSION["hide_logout"] = true;
719 $_SESSION["auth_module"] = false;
721 if (!$_SESSION["csrf_token"]) {
722 $_SESSION["csrf_token"] = uniqid_short();
725 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
727 initialize_user_prefs($_SESSION["uid"]);
733 function make_password($length = 8) {
736 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
740 while ($i < $length) {
741 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
743 if (!strstr($password, $char)) {
751 // this is called after user is created to initialize default feeds, labels
754 // user preferences are checked on every login, not here
756 function initialize_user($uid) {
758 db_query("insert into ttrss_feeds (owner_uid,title,feed_url)
759 values ('$uid', 'Tiny Tiny RSS: Forum',
760 'http://tt-rss.org/forum/rss.php')");
763 function logout_user() {
765 if (isset($_COOKIE[session_name()])) {
766 setcookie(session_name(), '', time()-42000, '/');
770 function validate_csrf($csrf_token) {
771 return $csrf_token == $_SESSION['csrf_token'];
774 function load_user_plugins($owner_uid, $pluginhost = false) {
776 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
778 if ($owner_uid && SCHEMA_VERSION
>= 100) {
779 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
781 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
783 if (get_schema_version() > 100) {
784 $pluginhost->load_data();
789 function login_sequence() {
790 if (SINGLE_USER_MODE
) {
792 authenticate_user("admin", null);
794 load_user_plugins($_SESSION["uid"]);
796 if (!validate_session()) $_SESSION["uid"] = false;
798 if (!$_SESSION["uid"]) {
800 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
801 $_SESSION["ref_schema_version"] = get_schema_version(true);
803 authenticate_user(null, null, true);
806 if (!$_SESSION["uid"]) {
808 setcookie(session_name(), '', time()-42000, '/');
815 /* bump login timestamp */
816 db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
818 $_SESSION["last_login_update"] = time();
821 if ($_SESSION["uid"]) {
823 load_user_plugins($_SESSION["uid"]);
827 db_query("DELETE FROM ttrss_counters_cache WHERE owner_uid = ".
828 $_SESSION["uid"] . " AND
829 (SELECT COUNT(id) FROM ttrss_feeds WHERE
830 ttrss_feeds.id = feed_id) = 0");
832 db_query("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ".
833 $_SESSION["uid"] . " AND
834 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
835 ttrss_feed_categories.id = feed_id) = 0");
842 function truncate_string($str, $max_len, $suffix = '…') {
843 if (mb_strlen($str, "utf-8") > $max_len) {
844 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
851 function truncate_middle($str, $max_len, $suffix = '…') {
852 if (strlen($str) > $max_len) {
853 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
859 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
862 $source_tz = new DateTimeZone($source_tz);
863 } catch (Exception
$e) {
864 $source_tz = new DateTimeZone('UTC');
868 $dest_tz = new DateTimeZone($dest_tz);
869 } catch (Exception
$e) {
870 $dest_tz = new DateTimeZone('UTC');
873 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
874 return $dt->format('U') +
$dest_tz->getOffset($dt);
877 function make_local_datetime($timestamp, $long, $owner_uid = false,
878 $no_smart_dt = false, $eta_min = false) {
880 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
881 if (!$timestamp) $timestamp = '1970-01-01 0:00';
886 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
888 $timestamp = substr($timestamp, 0, 19);
890 # We store date in UTC internally
891 $dt = new DateTime($timestamp, $utc_tz);
893 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
895 if ($user_tz_string != 'Automatic') {
898 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
899 } catch (Exception
$e) {
903 $tz_offset = $user_tz->getOffset($dt);
905 $tz_offset = (int) -$_SESSION["clientTzOffset"];
908 $user_timestamp = $dt->format('U') +
$tz_offset;
911 return smart_date_time($user_timestamp,
912 $tz_offset, $owner_uid, $eta_min);
915 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
917 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
919 return date($format, $user_timestamp);
923 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
924 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
926 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
927 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
928 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
929 return date("G:i", $timestamp);
930 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
931 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
932 return date($format, $timestamp);
934 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
935 return date($format, $timestamp);
939 function sql_bool_to_bool($s) {
940 if ($s == "t" ||
$s == "1" ||
strtolower($s) == "true") {
947 function bool_to_sql_bool($s) {
955 // Session caching removed due to causing wrong redirects to upgrade
956 // script when get_schema_version() is called on an obsolete session
957 // created on a previous schema version.
958 function get_schema_version($nocache = false) {
959 global $schema_version;
961 if (!$schema_version && !$nocache) {
962 $result = db_query("SELECT schema_version FROM ttrss_version");
963 $version = db_fetch_result($result, 0, "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_TYPE
== "mysql") {
983 $result = db_query("SELECT true", false);
984 if (db_num_rows($result) != 1) {
989 if (db_escape_string("testTEST") != "testTEST") {
993 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
996 function file_is_locked($filename) {
997 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
998 if (function_exists('flock')) {
999 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1001 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1002 flock($fp, LOCK_UN
);
1012 return true; // consider the file always locked and skip the test
1019 function make_lockfile($filename) {
1020 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1022 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1023 $stat_h = fstat($fp);
1024 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1026 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1027 if ($stat_h["ino"] != $stat_f["ino"] ||
1028 $stat_h["dev"] != $stat_f["dev"]) {
1034 if (function_exists('posix_getpid')) {
1035 fwrite($fp, posix_getpid() . "\n");
1043 function make_stampfile($filename) {
1044 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1046 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1047 fwrite($fp, time() . "\n");
1048 flock($fp, LOCK_UN
);
1056 function sql_random_function() {
1057 if (DB_TYPE
== "mysql") {
1064 function getFeedUnread($feed, $is_cat = false) {
1065 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1069 /*function get_pgsql_version() {
1070 $result = db_query("SELECT version() AS version");
1071 $version = explode(" ", db_fetch_result($result, 0, "version"));
1075 function checkbox_to_sql_bool($val) {
1076 return ($val == "on") ?
"true" : "false";
1079 /*function getFeedCatTitle($id) {
1081 return __("Special");
1082 } else if ($id < LABEL_BASE_INDEX) {
1083 return __("Labels");
1084 } else if ($id > 0) {
1085 $result = db_query("SELECT ttrss_feed_categories.title
1086 FROM ttrss_feeds, ttrss_feed_categories WHERE ttrss_feeds.id = '$id' AND
1087 cat_id = ttrss_feed_categories.id");
1088 if (db_num_rows($result) == 1) {
1089 return db_fetch_result($result, 0, "title");
1091 return __("Uncategorized");
1094 return "getFeedCatTitle($id) failed";
1099 function uniqid_short() {
1100 return uniqid(base_convert(rand(), 10, 36));
1103 function make_init_params() {
1106 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1107 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1108 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1109 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1111 $params[strtolower($param)] = (int) get_pref($param);
1114 $params["icons_url"] = ICONS_URL
;
1115 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1116 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1117 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1118 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1119 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1120 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1122 $theme = get_pref( "USER_CSS_THEME", false, false);
1123 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1125 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1127 $params["php_platform"] = PHP_OS
;
1128 $params["php_version"] = PHP_VERSION
;
1130 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1132 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1133 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
1135 $max_feed_id = db_fetch_result($result, 0, "mid");
1136 $num_feeds = db_fetch_result($result, 0, "nf");
1138 $params["max_feed_id"] = (int) $max_feed_id;
1139 $params["num_feeds"] = (int) $num_feeds;
1141 $params["hotkeys"] = get_hotkeys_map();
1143 $params["csrf_token"] = $_SESSION["csrf_token"];
1144 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1146 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1148 $params["icon_alert"] = base64_img("images/alert.png");
1149 $params["icon_information"] = base64_img("images/information.png");
1150 $params["icon_cross"] = base64_img("images/cross.png");
1151 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1153 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1158 function get_hotkeys_info() {
1160 __("Navigation") => array(
1161 "next_feed" => __("Open next feed"),
1162 "prev_feed" => __("Open previous feed"),
1163 "next_article" => __("Open next article"),
1164 "prev_article" => __("Open previous article"),
1165 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1166 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1167 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1168 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1169 "search_dialog" => __("Show search dialog")),
1170 __("Article") => array(
1171 "toggle_mark" => __("Toggle starred"),
1172 "toggle_publ" => __("Toggle published"),
1173 "toggle_unread" => __("Toggle unread"),
1174 "edit_tags" => __("Edit tags"),
1175 "open_in_new_window" => __("Open in new window"),
1176 "catchup_below" => __("Mark below as read"),
1177 "catchup_above" => __("Mark above as read"),
1178 "article_scroll_down" => __("Scroll down"),
1179 "article_scroll_up" => __("Scroll up"),
1180 "select_article_cursor" => __("Select article under cursor"),
1181 "email_article" => __("Email article"),
1182 "close_article" => __("Close/collapse article"),
1183 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1184 "toggle_widescreen" => __("Toggle widescreen mode"),
1185 "toggle_embed_original" => __("Toggle embed original")),
1186 __("Article selection") => array(
1187 "select_all" => __("Select all articles"),
1188 "select_unread" => __("Select unread"),
1189 "select_marked" => __("Select starred"),
1190 "select_published" => __("Select published"),
1191 "select_invert" => __("Invert selection"),
1192 "select_none" => __("Deselect everything")),
1193 __("Feed") => array(
1194 "feed_refresh" => __("Refresh current feed"),
1195 "feed_unhide_read" => __("Un/hide read feeds"),
1196 "feed_subscribe" => __("Subscribe to feed"),
1197 "feed_edit" => __("Edit feed"),
1198 "feed_catchup" => __("Mark as read"),
1199 "feed_reverse" => __("Reverse headlines"),
1200 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1201 "feed_debug_update" => __("Debug feed update"),
1202 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1203 "catchup_all" => __("Mark all feeds as read"),
1204 "cat_toggle_collapse" => __("Un/collapse current category"),
1205 "toggle_combined_mode" => __("Toggle combined mode"),
1206 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1207 __("Go to") => array(
1208 "goto_all" => __("All articles"),
1209 "goto_fresh" => __("Fresh"),
1210 "goto_marked" => __("Starred"),
1211 "goto_published" => __("Published"),
1212 "goto_tagcloud" => __("Tag cloud"),
1213 "goto_prefs" => __("Preferences")),
1214 __("Other") => array(
1215 "create_label" => __("Create label"),
1216 "create_filter" => __("Create filter"),
1217 "collapse_sidebar" => __("Un/collapse sidebar"),
1218 "help_dialog" => __("Show help dialog"))
1221 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1222 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1228 function get_hotkeys_map() {
1230 // "navigation" => array(
1233 "n" => "next_article",
1234 "p" => "prev_article",
1235 "(38)|up" => "prev_article",
1236 "(40)|down" => "next_article",
1237 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1238 // "^(40)|Ctrl-down" => "next_article_noscroll",
1239 "(191)|/" => "search_dialog",
1240 // "article" => array(
1241 "s" => "toggle_mark",
1242 "*s" => "toggle_publ",
1243 "u" => "toggle_unread",
1244 "*t" => "edit_tags",
1245 "o" => "open_in_new_window",
1246 "c p" => "catchup_below",
1247 "c n" => "catchup_above",
1248 "*n" => "article_scroll_down",
1249 "*p" => "article_scroll_up",
1250 "*(38)|Shift+up" => "article_scroll_up",
1251 "*(40)|Shift+down" => "article_scroll_down",
1252 "a *w" => "toggle_widescreen",
1253 "a e" => "toggle_embed_original",
1254 "e" => "email_article",
1255 "a q" => "close_article",
1256 // "article_selection" => array(
1257 "a a" => "select_all",
1258 "a u" => "select_unread",
1259 "a *u" => "select_marked",
1260 "a p" => "select_published",
1261 "a i" => "select_invert",
1262 "a n" => "select_none",
1264 "f r" => "feed_refresh",
1265 "f a" => "feed_unhide_read",
1266 "f s" => "feed_subscribe",
1267 "f e" => "feed_edit",
1268 "f q" => "feed_catchup",
1269 "f x" => "feed_reverse",
1270 "f g" => "feed_toggle_vgroup",
1271 "f *d" => "feed_debug_update",
1272 "f *g" => "feed_debug_viewfeed",
1273 "f *c" => "toggle_combined_mode",
1274 "f c" => "toggle_cdm_expanded",
1275 "*q" => "catchup_all",
1276 "x" => "cat_toggle_collapse",
1278 "g a" => "goto_all",
1279 "g f" => "goto_fresh",
1280 "g s" => "goto_marked",
1281 "g p" => "goto_published",
1282 "g t" => "goto_tagcloud",
1283 "g *p" => "goto_prefs",
1284 // "other" => array(
1285 "(9)|Tab" => "select_article_cursor", // tab
1286 "c l" => "create_label",
1287 "c f" => "create_filter",
1288 "c s" => "collapse_sidebar",
1289 "^(191)|Ctrl+/" => "help_dialog",
1292 if (get_pref('COMBINED_DISPLAY_MODE')) {
1293 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1294 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1297 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1298 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1301 $prefixes = array();
1303 foreach (array_keys($hotkeys) as $hotkey) {
1304 $pair = explode(" ", $hotkey, 2);
1306 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1307 array_push($prefixes, $pair[0]);
1311 return array($prefixes, $hotkeys);
1314 function check_for_update() {
1315 if (defined("GIT_VERSION_TIMESTAMP")) {
1316 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1319 $content = json_decode($content, true);
1321 if ($content && isset($content["changeset"])) {
1322 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1323 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1325 return $content["changeset"]["id"];
1334 function make_runtime_info($disable_update_check = false) {
1337 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1338 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
1340 $max_feed_id = db_fetch_result($result, 0, "mid");
1341 $num_feeds = db_fetch_result($result, 0, "nf");
1343 $data["max_feed_id"] = (int) $max_feed_id;
1344 $data["num_feeds"] = (int) $num_feeds;
1346 $data['last_article_id'] = Article
::getLastArticleId();
1347 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1349 $data['dep_ts'] = calculate_dep_timestamp();
1350 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1352 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1354 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1355 $update_result = @check_for_update
();
1357 $data["update_result"] = $update_result;
1359 $_SESSION["last_version_check"] = time();
1362 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1364 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1366 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1368 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1371 $stamp_delta = time() - $stamp;
1373 if ($stamp_delta > 1800) {
1377 $_SESSION["daemon_stamp_check"] = time();
1380 $data['daemon_stamp_ok'] = $stamp_check;
1382 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1384 $data['daemon_stamp'] = $stamp_fmt;
1392 function search_to_sql($search, $search_language) {
1394 $keywords = str_getcsv(trim($search), " ");
1395 $query_keywords = array();
1396 $search_words = array();
1397 $search_query_leftover = array();
1399 if ($search_language)
1400 $search_language = db_escape_string(mb_strtolower($search_language));
1402 $search_language = "english";
1404 foreach ($keywords as $k) {
1405 if (strpos($k, "-") === 0) {
1412 $commandpair = explode(":", mb_strtolower($k), 2);
1414 switch ($commandpair[0]) {
1416 if ($commandpair[1]) {
1417 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
1418 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1420 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1421 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1422 array_push($search_words, $k);
1426 if ($commandpair[1]) {
1427 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
1428 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1430 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1431 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1432 array_push($search_words, $k);
1436 if ($commandpair[1]) {
1437 if ($commandpair[1] == "true")
1438 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1439 else if ($commandpair[1] == "false")
1440 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1442 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
1443 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
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);
1452 if ($commandpair[1]) {
1453 if ($commandpair[1] == "true")
1454 array_push($query_keywords, "($not (marked = true))");
1456 array_push($query_keywords, "($not (marked = 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 ($commandpair[1]) {
1465 if ($commandpair[1] == "true")
1466 array_push($query_keywords, "($not (published = true))");
1468 array_push($query_keywords, "($not (published = false))");
1471 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1472 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1473 if (!$not) array_push($search_words, $k);
1477 if ($commandpair[1]) {
1478 if ($commandpair[1] == "true")
1479 array_push($query_keywords, "($not (unread = true))");
1481 array_push($query_keywords, "($not (unread = false))");
1484 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1485 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1486 if (!$not) array_push($search_words, $k);
1490 if (strpos($k, "@") === 0) {
1492 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1493 $orig_ts = strtotime(substr($k, 1));
1494 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1496 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1498 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1501 if (DB_TYPE
== "pgsql") {
1502 $k = mb_strtolower($k);
1503 array_push($search_query_leftover, $not ?
"!$k" : $k);
1505 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1506 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1509 if (!$not) array_push($search_words, $k);
1514 if (count($search_query_leftover) > 0) {
1515 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
1517 if (DB_TYPE
== "pgsql") {
1518 array_push($query_keywords,
1519 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
1524 $search_query_part = implode("AND", $query_keywords);
1526 return array($search_query_part, $search_words);
1529 function iframe_whitelisted($entry) {
1530 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1532 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1535 foreach ($whitelist as $w) {
1536 if ($src == $w ||
$src == "www.$w")
1544 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1545 if (!$owner) $owner = $_SESSION["uid"];
1547 $res = trim($str); if (!$res) return '';
1549 $charset_hack = '<head>
1550 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1553 $res = trim($res); if (!$res) return '';
1555 libxml_use_internal_errors(true);
1557 $doc = new DOMDocument();
1558 $doc->loadHTML($charset_hack . $res);
1559 $xpath = new DOMXPath($doc);
1561 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1563 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1565 foreach ($entries as $entry) {
1567 if ($entry->hasAttribute('href')) {
1568 $entry->setAttribute('href',
1569 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1571 $entry->setAttribute('rel', 'noopener noreferrer');
1574 if ($entry->hasAttribute('src')) {
1575 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1576 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1578 if (file_exists($cached_filename)) {
1580 // this is strictly cosmetic
1581 if ($entry->tagName
== 'img') {
1583 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1585 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1591 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1593 if ($entry->hasAttribute('srcset')) {
1594 $entry->removeAttribute('srcset');
1597 if ($entry->hasAttribute('sizes')) {
1598 $entry->removeAttribute('sizes');
1602 $entry->setAttribute('src', $src);
1605 if ($entry->nodeName
== 'img') {
1607 if ($entry->hasAttribute('src')) {
1608 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1610 if (is_prefix_https() && !$is_https_url) {
1612 if ($entry->hasAttribute('srcset')) {
1613 $entry->removeAttribute('srcset');
1616 if ($entry->hasAttribute('sizes')) {
1617 $entry->removeAttribute('sizes');
1622 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1623 $force_remove_images ||
$_SESSION["bw_limit"]) {
1625 $p = $doc->createElement('p');
1627 $a = $doc->createElement('a');
1628 $a->setAttribute('href', $entry->getAttribute('src'));
1630 $a->appendChild(new DOMText($entry->getAttribute('src')));
1631 $a->setAttribute('target', '_blank');
1632 $a->setAttribute('rel', 'noopener noreferrer');
1634 $p->appendChild($a);
1636 $entry->parentNode
->replaceChild($p, $entry);
1640 if (strtolower($entry->nodeName
) == "a") {
1641 $entry->setAttribute("target", "_blank");
1642 $entry->setAttribute("rel", "noopener noreferrer");
1646 $entries = $xpath->query('//iframe');
1647 foreach ($entries as $entry) {
1648 if (!iframe_whitelisted($entry)) {
1649 $entry->setAttribute('sandbox', 'allow-scripts');
1651 if (is_prefix_https()) {
1652 $entry->setAttribute("src",
1653 str_replace("http://", "https://",
1654 $entry->getAttribute("src")));
1659 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1660 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1661 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1662 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1663 'dt', 'em', 'footer', 'figure', 'figcaption',
1664 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'html', 'i',
1665 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1666 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1667 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1668 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1669 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1671 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1673 $disallowed_attributes = array('id', 'style', 'class');
1675 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1676 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1677 if (is_array($retval)) {
1679 $allowed_elements = $retval[1];
1680 $disallowed_attributes = $retval[2];
1686 $doc->removeChild($doc->firstChild
); //remove doctype
1687 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1689 if ($highlight_words) {
1690 foreach ($highlight_words as $word) {
1692 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1694 $elements = $xpath->query("//*/text()");
1696 foreach ($elements as $child) {
1698 $fragment = $doc->createDocumentFragment();
1699 $text = $child->textContent
;
1701 while (($pos = mb_stripos($text, $word)) !== false) {
1702 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1703 $word = mb_substr($text, $pos, mb_strlen($word));
1704 $highlight = $doc->createElement('span');
1705 $highlight->appendChild(new DomText($word));
1706 $highlight->setAttribute('class', 'highlight');
1707 $fragment->appendChild($highlight);
1708 $text = mb_substr($text, $pos +
mb_strlen($word));
1711 if (!empty($text)) $fragment->appendChild(new DomText($text));
1713 $child->parentNode
->replaceChild($fragment, $child);
1718 $res = $doc->saveHTML();
1720 /* strip everything outside of <body>...</body> */
1722 $res_frag = array();
1723 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1724 return $res_frag[1];
1730 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1731 $xpath = new DOMXPath($doc);
1732 $entries = $xpath->query('//*');
1734 foreach ($entries as $entry) {
1735 if (!in_array($entry->nodeName
, $allowed_elements)) {
1736 $entry->parentNode
->removeChild($entry);
1739 if ($entry->hasAttributes()) {
1740 $attrs_to_remove = array();
1742 foreach ($entry->attributes
as $attr) {
1744 if (strpos($attr->nodeName
, 'on') === 0) {
1745 array_push($attrs_to_remove, $attr);
1748 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1749 array_push($attrs_to_remove, $attr);
1752 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1753 array_push($attrs_to_remove, $attr);
1757 foreach ($attrs_to_remove as $attr) {
1758 $entry->removeAttributeNode($attr);
1766 function trim_array($array) {
1768 array_walk($tmp, 'trim');
1772 function tag_is_valid($tag) {
1773 if ($tag == '') return false;
1774 if (is_numeric($tag)) return false;
1775 if (mb_strlen($tag) > 250) return false;
1777 if (!$tag) return false;
1782 function render_login_form() {
1783 header('Cache-Control: public');
1785 require_once "login_form.php";
1789 function T_sprintf() {
1790 $args = func_get_args();
1791 return vsprintf(__(array_shift($args)), $args);
1794 function print_checkpoint($n, $s) {
1795 $ts = microtime(true);
1796 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1800 function sanitize_tag($tag) {
1803 $tag = mb_strtolower($tag, 'utf-8');
1805 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1807 if (DB_TYPE
== "mysql") {
1808 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1814 function is_server_https() {
1815 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1818 function is_prefix_https() {
1819 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1822 // this returns SELF_URL_PATH sans ending slash
1823 function get_self_url_prefix() {
1824 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1825 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1827 return SELF_URL_PATH
;
1831 function encrypt_password($pass, $salt = '', $mode2 = false) {
1832 if ($salt && $mode2) {
1833 return "MODE2:" . hash('sha256', $salt . $pass);
1835 return "SHA1X:" . sha1("$salt:$pass");
1837 return "SHA1:" . sha1($pass);
1839 } // function encrypt_password
1841 function load_filters($feed_id, $owner_uid) {
1844 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1847 $null_cat_qpart = "cat_id IS NULL OR";
1849 $null_cat_qpart = "";
1851 $result = db_query("SELECT * FROM ttrss_filters2 WHERE
1852 owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title");
1854 $check_cats = array_merge(
1855 Feeds
::getParentCategories($cat_id, $owner_uid),
1858 $check_cats_str = join(",", $check_cats);
1859 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1861 while ($line = db_fetch_assoc($result)) {
1862 $filter_id = $line["id"];
1864 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1866 $result2 = db_query("SELECT
1867 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1868 FROM ttrss_filters2_rules AS r,
1869 ttrss_filter_types AS t
1871 (match_on IS NOT NULL OR
1872 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1873 (feed_id IS NULL OR feed_id = '$feed_id'))) AND
1874 filter_type = t.id AND filter_id = '$filter_id'");
1879 while ($rule_line = db_fetch_assoc($result2)) {
1880 # print_r($rule_line);
1882 if ($rule_line["match_on"]) {
1883 $match_on = json_decode($rule_line["match_on"], true);
1885 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1888 $rule["reg_exp"] = $rule_line["reg_exp"];
1889 $rule["type"] = $rule_line["type_name"];
1890 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1892 array_push($rules, $rule);
1893 } else if (!$match_any_rule) {
1894 // this filter contains a rule that doesn't match to this feed/category combination
1895 // thus filter has to be rejected
1904 $rule["reg_exp"] = $rule_line["reg_exp"];
1905 $rule["type"] = $rule_line["type_name"];
1906 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1908 array_push($rules, $rule);
1912 if (count($rules) > 0) {
1913 $result2 = db_query("SELECT a.action_param,t.name AS type_name
1914 FROM ttrss_filters2_actions AS a,
1915 ttrss_filter_actions AS t
1917 action_id = t.id AND filter_id = '$filter_id'");
1919 while ($action_line = db_fetch_assoc($result2)) {
1920 # print_r($action_line);
1923 $action["type"] = $action_line["type_name"];
1924 $action["param"] = $action_line["action_param"];
1926 array_push($actions, $action);
1931 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1932 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1933 $filter["rules"] = $rules;
1934 $filter["actions"] = $actions;
1936 if (count($rules) > 0 && count($actions) > 0) {
1937 array_push($filters, $filter);
1944 function get_score_pic($score) {
1946 return "score_high.png";
1947 } else if ($score > 0) {
1948 return "score_half_high.png";
1949 } else if ($score < -100) {
1950 return "score_low.png";
1951 } else if ($score < 0) {
1952 return "score_half_low.png";
1954 return "score_neutral.png";
1958 function feed_has_icon($id) {
1959 return is_file(ICONS_DIR
. "/$id.ico") && filesize(ICONS_DIR
. "/$id.ico") > 0;
1962 function init_plugins() {
1963 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1968 function add_feed_category($feed_cat, $parent_cat_id = false) {
1970 if (!$feed_cat) return false;
1974 if ($parent_cat_id) {
1975 $parent_qpart = "parent_cat = '$parent_cat_id'";
1976 $parent_insert = "'$parent_cat_id'";
1978 $parent_qpart = "parent_cat IS NULL";
1979 $parent_insert = "NULL";
1982 $feed_cat = mb_substr($feed_cat, 0, 250);
1985 "SELECT id FROM ttrss_feed_categories
1986 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1988 if (db_num_rows($result) == 0) {
1991 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1992 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
2003 * Fixes incomplete URLs by prepending "http://".
2004 * Also replaces feed:// with http://, and
2005 * prepends a trailing slash if the url is a domain name only.
2007 * @param string $url Possibly incomplete URL
2009 * @return string Fixed URL.
2011 function fix_url($url) {
2013 // support schema-less urls
2014 if (strpos($url, '//') === 0) {
2015 $url = 'https:' . $url;
2018 if (strpos($url, '://') === false) {
2019 $url = 'http://' . $url;
2020 } else if (substr($url, 0, 5) == 'feed:') {
2021 $url = 'http:' . substr($url, 5);
2024 //prepend slash if the URL has no slash in it
2025 // "http://www.example" -> "http://www.example/"
2026 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2030 //convert IDNA hostname to punycode if possible
2031 if (function_exists("idn_to_ascii")) {
2032 $parts = parse_url($url);
2033 if (mb_detect_encoding($parts['host']) != 'ASCII')
2035 $parts['host'] = idn_to_ascii($parts['host']);
2036 $url = build_url($parts);
2040 if ($url != "http:///")
2046 function validate_feed_url($url) {
2047 $parts = parse_url($url);
2049 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2053 /* function save_email_address($email) {
2054 // FIXME: implement persistent storage of emails
2056 if (!$_SESSION['stored_emails'])
2057 $_SESSION['stored_emails'] = array();
2059 if (!in_array($email, $_SESSION['stored_emails']))
2060 array_push($_SESSION['stored_emails'], $email);
2064 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2066 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2068 $sql_is_cat = bool_to_sql_bool($is_cat);
2070 $result = db_query("SELECT access_key FROM ttrss_access_keys
2071 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
2072 AND owner_uid = " . $owner_uid);
2074 if (db_num_rows($result) == 1) {
2075 return db_fetch_result($result, 0, "access_key");
2077 $key = db_escape_string(uniqid_short());
2079 $result = db_query("INSERT INTO ttrss_access_keys
2080 (access_key, feed_id, is_cat, owner_uid)
2081 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
2088 function get_feeds_from_html($url, $content)
2090 $url = fix_url($url);
2091 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2093 libxml_use_internal_errors(true);
2095 $doc = new DOMDocument();
2096 $doc->loadHTML($content);
2097 $xpath = new DOMXPath($doc);
2098 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2099 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2100 $feedUrls = array();
2101 foreach ($entries as $entry) {
2102 if ($entry->hasAttribute('href')) {
2103 $title = $entry->getAttribute('title');
2105 $title = $entry->getAttribute('type');
2107 $feedUrl = rewrite_relative_url(
2108 $baseUrl, $entry->getAttribute('href')
2110 $feedUrls[$feedUrl] = $title;
2116 function is_html($content) {
2117 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2120 function url_is_html($url, $login = false, $pass = false) {
2121 return is_html(fetch_file_contents($url, false, $login, $pass));
2124 function build_url($parts) {
2125 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2128 function cleanup_url_path($path) {
2129 $path = str_replace("/./", "/", $path);
2130 $path = str_replace("//", "/", $path);
2136 * Converts a (possibly) relative URL to a absolute one.
2138 * @param string $url Base URL (i.e. from where the document is)
2139 * @param string $rel_url Possibly relative URL in the document
2141 * @return string Absolute URL
2143 function rewrite_relative_url($url, $rel_url) {
2144 if (strpos($rel_url, "://") !== false) {
2146 } else if (strpos($rel_url, "//") === 0) {
2147 # protocol-relative URL (rare but they exist)
2149 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2150 # magnet:, feed:, etc
2152 } else if (strpos($rel_url, "/") === 0) {
2153 $parts = parse_url($url);
2154 $parts['path'] = $rel_url;
2155 $parts['path'] = cleanup_url_path($parts['path']);
2157 return build_url($parts);
2160 $parts = parse_url($url);
2161 if (!isset($parts['path'])) {
2162 $parts['path'] = '/';
2164 $dir = $parts['path'];
2165 if (substr($dir, -1) !== '/') {
2166 $dir = dirname($parts['path']);
2167 $dir !== '/' && $dir .= '/';
2169 $parts['path'] = $dir . $rel_url;
2170 $parts['path'] = cleanup_url_path($parts['path']);
2172 return build_url($parts);
2176 function cleanup_tags($days = 14, $limit = 1000) {
2178 if (DB_TYPE
== "pgsql") {
2179 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2180 } else if (DB_TYPE
== "mysql") {
2181 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2186 while ($limit > 0) {
2189 $query = "SELECT ttrss_tags.id AS id
2190 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2191 WHERE post_int_id = int_id AND $interval_query AND
2192 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2194 $result = db_query($query);
2198 while ($line = db_fetch_assoc($result)) {
2199 array_push($ids, $line['id']);
2202 if (count($ids) > 0) {
2203 $ids = join(",", $ids);
2205 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2206 $tags_deleted +
= db_affected_rows($tmp_result);
2211 $limit -= $limit_part;
2214 return $tags_deleted;
2217 function print_user_stylesheet() {
2218 $value = get_pref('USER_STYLESHEET');
2221 print "<style type=\"text/css\">";
2222 print str_replace("<br/>", "\n", $value);
2228 function filter_to_sql($filter, $owner_uid) {
2231 if (DB_TYPE
== "pgsql")
2234 $reg_qpart = "REGEXP";
2236 foreach ($filter["rules"] AS $rule) {
2237 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2238 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2239 $rule['reg_exp']) !== FALSE;
2241 if ($regexp_valid) {
2243 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2245 switch ($rule["type"]) {
2247 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2248 $rule['reg_exp'] . "')";
2251 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2252 $rule['reg_exp'] . "')";
2255 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2256 $rule['reg_exp'] . "') OR LOWER(" .
2257 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2260 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2261 $rule['reg_exp'] . "')";
2264 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2265 $rule['reg_exp'] . "')";
2268 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2269 $rule['reg_exp'] . "')";
2273 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2275 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2276 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2279 if (isset($rule["cat_id"])) {
2281 if ($rule["cat_id"] > 0) {
2282 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2283 array_push($children, $rule["cat_id"]);
2285 $children = join(",", $children);
2287 $cat_qpart = "cat_id IN ($children)";
2289 $cat_qpart = "cat_id IS NULL";
2292 $qpart .= " AND $cat_qpart";
2295 $qpart .= " AND feed_id IS NOT NULL";
2297 array_push($query, "($qpart)");
2302 if (count($query) > 0) {
2303 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2305 $fullquery = "(false)";
2308 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2313 if (!function_exists('gzdecode')) {
2314 function gzdecode($string) { // no support for 2nd argument
2315 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2316 base64_encode($string));
2320 function get_random_bytes($length) {
2321 if (function_exists('openssl_random_pseudo_bytes')) {
2322 return openssl_random_pseudo_bytes($length);
2326 for ($i = 0; $i < $length; $i++
)
2327 $output .= chr(mt_rand(0, 255));
2333 function read_stdin() {
2334 $fp = fopen("php://stdin", "r");
2337 $line = trim(fgets($fp));
2345 function implements_interface($class, $interface) {
2346 return in_array($interface, class_implements($class));
2349 function get_minified_js($files) {
2350 require_once 'lib/jshrink/Minifier.php';
2354 foreach ($files as $js) {
2355 if (!isset($_GET['debug'])) {
2356 $cached_file = CACHE_DIR
. "/js/".basename($js).".js";
2358 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2360 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2362 if ($header && $contents) {
2363 list($htag, $hversion) = explode(":", $header);
2365 if ($htag == "tt-rss" && $hversion == VERSION
) {
2372 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js.js"));
2373 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2377 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2384 function calculate_dep_timestamp() {
2385 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2389 foreach ($files as $file) {
2390 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2396 function T_js_decl($s1, $s2) {
2398 $s1 = preg_replace("/\n/", "", $s1);
2399 $s2 = preg_replace("/\n/", "", $s2);
2401 $s1 = preg_replace("/\"/", "\\\"", $s1);
2402 $s2 = preg_replace("/\"/", "\\\"", $s2);
2404 return "T_messages[\"$s1\"] = \"$s2\";\n";
2408 function init_js_translations() {
2410 print 'var T_messages = new Object();
2413 if (T_messages[msg]) {
2414 return T_messages[msg];
2420 function ngettext(msg1, msg2, n) {
2421 return __((parseInt(n) > 1) ? msg2 : msg1);
2424 $l10n = _get_reader();
2426 for ($i = 0; $i < $l10n->total
; $i++
) {
2427 $orig = $l10n->get_original_string($i);
2428 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2429 $key = explode(chr(0), $orig);
2430 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2431 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2433 $translation = __($orig);
2434 print T_js_decl($orig, $translation);
2439 function get_theme_path($theme) {
2440 $check = "themes/$theme";
2441 if (file_exists($check)) return $check;
2443 $check = "themes.local/$theme";
2444 if (file_exists($check)) return $check;
2447 function theme_valid($theme) {
2448 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2450 if (in_array($theme, $bundled_themes)) return true;
2452 $file = "themes/" . basename($theme);
2454 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2456 if (file_exists($file) && is_readable($file)) {
2457 $fh = fopen($file, "r");
2460 $header = fgets($fh);
2463 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2471 * @SuppressWarnings(unused)
2473 function error_json($code) {
2474 require_once "errors.php";
2476 @$message = $ERRORS[$code];
2478 return json_encode(array("error" =>
2479 array("code" => $code, "message" => $message)));
2483 /*function abs_to_rel_path($dir) {
2484 $tmp = str_replace(dirname(__DIR__), "", $dir);
2486 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2491 function get_upload_error_message($code) {
2494 0 => __('There is no error, the file uploaded with success'),
2495 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2496 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2497 3 => __('The uploaded file was only partially uploaded'),
2498 4 => __('No file was uploaded'),
2499 6 => __('Missing a temporary folder'),
2500 7 => __('Failed to write file to disk.'),
2501 8 => __('A PHP extension stopped the file upload.'),
2504 return $errors[$code];
2507 function base64_img($filename) {
2508 if (file_exists($filename)) {
2509 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2511 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2517 /* this is essentially a wrapper for readfile() which allows plugins to hook
2518 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2520 hook function should return true if request was handled (or at least attempted to)
2522 note that this can be called without user context so the plugin to handle this
2523 should be loaded systemwide in config.php */
2524 function send_local_file($filename) {
2525 if (file_exists($filename)) {
2526 $tmppluginhost = new PluginHost();
2528 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2529 $tmppluginhost->load_data();
2531 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2532 if ($plugin->hook_send_local_file($filename)) return true;
2535 $mimetype = mime_content_type($filename);
2536 header("Content-type: $mimetype");
2538 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2539 header("Last-Modified: $stamp", true);
2541 return readfile($filename);