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 if (strstr($header, ": ") !== FALSE) {
436 list ($key, $value) = explode(": ", $header);
438 if (strtolower($key) == "last-modified") {
439 $fetch_last_modified = $value;
443 if (substr(strtolower($header), 0, 7) == 'http/1.') {
444 $fetch_last_error_code = (int) substr($header, 9, 3);
445 $fetch_last_error = $header;
449 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
450 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
451 $contents = @curl_exec
($ch);
454 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
455 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
457 $fetch_last_error_code = $http_code;
459 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
461 if (curl_errno($ch) != 0) {
462 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
465 $fetch_last_error_content = $contents;
471 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
481 $fetch_curl_used = false;
483 if ($login && $pass){
484 $url_parts = array();
486 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
488 $pass = urlencode($pass);
490 if ($url_parts[1] && $url_parts[2]) {
491 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
495 // TODO: should this support POST requests or not? idk
497 if (!$post_query && $last_modified) {
498 $context = stream_context_create(array(
501 'ignore_errors' => true,
502 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
503 'protocol_version'=> 1.1,
504 'header' => "If-Modified-Since: $last_modified\r\n")
507 $context = stream_context_create(array(
510 'ignore_errors' => true,
511 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
512 'protocol_version'=> 1.1
516 $old_error = error_get_last();
518 $data = @file_get_contents
($url, false, $context);
520 if (isset($http_response_header) && is_array($http_response_header)) {
521 foreach ($http_response_header as $header) {
522 if (strstr($header, ": ") !== FALSE) {
523 list ($key, $value) = explode(": ", $header);
525 $key = strtolower($key);
527 if ($key == 'content-type') {
528 $fetch_last_content_type = $value;
529 // don't abort here b/c there might be more than one
530 // e.g. if we were being redirected -- last one is the right one
531 } else if ($key == 'last-modified') {
532 $fetch_last_modified = $value;
536 if (substr(strtolower($header), 0, 7) == 'http/1.') {
537 $fetch_last_error_code = (int) substr($header, 9, 3);
538 $fetch_last_error = $header;
543 if ($fetch_last_error_code != 200) {
544 $error = error_get_last();
546 if ($error['message'] != $old_error['message']) {
547 $fetch_last_error .= "; " . $error["message"];
550 $fetch_last_error_content = $data;
560 * Try to determine the favicon URL for a feed.
561 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
562 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
564 * @param string $url A feed or page URL
566 * @return mixed The favicon URL, or false if none was found.
568 function get_favicon_url($url) {
570 $favicon_url = false;
572 if ($html = @fetch_file_contents
($url)) {
574 libxml_use_internal_errors(true);
576 $doc = new DOMDocument();
577 $doc->loadHTML($html);
578 $xpath = new DOMXPath($doc);
580 $base = $xpath->query('/html/head/base[@href]');
581 foreach ($base as $b) {
582 $url = rewrite_relative_url($url, $b->getAttribute("href"));
586 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
587 if (count($entries) > 0) {
588 foreach ($entries as $entry) {
589 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
596 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
599 } // function get_favicon_url
601 function initialize_user_prefs($uid, $profile = false) {
603 $uid = db_escape_string($uid);
607 $profile_qpart = "AND profile IS NULL";
609 $profile_qpart = "AND profile = '$profile'";
612 if (get_schema_version() < 63) $profile_qpart = "";
616 $result = db_query("SELECT pref_name,def_value FROM ttrss_prefs");
618 $u_result = db_query("SELECT pref_name
619 FROM ttrss_user_prefs WHERE owner_uid = '$uid' $profile_qpart");
621 $active_prefs = array();
623 while ($line = db_fetch_assoc($u_result)) {
624 array_push($active_prefs, $line["pref_name"]);
627 while ($line = db_fetch_assoc($result)) {
628 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
629 // print "adding " . $line["pref_name"] . "<br>";
631 $line["def_value"] = db_escape_string($line["def_value"]);
632 $line["pref_name"] = db_escape_string($line["pref_name"]);
634 if (get_schema_version() < 63) {
635 db_query("INSERT INTO ttrss_user_prefs
636 (owner_uid,pref_name,value) VALUES
637 ('$uid', '".$line["pref_name"]."','".$line["def_value"]."')");
640 db_query("INSERT INTO ttrss_user_prefs
641 (owner_uid,pref_name,value, profile) VALUES
642 ('$uid', '".$line["pref_name"]."','".$line["def_value"]."', $profile)");
652 function get_ssl_certificate_id() {
653 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
654 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
655 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
656 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
657 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
659 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
660 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
661 $_SERVER["SSL_CLIENT_V_START"] .
662 $_SERVER["SSL_CLIENT_V_END"] .
663 $_SERVER["SSL_CLIENT_S_DN"]);
668 function authenticate_user($login, $password, $check_only = false) {
670 if (!SINGLE_USER_MODE
) {
673 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
675 $user_id = (int) $plugin->authenticate($login, $password);
678 $_SESSION["auth_module"] = strtolower(get_class($plugin));
683 if ($user_id && !$check_only) {
686 $_SESSION["uid"] = $user_id;
687 $_SESSION["version"] = VERSION_STATIC
;
689 $result = db_query("SELECT login,access_level,pwd_hash FROM ttrss_users
690 WHERE id = '$user_id'");
692 $_SESSION["name"] = db_fetch_result($result, 0, "login");
693 $_SESSION["access_level"] = db_fetch_result($result, 0, "access_level");
694 $_SESSION["csrf_token"] = uniqid_short();
696 db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
699 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
700 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
701 $_SESSION["pwd_hash"] = db_fetch_result($result, 0, "pwd_hash");
703 $_SESSION["last_version_check"] = time();
705 initialize_user_prefs($_SESSION["uid"]);
714 $_SESSION["uid"] = 1;
715 $_SESSION["name"] = "admin";
716 $_SESSION["access_level"] = 10;
718 $_SESSION["hide_hello"] = true;
719 $_SESSION["hide_logout"] = true;
721 $_SESSION["auth_module"] = false;
723 if (!$_SESSION["csrf_token"]) {
724 $_SESSION["csrf_token"] = uniqid_short();
727 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
729 initialize_user_prefs($_SESSION["uid"]);
735 function make_password($length = 8) {
738 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
742 while ($i < $length) {
743 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
745 if (!strstr($password, $char)) {
753 // this is called after user is created to initialize default feeds, labels
756 // user preferences are checked on every login, not here
758 function initialize_user($uid) {
760 db_query("insert into ttrss_feeds (owner_uid,title,feed_url)
761 values ('$uid', 'Tiny Tiny RSS: Forum',
762 'http://tt-rss.org/forum/rss.php')");
765 function logout_user() {
767 if (isset($_COOKIE[session_name()])) {
768 setcookie(session_name(), '', time()-42000, '/');
772 function validate_csrf($csrf_token) {
773 return $csrf_token == $_SESSION['csrf_token'];
776 function load_user_plugins($owner_uid, $pluginhost = false) {
778 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
780 if ($owner_uid && SCHEMA_VERSION
>= 100) {
781 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
783 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
785 if (get_schema_version() > 100) {
786 $pluginhost->load_data();
791 function login_sequence() {
792 if (SINGLE_USER_MODE
) {
794 authenticate_user("admin", null);
796 load_user_plugins($_SESSION["uid"]);
798 if (!validate_session()) $_SESSION["uid"] = false;
800 if (!$_SESSION["uid"]) {
802 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
803 $_SESSION["ref_schema_version"] = get_schema_version(true);
805 authenticate_user(null, null, true);
808 if (!$_SESSION["uid"]) {
810 setcookie(session_name(), '', time()-42000, '/');
817 /* bump login timestamp */
818 db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
820 $_SESSION["last_login_update"] = time();
823 if ($_SESSION["uid"]) {
825 load_user_plugins($_SESSION["uid"]);
829 db_query("DELETE FROM ttrss_counters_cache WHERE owner_uid = ".
830 $_SESSION["uid"] . " AND
831 (SELECT COUNT(id) FROM ttrss_feeds WHERE
832 ttrss_feeds.id = feed_id) = 0");
834 db_query("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ".
835 $_SESSION["uid"] . " AND
836 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
837 ttrss_feed_categories.id = feed_id) = 0");
844 function truncate_string($str, $max_len, $suffix = '…') {
845 if (mb_strlen($str, "utf-8") > $max_len) {
846 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
853 function truncate_middle($str, $max_len, $suffix = '…') {
854 if (strlen($str) > $max_len) {
855 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
861 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
864 $source_tz = new DateTimeZone($source_tz);
865 } catch (Exception
$e) {
866 $source_tz = new DateTimeZone('UTC');
870 $dest_tz = new DateTimeZone($dest_tz);
871 } catch (Exception
$e) {
872 $dest_tz = new DateTimeZone('UTC');
875 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
876 return $dt->format('U') +
$dest_tz->getOffset($dt);
879 function make_local_datetime($timestamp, $long, $owner_uid = false,
880 $no_smart_dt = false, $eta_min = false) {
882 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
883 if (!$timestamp) $timestamp = '1970-01-01 0:00';
888 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
890 $timestamp = substr($timestamp, 0, 19);
892 # We store date in UTC internally
893 $dt = new DateTime($timestamp, $utc_tz);
895 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
897 if ($user_tz_string != 'Automatic') {
900 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
901 } catch (Exception
$e) {
905 $tz_offset = $user_tz->getOffset($dt);
907 $tz_offset = (int) -$_SESSION["clientTzOffset"];
910 $user_timestamp = $dt->format('U') +
$tz_offset;
913 return smart_date_time($user_timestamp,
914 $tz_offset, $owner_uid, $eta_min);
917 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
919 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
921 return date($format, $user_timestamp);
925 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
926 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
928 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
929 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
930 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
931 return date("G:i", $timestamp);
932 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
933 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
934 return date($format, $timestamp);
936 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
937 return date($format, $timestamp);
941 function sql_bool_to_bool($s) {
942 if ($s == "t" ||
$s == "1" ||
strtolower($s) == "true") {
949 function bool_to_sql_bool($s) {
957 // Session caching removed due to causing wrong redirects to upgrade
958 // script when get_schema_version() is called on an obsolete session
959 // created on a previous schema version.
960 function get_schema_version($nocache = false) {
961 global $schema_version;
963 if (!$schema_version && !$nocache) {
964 $result = db_query("SELECT schema_version FROM ttrss_version");
965 $version = db_fetch_result($result, 0, "schema_version");
966 $schema_version = $version;
969 return $schema_version;
973 function sanity_check() {
974 require_once 'errors.php';
978 $schema_version = get_schema_version(true);
980 if ($schema_version != SCHEMA_VERSION
) {
984 if (DB_TYPE
== "mysql") {
985 $result = db_query("SELECT true", false);
986 if (db_num_rows($result) != 1) {
991 if (db_escape_string("testTEST") != "testTEST") {
995 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
998 function file_is_locked($filename) {
999 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
1000 if (function_exists('flock')) {
1001 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
1003 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1004 flock($fp, LOCK_UN
);
1014 return true; // consider the file always locked and skip the test
1021 function make_lockfile($filename) {
1022 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1024 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
1025 $stat_h = fstat($fp);
1026 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
1028 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
1029 if ($stat_h["ino"] != $stat_f["ino"] ||
1030 $stat_h["dev"] != $stat_f["dev"]) {
1036 if (function_exists('posix_getpid')) {
1037 fwrite($fp, posix_getpid() . "\n");
1045 function make_stampfile($filename) {
1046 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1048 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1049 fwrite($fp, time() . "\n");
1050 flock($fp, LOCK_UN
);
1058 function sql_random_function() {
1059 if (DB_TYPE
== "mysql") {
1066 function getFeedUnread($feed, $is_cat = false) {
1067 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1071 /*function get_pgsql_version() {
1072 $result = db_query("SELECT version() AS version");
1073 $version = explode(" ", db_fetch_result($result, 0, "version"));
1077 function checkbox_to_sql_bool($val) {
1078 return ($val == "on") ?
"true" : "false";
1081 /*function getFeedCatTitle($id) {
1083 return __("Special");
1084 } else if ($id < LABEL_BASE_INDEX) {
1085 return __("Labels");
1086 } else if ($id > 0) {
1087 $result = db_query("SELECT ttrss_feed_categories.title
1088 FROM ttrss_feeds, ttrss_feed_categories WHERE ttrss_feeds.id = '$id' AND
1089 cat_id = ttrss_feed_categories.id");
1090 if (db_num_rows($result) == 1) {
1091 return db_fetch_result($result, 0, "title");
1093 return __("Uncategorized");
1096 return "getFeedCatTitle($id) failed";
1101 function uniqid_short() {
1102 return uniqid(base_convert(rand(), 10, 36));
1105 function make_init_params() {
1108 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1109 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1110 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1111 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1113 $params[strtolower($param)] = (int) get_pref($param);
1116 $params["icons_url"] = ICONS_URL
;
1117 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1118 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1119 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1120 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1121 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1122 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1124 $theme = get_pref( "USER_CSS_THEME", false, false);
1125 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1127 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1129 $params["php_platform"] = PHP_OS
;
1130 $params["php_version"] = PHP_VERSION
;
1132 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1134 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1135 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
1137 $max_feed_id = db_fetch_result($result, 0, "mid");
1138 $num_feeds = db_fetch_result($result, 0, "nf");
1140 $params["max_feed_id"] = (int) $max_feed_id;
1141 $params["num_feeds"] = (int) $num_feeds;
1143 $params["hotkeys"] = get_hotkeys_map();
1145 $params["csrf_token"] = $_SESSION["csrf_token"];
1146 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1148 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1150 $params["icon_alert"] = base64_img("images/alert.png");
1151 $params["icon_information"] = base64_img("images/information.png");
1152 $params["icon_cross"] = base64_img("images/cross.png");
1153 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1155 $params["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1160 function get_hotkeys_info() {
1162 __("Navigation") => array(
1163 "next_feed" => __("Open next feed"),
1164 "prev_feed" => __("Open previous feed"),
1165 "next_article" => __("Open next article"),
1166 "prev_article" => __("Open previous article"),
1167 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1168 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1169 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1170 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1171 "search_dialog" => __("Show search dialog")),
1172 __("Article") => array(
1173 "toggle_mark" => __("Toggle starred"),
1174 "toggle_publ" => __("Toggle published"),
1175 "toggle_unread" => __("Toggle unread"),
1176 "edit_tags" => __("Edit tags"),
1177 "open_in_new_window" => __("Open in new window"),
1178 "catchup_below" => __("Mark below as read"),
1179 "catchup_above" => __("Mark above as read"),
1180 "article_scroll_down" => __("Scroll down"),
1181 "article_scroll_up" => __("Scroll up"),
1182 "select_article_cursor" => __("Select article under cursor"),
1183 "email_article" => __("Email article"),
1184 "close_article" => __("Close/collapse article"),
1185 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1186 "toggle_widescreen" => __("Toggle widescreen mode"),
1187 "toggle_embed_original" => __("Toggle embed original")),
1188 __("Article selection") => array(
1189 "select_all" => __("Select all articles"),
1190 "select_unread" => __("Select unread"),
1191 "select_marked" => __("Select starred"),
1192 "select_published" => __("Select published"),
1193 "select_invert" => __("Invert selection"),
1194 "select_none" => __("Deselect everything")),
1195 __("Feed") => array(
1196 "feed_refresh" => __("Refresh current feed"),
1197 "feed_unhide_read" => __("Un/hide read feeds"),
1198 "feed_subscribe" => __("Subscribe to feed"),
1199 "feed_edit" => __("Edit feed"),
1200 "feed_catchup" => __("Mark as read"),
1201 "feed_reverse" => __("Reverse headlines"),
1202 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1203 "feed_debug_update" => __("Debug feed update"),
1204 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1205 "catchup_all" => __("Mark all feeds as read"),
1206 "cat_toggle_collapse" => __("Un/collapse current category"),
1207 "toggle_combined_mode" => __("Toggle combined mode"),
1208 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1209 __("Go to") => array(
1210 "goto_all" => __("All articles"),
1211 "goto_fresh" => __("Fresh"),
1212 "goto_marked" => __("Starred"),
1213 "goto_published" => __("Published"),
1214 "goto_tagcloud" => __("Tag cloud"),
1215 "goto_prefs" => __("Preferences")),
1216 __("Other") => array(
1217 "create_label" => __("Create label"),
1218 "create_filter" => __("Create filter"),
1219 "collapse_sidebar" => __("Un/collapse sidebar"),
1220 "help_dialog" => __("Show help dialog"))
1223 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1224 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1230 function get_hotkeys_map() {
1232 // "navigation" => array(
1235 "n" => "next_article",
1236 "p" => "prev_article",
1237 "(38)|up" => "prev_article",
1238 "(40)|down" => "next_article",
1239 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1240 // "^(40)|Ctrl-down" => "next_article_noscroll",
1241 "(191)|/" => "search_dialog",
1242 // "article" => array(
1243 "s" => "toggle_mark",
1244 "*s" => "toggle_publ",
1245 "u" => "toggle_unread",
1246 "*t" => "edit_tags",
1247 "o" => "open_in_new_window",
1248 "c p" => "catchup_below",
1249 "c n" => "catchup_above",
1250 "*n" => "article_scroll_down",
1251 "*p" => "article_scroll_up",
1252 "*(38)|Shift+up" => "article_scroll_up",
1253 "*(40)|Shift+down" => "article_scroll_down",
1254 "a *w" => "toggle_widescreen",
1255 "a e" => "toggle_embed_original",
1256 "e" => "email_article",
1257 "a q" => "close_article",
1258 // "article_selection" => array(
1259 "a a" => "select_all",
1260 "a u" => "select_unread",
1261 "a *u" => "select_marked",
1262 "a p" => "select_published",
1263 "a i" => "select_invert",
1264 "a n" => "select_none",
1266 "f r" => "feed_refresh",
1267 "f a" => "feed_unhide_read",
1268 "f s" => "feed_subscribe",
1269 "f e" => "feed_edit",
1270 "f q" => "feed_catchup",
1271 "f x" => "feed_reverse",
1272 "f g" => "feed_toggle_vgroup",
1273 "f *d" => "feed_debug_update",
1274 "f *g" => "feed_debug_viewfeed",
1275 "f *c" => "toggle_combined_mode",
1276 "f c" => "toggle_cdm_expanded",
1277 "*q" => "catchup_all",
1278 "x" => "cat_toggle_collapse",
1280 "g a" => "goto_all",
1281 "g f" => "goto_fresh",
1282 "g s" => "goto_marked",
1283 "g p" => "goto_published",
1284 "g t" => "goto_tagcloud",
1285 "g *p" => "goto_prefs",
1286 // "other" => array(
1287 "(9)|Tab" => "select_article_cursor", // tab
1288 "c l" => "create_label",
1289 "c f" => "create_filter",
1290 "c s" => "collapse_sidebar",
1291 "^(191)|Ctrl+/" => "help_dialog",
1294 if (get_pref('COMBINED_DISPLAY_MODE')) {
1295 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1296 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1299 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1300 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1303 $prefixes = array();
1305 foreach (array_keys($hotkeys) as $hotkey) {
1306 $pair = explode(" ", $hotkey, 2);
1308 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1309 array_push($prefixes, $pair[0]);
1313 return array($prefixes, $hotkeys);
1316 function check_for_update() {
1317 if (defined("GIT_VERSION_TIMESTAMP")) {
1318 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1321 $content = json_decode($content, true);
1323 if ($content && isset($content["changeset"])) {
1324 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1325 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1327 return $content["changeset"]["id"];
1336 function make_runtime_info($disable_update_check = false) {
1339 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1340 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
1342 $max_feed_id = db_fetch_result($result, 0, "mid");
1343 $num_feeds = db_fetch_result($result, 0, "nf");
1345 $data["max_feed_id"] = (int) $max_feed_id;
1346 $data["num_feeds"] = (int) $num_feeds;
1348 $data['last_article_id'] = Article
::getLastArticleId();
1349 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1351 $data['dep_ts'] = calculate_dep_timestamp();
1352 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1354 $data["labels"] = Labels
::get_all_labels($_SESSION["uid"]);
1356 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1357 $update_result = @check_for_update
();
1359 $data["update_result"] = $update_result;
1361 $_SESSION["last_version_check"] = time();
1364 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1366 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1368 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1370 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1373 $stamp_delta = time() - $stamp;
1375 if ($stamp_delta > 1800) {
1379 $_SESSION["daemon_stamp_check"] = time();
1382 $data['daemon_stamp_ok'] = $stamp_check;
1384 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1386 $data['daemon_stamp'] = $stamp_fmt;
1394 function search_to_sql($search, $search_language) {
1396 $keywords = str_getcsv(trim($search), " ");
1397 $query_keywords = array();
1398 $search_words = array();
1399 $search_query_leftover = array();
1401 if ($search_language)
1402 $search_language = db_escape_string(mb_strtolower($search_language));
1404 $search_language = "english";
1406 foreach ($keywords as $k) {
1407 if (strpos($k, "-") === 0) {
1414 $commandpair = explode(":", mb_strtolower($k), 2);
1416 switch ($commandpair[0]) {
1418 if ($commandpair[1]) {
1419 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
1420 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1422 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1423 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1424 array_push($search_words, $k);
1428 if ($commandpair[1]) {
1429 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
1430 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1432 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1433 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1434 array_push($search_words, $k);
1438 if ($commandpair[1]) {
1439 if ($commandpair[1] == "true")
1440 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1441 else if ($commandpair[1] == "false")
1442 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1444 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
1445 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1447 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1448 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1449 if (!$not) array_push($search_words, $k);
1454 if ($commandpair[1]) {
1455 if ($commandpair[1] == "true")
1456 array_push($query_keywords, "($not (marked = true))");
1458 array_push($query_keywords, "($not (marked = false))");
1460 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1461 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1462 if (!$not) array_push($search_words, $k);
1466 if ($commandpair[1]) {
1467 if ($commandpair[1] == "true")
1468 array_push($query_keywords, "($not (published = true))");
1470 array_push($query_keywords, "($not (published = false))");
1473 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1474 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1475 if (!$not) array_push($search_words, $k);
1479 if ($commandpair[1]) {
1480 if ($commandpair[1] == "true")
1481 array_push($query_keywords, "($not (unread = true))");
1483 array_push($query_keywords, "($not (unread = false))");
1486 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1487 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1488 if (!$not) array_push($search_words, $k);
1492 if (strpos($k, "@") === 0) {
1494 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1495 $orig_ts = strtotime(substr($k, 1));
1496 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1498 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1500 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1503 if (DB_TYPE
== "pgsql") {
1504 $k = mb_strtolower($k);
1505 array_push($search_query_leftover, $not ?
"!$k" : $k);
1507 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1508 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1511 if (!$not) array_push($search_words, $k);
1516 if (count($search_query_leftover) > 0) {
1517 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
1519 if (DB_TYPE
== "pgsql") {
1520 array_push($query_keywords,
1521 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
1526 $search_query_part = implode("AND", $query_keywords);
1528 return array($search_query_part, $search_words);
1531 function iframe_whitelisted($entry) {
1532 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1534 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1537 foreach ($whitelist as $w) {
1538 if ($src == $w ||
$src == "www.$w")
1546 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1547 if (!$owner) $owner = $_SESSION["uid"];
1549 $res = trim($str); if (!$res) return '';
1551 $charset_hack = '<head>
1552 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1555 $res = trim($res); if (!$res) return '';
1557 libxml_use_internal_errors(true);
1559 $doc = new DOMDocument();
1560 $doc->loadHTML($charset_hack . $res);
1561 $xpath = new DOMXPath($doc);
1563 $rewrite_base_url = $site_url ?
$site_url : get_self_url_prefix();
1565 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1567 foreach ($entries as $entry) {
1569 if ($entry->hasAttribute('href')) {
1570 $entry->setAttribute('href',
1571 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1573 $entry->setAttribute('rel', 'noopener noreferrer');
1576 if ($entry->hasAttribute('src')) {
1577 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1578 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1580 if (file_exists($cached_filename)) {
1582 // this is strictly cosmetic
1583 if ($entry->tagName
== 'img') {
1585 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1587 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1593 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1595 if ($entry->hasAttribute('srcset')) {
1596 $entry->removeAttribute('srcset');
1599 if ($entry->hasAttribute('sizes')) {
1600 $entry->removeAttribute('sizes');
1604 $entry->setAttribute('src', $src);
1607 if ($entry->nodeName
== 'img') {
1609 if ($entry->hasAttribute('src')) {
1610 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1612 if (is_prefix_https() && !$is_https_url) {
1614 if ($entry->hasAttribute('srcset')) {
1615 $entry->removeAttribute('srcset');
1618 if ($entry->hasAttribute('sizes')) {
1619 $entry->removeAttribute('sizes');
1624 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1625 $force_remove_images ||
$_SESSION["bw_limit"]) {
1627 $p = $doc->createElement('p');
1629 $a = $doc->createElement('a');
1630 $a->setAttribute('href', $entry->getAttribute('src'));
1632 $a->appendChild(new DOMText($entry->getAttribute('src')));
1633 $a->setAttribute('target', '_blank');
1634 $a->setAttribute('rel', 'noopener noreferrer');
1636 $p->appendChild($a);
1638 $entry->parentNode
->replaceChild($p, $entry);
1642 if (strtolower($entry->nodeName
) == "a") {
1643 $entry->setAttribute("target", "_blank");
1644 $entry->setAttribute("rel", "noopener noreferrer");
1648 $entries = $xpath->query('//iframe');
1649 foreach ($entries as $entry) {
1650 if (!iframe_whitelisted($entry)) {
1651 $entry->setAttribute('sandbox', 'allow-scripts');
1653 if (is_prefix_https()) {
1654 $entry->setAttribute("src",
1655 str_replace("http://", "https://",
1656 $entry->getAttribute("src")));
1661 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1662 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1663 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1664 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1665 'dt', 'em', 'footer', 'figure', 'figcaption',
1666 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1667 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1668 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1669 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1670 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1671 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1673 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1675 $disallowed_attributes = array('id', 'style', 'class');
1677 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1678 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1679 if (is_array($retval)) {
1681 $allowed_elements = $retval[1];
1682 $disallowed_attributes = $retval[2];
1688 $doc->removeChild($doc->firstChild
); //remove doctype
1689 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1691 if ($highlight_words) {
1692 foreach ($highlight_words as $word) {
1694 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1696 $elements = $xpath->query("//*/text()");
1698 foreach ($elements as $child) {
1700 $fragment = $doc->createDocumentFragment();
1701 $text = $child->textContent
;
1703 while (($pos = mb_stripos($text, $word)) !== false) {
1704 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1705 $word = mb_substr($text, $pos, mb_strlen($word));
1706 $highlight = $doc->createElement('span');
1707 $highlight->appendChild(new DomText($word));
1708 $highlight->setAttribute('class', 'highlight');
1709 $fragment->appendChild($highlight);
1710 $text = mb_substr($text, $pos +
mb_strlen($word));
1713 if (!empty($text)) $fragment->appendChild(new DomText($text));
1715 $child->parentNode
->replaceChild($fragment, $child);
1720 $res = $doc->saveHTML();
1722 /* strip everything outside of <body>...</body> */
1724 $res_frag = array();
1725 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1726 return $res_frag[1];
1732 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1733 $xpath = new DOMXPath($doc);
1734 $entries = $xpath->query('//*');
1736 foreach ($entries as $entry) {
1737 if (!in_array($entry->nodeName
, $allowed_elements)) {
1738 $entry->parentNode
->removeChild($entry);
1741 if ($entry->hasAttributes()) {
1742 $attrs_to_remove = array();
1744 foreach ($entry->attributes
as $attr) {
1746 if (strpos($attr->nodeName
, 'on') === 0) {
1747 array_push($attrs_to_remove, $attr);
1750 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1751 array_push($attrs_to_remove, $attr);
1754 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1755 array_push($attrs_to_remove, $attr);
1759 foreach ($attrs_to_remove as $attr) {
1760 $entry->removeAttributeNode($attr);
1768 function trim_array($array) {
1770 array_walk($tmp, 'trim');
1774 function tag_is_valid($tag) {
1775 if ($tag == '') return false;
1776 if (is_numeric($tag)) return false;
1777 if (mb_strlen($tag) > 250) return false;
1779 if (!$tag) return false;
1784 function render_login_form() {
1785 header('Cache-Control: public');
1787 require_once "login_form.php";
1791 function T_sprintf() {
1792 $args = func_get_args();
1793 return vsprintf(__(array_shift($args)), $args);
1796 function print_checkpoint($n, $s) {
1797 $ts = microtime(true);
1798 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1802 function sanitize_tag($tag) {
1805 $tag = mb_strtolower($tag, 'utf-8');
1807 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1809 if (DB_TYPE
== "mysql") {
1810 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1816 function is_server_https() {
1817 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) ||
$_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1820 function is_prefix_https() {
1821 return parse_url(SELF_URL_PATH
, PHP_URL_SCHEME
) == 'https';
1824 // this returns SELF_URL_PATH sans ending slash
1825 function get_self_url_prefix() {
1826 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1827 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1829 return SELF_URL_PATH
;
1833 function encrypt_password($pass, $salt = '', $mode2 = false) {
1834 if ($salt && $mode2) {
1835 return "MODE2:" . hash('sha256', $salt . $pass);
1837 return "SHA1X:" . sha1("$salt:$pass");
1839 return "SHA1:" . sha1($pass);
1841 } // function encrypt_password
1843 function load_filters($feed_id, $owner_uid) {
1846 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1849 $null_cat_qpart = "cat_id IS NULL OR";
1851 $null_cat_qpart = "";
1853 $result = db_query("SELECT * FROM ttrss_filters2 WHERE
1854 owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title");
1856 $check_cats = array_merge(
1857 Feeds
::getParentCategories($cat_id, $owner_uid),
1860 $check_cats_str = join(",", $check_cats);
1861 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1863 while ($line = db_fetch_assoc($result)) {
1864 $filter_id = $line["id"];
1866 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1868 $result2 = db_query("SELECT
1869 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1870 FROM ttrss_filters2_rules AS r,
1871 ttrss_filter_types AS t
1873 (match_on IS NOT NULL OR
1874 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1875 (feed_id IS NULL OR feed_id = '$feed_id'))) AND
1876 filter_type = t.id AND filter_id = '$filter_id'");
1881 while ($rule_line = db_fetch_assoc($result2)) {
1882 # print_r($rule_line);
1884 if ($rule_line["match_on"]) {
1885 $match_on = json_decode($rule_line["match_on"], true);
1887 if (in_array("0", $match_on) ||
in_array($feed_id, $match_on) ||
count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1890 $rule["reg_exp"] = $rule_line["reg_exp"];
1891 $rule["type"] = $rule_line["type_name"];
1892 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1894 array_push($rules, $rule);
1895 } else if (!$match_any_rule) {
1896 // this filter contains a rule that doesn't match to this feed/category combination
1897 // thus filter has to be rejected
1906 $rule["reg_exp"] = $rule_line["reg_exp"];
1907 $rule["type"] = $rule_line["type_name"];
1908 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1910 array_push($rules, $rule);
1914 if (count($rules) > 0) {
1915 $result2 = db_query("SELECT a.action_param,t.name AS type_name
1916 FROM ttrss_filters2_actions AS a,
1917 ttrss_filter_actions AS t
1919 action_id = t.id AND filter_id = '$filter_id'");
1921 while ($action_line = db_fetch_assoc($result2)) {
1922 # print_r($action_line);
1925 $action["type"] = $action_line["type_name"];
1926 $action["param"] = $action_line["action_param"];
1928 array_push($actions, $action);
1933 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1934 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1935 $filter["rules"] = $rules;
1936 $filter["actions"] = $actions;
1938 if (count($rules) > 0 && count($actions) > 0) {
1939 array_push($filters, $filter);
1946 function get_score_pic($score) {
1948 return "score_high.png";
1949 } else if ($score > 0) {
1950 return "score_half_high.png";
1951 } else if ($score < -100) {
1952 return "score_low.png";
1953 } else if ($score < 0) {
1954 return "score_half_low.png";
1956 return "score_neutral.png";
1960 function feed_has_icon($id) {
1961 return is_file(ICONS_DIR
. "/$id.ico") && filesize(ICONS_DIR
. "/$id.ico") > 0;
1964 function init_plugins() {
1965 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1970 function add_feed_category($feed_cat, $parent_cat_id = false) {
1972 if (!$feed_cat) return false;
1976 if ($parent_cat_id) {
1977 $parent_qpart = "parent_cat = '$parent_cat_id'";
1978 $parent_insert = "'$parent_cat_id'";
1980 $parent_qpart = "parent_cat IS NULL";
1981 $parent_insert = "NULL";
1984 $feed_cat = mb_substr($feed_cat, 0, 250);
1987 "SELECT id FROM ttrss_feed_categories
1988 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1990 if (db_num_rows($result) == 0) {
1993 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1994 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
2005 * Fixes incomplete URLs by prepending "http://".
2006 * Also replaces feed:// with http://, and
2007 * prepends a trailing slash if the url is a domain name only.
2009 * @param string $url Possibly incomplete URL
2011 * @return string Fixed URL.
2013 function fix_url($url) {
2015 // support schema-less urls
2016 if (strpos($url, '//') === 0) {
2017 $url = 'https:' . $url;
2020 if (strpos($url, '://') === false) {
2021 $url = 'http://' . $url;
2022 } else if (substr($url, 0, 5) == 'feed:') {
2023 $url = 'http:' . substr($url, 5);
2026 //prepend slash if the URL has no slash in it
2027 // "http://www.example" -> "http://www.example/"
2028 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
2032 //convert IDNA hostname to punycode if possible
2033 if (function_exists("idn_to_ascii")) {
2034 $parts = parse_url($url);
2035 if (mb_detect_encoding($parts['host']) != 'ASCII')
2037 $parts['host'] = idn_to_ascii($parts['host']);
2038 $url = build_url($parts);
2042 if ($url != "http:///")
2048 function validate_feed_url($url) {
2049 $parts = parse_url($url);
2051 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
2055 /* function save_email_address($email) {
2056 // FIXME: implement persistent storage of emails
2058 if (!$_SESSION['stored_emails'])
2059 $_SESSION['stored_emails'] = array();
2061 if (!in_array($email, $_SESSION['stored_emails']))
2062 array_push($_SESSION['stored_emails'], $email);
2066 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2068 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2070 $sql_is_cat = bool_to_sql_bool($is_cat);
2072 $result = db_query("SELECT access_key FROM ttrss_access_keys
2073 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
2074 AND owner_uid = " . $owner_uid);
2076 if (db_num_rows($result) == 1) {
2077 return db_fetch_result($result, 0, "access_key");
2079 $key = db_escape_string(uniqid_short());
2081 $result = db_query("INSERT INTO ttrss_access_keys
2082 (access_key, feed_id, is_cat, owner_uid)
2083 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
2090 function get_feeds_from_html($url, $content)
2092 $url = fix_url($url);
2093 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2095 libxml_use_internal_errors(true);
2097 $doc = new DOMDocument();
2098 $doc->loadHTML($content);
2099 $xpath = new DOMXPath($doc);
2100 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2101 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2102 $feedUrls = array();
2103 foreach ($entries as $entry) {
2104 if ($entry->hasAttribute('href')) {
2105 $title = $entry->getAttribute('title');
2107 $title = $entry->getAttribute('type');
2109 $feedUrl = rewrite_relative_url(
2110 $baseUrl, $entry->getAttribute('href')
2112 $feedUrls[$feedUrl] = $title;
2118 function is_html($content) {
2119 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2122 function url_is_html($url, $login = false, $pass = false) {
2123 return is_html(fetch_file_contents($url, false, $login, $pass));
2126 function build_url($parts) {
2127 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2130 function cleanup_url_path($path) {
2131 $path = str_replace("/./", "/", $path);
2132 $path = str_replace("//", "/", $path);
2138 * Converts a (possibly) relative URL to a absolute one.
2140 * @param string $url Base URL (i.e. from where the document is)
2141 * @param string $rel_url Possibly relative URL in the document
2143 * @return string Absolute URL
2145 function rewrite_relative_url($url, $rel_url) {
2146 if (strpos($rel_url, "://") !== false) {
2148 } else if (strpos($rel_url, "//") === 0) {
2149 # protocol-relative URL (rare but they exist)
2151 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2152 # magnet:, feed:, etc
2154 } else if (strpos($rel_url, "/") === 0) {
2155 $parts = parse_url($url);
2156 $parts['path'] = $rel_url;
2157 $parts['path'] = cleanup_url_path($parts['path']);
2159 return build_url($parts);
2162 $parts = parse_url($url);
2163 if (!isset($parts['path'])) {
2164 $parts['path'] = '/';
2166 $dir = $parts['path'];
2167 if (substr($dir, -1) !== '/') {
2168 $dir = dirname($parts['path']);
2169 $dir !== '/' && $dir .= '/';
2171 $parts['path'] = $dir . $rel_url;
2172 $parts['path'] = cleanup_url_path($parts['path']);
2174 return build_url($parts);
2178 function cleanup_tags($days = 14, $limit = 1000) {
2180 if (DB_TYPE
== "pgsql") {
2181 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2182 } else if (DB_TYPE
== "mysql") {
2183 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2188 while ($limit > 0) {
2191 $query = "SELECT ttrss_tags.id AS id
2192 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2193 WHERE post_int_id = int_id AND $interval_query AND
2194 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2196 $result = db_query($query);
2200 while ($line = db_fetch_assoc($result)) {
2201 array_push($ids, $line['id']);
2204 if (count($ids) > 0) {
2205 $ids = join(",", $ids);
2207 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2208 $tags_deleted +
= db_affected_rows($tmp_result);
2213 $limit -= $limit_part;
2216 return $tags_deleted;
2219 function print_user_stylesheet() {
2220 $value = get_pref('USER_STYLESHEET');
2223 print "<style type=\"text/css\">";
2224 print str_replace("<br/>", "\n", $value);
2230 function filter_to_sql($filter, $owner_uid) {
2233 if (DB_TYPE
== "pgsql")
2236 $reg_qpart = "REGEXP";
2238 foreach ($filter["rules"] AS $rule) {
2239 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2240 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2241 $rule['reg_exp']) !== FALSE;
2243 if ($regexp_valid) {
2245 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2247 switch ($rule["type"]) {
2249 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2250 $rule['reg_exp'] . "')";
2253 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2254 $rule['reg_exp'] . "')";
2257 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2258 $rule['reg_exp'] . "') OR LOWER(" .
2259 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2262 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2263 $rule['reg_exp'] . "')";
2266 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2267 $rule['reg_exp'] . "')";
2270 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2271 $rule['reg_exp'] . "')";
2275 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2277 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2278 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2281 if (isset($rule["cat_id"])) {
2283 if ($rule["cat_id"] > 0) {
2284 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2285 array_push($children, $rule["cat_id"]);
2287 $children = join(",", $children);
2289 $cat_qpart = "cat_id IN ($children)";
2291 $cat_qpart = "cat_id IS NULL";
2294 $qpart .= " AND $cat_qpart";
2297 $qpart .= " AND feed_id IS NOT NULL";
2299 array_push($query, "($qpart)");
2304 if (count($query) > 0) {
2305 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2307 $fullquery = "(false)";
2310 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2315 if (!function_exists('gzdecode')) {
2316 function gzdecode($string) { // no support for 2nd argument
2317 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2318 base64_encode($string));
2322 function get_random_bytes($length) {
2323 if (function_exists('openssl_random_pseudo_bytes')) {
2324 return openssl_random_pseudo_bytes($length);
2328 for ($i = 0; $i < $length; $i++
)
2329 $output .= chr(mt_rand(0, 255));
2335 function read_stdin() {
2336 $fp = fopen("php://stdin", "r");
2339 $line = trim(fgets($fp));
2347 function implements_interface($class, $interface) {
2348 return in_array($interface, class_implements($class));
2351 function get_minified_js($files) {
2352 require_once 'lib/jshrink/Minifier.php';
2356 foreach ($files as $js) {
2357 if (!isset($_GET['debug'])) {
2358 $cached_file = CACHE_DIR
. "/js/".basename($js).".js";
2360 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2362 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2364 if ($header && $contents) {
2365 list($htag, $hversion) = explode(":", $header);
2367 if ($htag == "tt-rss" && $hversion == VERSION
) {
2374 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js.js"));
2375 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2379 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2386 function calculate_dep_timestamp() {
2387 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2391 foreach ($files as $file) {
2392 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2398 function T_js_decl($s1, $s2) {
2400 $s1 = preg_replace("/\n/", "", $s1);
2401 $s2 = preg_replace("/\n/", "", $s2);
2403 $s1 = preg_replace("/\"/", "\\\"", $s1);
2404 $s2 = preg_replace("/\"/", "\\\"", $s2);
2406 return "T_messages[\"$s1\"] = \"$s2\";\n";
2410 function init_js_translations() {
2412 print 'var T_messages = new Object();
2415 if (T_messages[msg]) {
2416 return T_messages[msg];
2422 function ngettext(msg1, msg2, n) {
2423 return __((parseInt(n) > 1) ? msg2 : msg1);
2426 $l10n = _get_reader();
2428 for ($i = 0; $i < $l10n->total
; $i++
) {
2429 $orig = $l10n->get_original_string($i);
2430 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2431 $key = explode(chr(0), $orig);
2432 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2433 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2435 $translation = __($orig);
2436 print T_js_decl($orig, $translation);
2441 function get_theme_path($theme) {
2442 $check = "themes/$theme";
2443 if (file_exists($check)) return $check;
2445 $check = "themes.local/$theme";
2446 if (file_exists($check)) return $check;
2449 function theme_valid($theme) {
2450 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2452 if (in_array($theme, $bundled_themes)) return true;
2454 $file = "themes/" . basename($theme);
2456 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2458 if (file_exists($file) && is_readable($file)) {
2459 $fh = fopen($file, "r");
2462 $header = fgets($fh);
2465 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2473 * @SuppressWarnings(unused)
2475 function error_json($code) {
2476 require_once "errors.php";
2478 @$message = $ERRORS[$code];
2480 return json_encode(array("error" =>
2481 array("code" => $code, "message" => $message)));
2485 /*function abs_to_rel_path($dir) {
2486 $tmp = str_replace(dirname(__DIR__), "", $dir);
2488 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2493 function get_upload_error_message($code) {
2496 0 => __('There is no error, the file uploaded with success'),
2497 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2498 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2499 3 => __('The uploaded file was only partially uploaded'),
2500 4 => __('No file was uploaded'),
2501 6 => __('Missing a temporary folder'),
2502 7 => __('Failed to write file to disk.'),
2503 8 => __('A PHP extension stopped the file upload.'),
2506 return $errors[$code];
2509 function base64_img($filename) {
2510 if (file_exists($filename)) {
2511 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2513 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2519 /* this is essentially a wrapper for readfile() which allows plugins to hook
2520 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2522 hook function should return true if request was handled (or at least attempted to)
2524 note that this can be called without user context so the plugin to handle this
2525 should be loaded systemwide in config.php */
2526 function send_local_file($filename) {
2527 if (file_exists($filename)) {
2528 $tmppluginhost = new PluginHost();
2530 $tmppluginhost->load(PLUGINS
, PluginHost
::KIND_SYSTEM
);
2531 $tmppluginhost->load_data();
2533 foreach ($tmppluginhost->get_hooks(PluginHost
::HOOK_SEND_LOCAL_FILE
) as $plugin) {
2534 if ($plugin->hook_send_local_file($filename)) return true;
2537 $mimetype = mime_content_type($filename);
2538 header("Content-type: $mimetype");
2540 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2541 header("Last-Modified: $stamp", true);
2543 return readfile($filename);