2 define('EXPECTED_CONFIG_VERSION', 26);
3 define('SCHEMA_VERSION', 130);
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 * @param string $name The constant name.
35 * @param mixed $value The constant value.
37 * @return boolean True if defined successfully or not.
39 function define_default($name, $value) {
40 defined($name) or define($name, $value);
43 ///// Some defaults that you can override in config.php //////
45 define_default('FEED_FETCH_TIMEOUT', 45);
46 // How may seconds to wait for response when requesting feed from a site
47 define_default('FEED_FETCH_NO_CACHE_TIMEOUT', 15);
48 // How may seconds to wait for response when requesting feed from a
49 // site when that feed wasn't cached before
50 define_default('FILE_FETCH_TIMEOUT', 45);
51 // Default timeout when fetching files from remote sites
52 define_default('FILE_FETCH_CONNECT_TIMEOUT', 15);
53 // How many seconds to wait for initial response from website when
54 // fetching files from remote sites
56 // feed updating stuff
57 define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
58 define_default('DAEMON_FEED_LIMIT', 500);
59 define_default('DAEMON_SLEEP_INTERVAL', 120);
60 define_default('_MIN_CACHE_FILE_SIZE', 1024);
62 if (DB_TYPE
== "pgsql") {
63 define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
65 define('SUBSTRING_FOR_DATE', 'SUBSTRING');
69 * Return available translations names.
72 * @return array A array of available translations.
74 function get_translations() {
76 "auto" => "Detect automatically",
77 "ar_SA" => "العربيّة (Arabic)",
78 "bg_BG" => "Bulgarian",
83 "el_GR" => "Ελληνικά",
84 "es_ES" => "Español (España)",
87 "fr_FR" => "Français",
88 "hu_HU" => "Magyar (Hungarian)",
89 "it_IT" => "Italiano",
90 "ja_JP" => "日本語 (Japanese)",
91 "lv_LV" => "Latviešu",
92 "nb_NO" => "Norwegian bokmål",
96 "pt_BR" => "Portuguese/Brazil",
97 "pt_PT" => "Portuguese/Portugal",
98 "zh_CN" => "Simplified Chinese",
99 "zh_TW" => "Traditional Chinese",
100 "sv_SE" => "Svenska",
102 "tr_TR" => "Türkçe");
107 require_once "lib/accept-to-gettext.php";
108 require_once "lib/gettext/gettext.inc";
110 function startup_gettext() {
112 # Get locale from Accept-Language header
113 $lang = al2gt(array_keys(get_translations()), "text/html");
115 if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
116 $lang = _TRANSLATION_OVERRIDE_DEFAULT
;
119 if ($_SESSION["uid"] && get_schema_version() >= 120) {
120 $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
122 if ($pref_lang && $pref_lang != 'auto') {
128 if (defined('LC_MESSAGES')) {
129 _setlocale(LC_MESSAGES
, $lang);
130 } else if (defined('LC_ALL')) {
131 _setlocale(LC_ALL
, $lang);
134 _bindtextdomain("messages", "locale");
136 _textdomain("messages");
137 _bind_textdomain_codeset("messages", "UTF-8");
141 require_once 'db-prefs.php';
142 require_once 'version.php';
143 require_once 'controls.php';
145 define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION
. ' (http://tt-rss.org/)');
146 ini_set('user_agent', SELF_USER_AGENT
);
148 $schema_version = false;
150 function _debug_suppress($suppress) {
151 global $suppress_debugging;
153 $suppress_debugging = $suppress;
157 * Print a timestamped debug message.
159 * @param string $msg The debug message.
162 function _debug($msg, $show = true) {
163 global $suppress_debugging;
165 //echo "[$suppress_debugging] $msg $show\n";
167 if ($suppress_debugging) return false;
169 $ts = strftime("%H:%M:%S", time());
170 if (function_exists('posix_getpid')) {
171 $ts = "$ts/" . posix_getpid();
174 if ($show && !(defined('QUIET') && QUIET
)) {
175 print "[$ts] $msg\n";
178 if (defined('LOGFILE')) {
179 $fp = fopen(LOGFILE
, 'a+');
184 if (function_exists("flock")) {
187 // try to lock logfile for writing
188 while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB
)) {
199 fputs($fp, "[$ts] $msg\n");
201 if (function_exists("flock")) {
212 * Purge a feed old posts.
214 * @param mixed $link A database connection.
215 * @param mixed $feed_id The id of the purged feed.
216 * @param mixed $purge_interval Olderness of purged posts.
217 * @param boolean $debug Set to True to enable the debug. False by default.
221 function purge_feed($feed_id, $purge_interval, $debug = false) {
223 if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
228 "SELECT owner_uid FROM ttrss_feeds WHERE id = '$feed_id'");
232 if (db_num_rows($result) == 1) {
233 $owner_uid = db_fetch_result($result, 0, "owner_uid");
236 if ($purge_interval == -1 ||
!$purge_interval) {
238 CCache
::update($feed_id, $owner_uid);
243 if (!$owner_uid) return;
245 if (FORCE_ARTICLE_PURGE
== 0) {
246 $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
249 $purge_unread = true;
250 $purge_interval = FORCE_ARTICLE_PURGE
;
253 if (!$purge_unread) $query_limit = " unread = false AND ";
255 if (DB_TYPE
== "pgsql") {
256 $result = db_query("DELETE FROM ttrss_user_entries
258 WHERE ttrss_entries.id = ref_id AND
260 feed_id = '$feed_id' AND
262 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
266 /* $result = db_query("DELETE FROM ttrss_user_entries WHERE
267 marked = false AND feed_id = '$feed_id' AND
268 (SELECT date_updated FROM ttrss_entries WHERE
269 id = ref_id) < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)"); */
271 $result = db_query("DELETE FROM ttrss_user_entries
272 USING ttrss_user_entries, ttrss_entries
273 WHERE ttrss_entries.id = ref_id AND
275 feed_id = '$feed_id' AND
277 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
280 $rows = db_affected_rows($result);
282 CCache
::update($feed_id, $owner_uid);
285 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
289 } // function purge_feed
291 function feed_purge_interval($feed_id) {
293 $result = db_query("SELECT purge_interval, owner_uid FROM ttrss_feeds
294 WHERE id = '$feed_id'");
296 if (db_num_rows($result) == 1) {
297 $purge_interval = db_fetch_result($result, 0, "purge_interval");
298 $owner_uid = db_fetch_result($result, 0, "owner_uid");
300 if ($purge_interval == 0) $purge_interval = get_pref(
301 'PURGE_OLD_DAYS', $owner_uid);
303 return $purge_interval;
310 /*function get_feed_update_interval($feed_id) {
311 $result = db_query("SELECT owner_uid, update_interval FROM
312 ttrss_feeds WHERE id = '$feed_id'");
314 if (db_num_rows($result) == 1) {
315 $update_interval = db_fetch_result($result, 0, "update_interval");
316 $owner_uid = db_fetch_result($result, 0, "owner_uid");
318 if ($update_interval != 0) {
319 return $update_interval;
321 return get_pref('DEFAULT_UPDATE_INTERVAL', $owner_uid, false);
329 // TODO: multiple-argument way is deprecated, first parameter is a hash now
330 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
331 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
333 global $fetch_last_error;
334 global $fetch_last_error_code;
335 global $fetch_last_error_content;
336 global $fetch_last_content_type;
337 global $fetch_curl_used;
339 $fetch_last_error = false;
340 $fetch_last_error_code = -1;
341 $fetch_last_error_content = "";
342 $fetch_last_content_type = "";
343 $fetch_curl_used = false;
345 if (!is_array($options)) {
347 // falling back on compatibility shim
348 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "timestamp", "useragent" ];
351 for ($i = 0; $i < func_num_args(); $i++
) {
352 $tmp[$option_names[$i]] = func_get_arg($i);
358 "url" => func_get_arg(0),
359 "type" => @func_get_arg(1),
360 "login" => @func_get_arg(2),
361 "pass" => @func_get_arg(3),
362 "post_query" => @func_get_arg(4),
363 "timeout" => @func_get_arg(5),
364 "timestamp" => @func_get_arg(6),
365 "useragent" => @func_get_arg(7)
369 $url = $options["url"];
370 $type = isset($options["type"]) ?
$options["type"] : false;
371 $login = isset($options["login"]) ?
$options["login"] : false;
372 $pass = isset($options["pass"]) ?
$options["pass"] : false;
373 $post_query = isset($options["post_query"]) ?
$options["post_query"] : false;
374 $timeout = isset($options["timeout"]) ?
$options["timeout"] : false;
375 $timestamp = isset($options["timestamp"]) ?
$options["timestamp"] : 0;
376 $useragent = isset($options["useragent"]) ?
$options["useragent"] : false;
377 $followlocation = isset($options["followlocation"]) ?
$options["followlocation"] : true;
379 $url = ltrim($url, ' ');
380 $url = str_replace(' ', '%20', $url);
382 if (strpos($url, "//") === 0)
383 $url = 'http:' . $url;
385 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
387 $fetch_curl_used = true;
389 $ch = curl_init($url);
391 if ($timestamp && !$post_query) {
392 curl_setopt($ch, CURLOPT_HTTPHEADER
,
393 array("If-Modified-Since: ".gmdate('D, d M Y H:i:s \G\M\T', $timestamp)));
396 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT
, $timeout ?
$timeout : FILE_FETCH_CONNECT_TIMEOUT
);
397 curl_setopt($ch, CURLOPT_TIMEOUT
, $timeout ?
$timeout : FILE_FETCH_TIMEOUT
);
398 curl_setopt($ch, CURLOPT_FOLLOWLOCATION
, !ini_get("open_basedir") && $followlocation);
399 curl_setopt($ch, CURLOPT_MAXREDIRS
, 20);
400 curl_setopt($ch, CURLOPT_BINARYTRANSFER
, true);
401 curl_setopt($ch, CURLOPT_RETURNTRANSFER
, true);
402 curl_setopt($ch, CURLOPT_HTTPAUTH
, CURLAUTH_ANY
);
403 curl_setopt($ch, CURLOPT_USERAGENT
, $useragent ?
$useragent :
405 curl_setopt($ch, CURLOPT_ENCODING
, "");
406 //curl_setopt($ch, CURLOPT_REFERER, $url);
408 if (!ini_get("open_basedir")) {
409 curl_setopt($ch, CURLOPT_COOKIEJAR
, "/dev/null");
412 if (defined('_CURL_HTTP_PROXY')) {
413 curl_setopt($ch, CURLOPT_PROXY
, _CURL_HTTP_PROXY
);
417 curl_setopt($ch, CURLOPT_POST
, true);
418 curl_setopt($ch, CURLOPT_POSTFIELDS
, $post_query);
422 curl_setopt($ch, CURLOPT_USERPWD
, "$login:$pass");
424 $contents = @curl_exec
($ch);
426 if (curl_errno($ch) === 23 ||
curl_errno($ch) === 61) {
427 curl_setopt($ch, CURLOPT_ENCODING
, 'none');
428 $contents = @curl_exec
($ch);
431 if ($contents === false) {
432 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
437 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE
);
438 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE
);
440 $fetch_last_error_code = $http_code;
442 if ($http_code != 200 ||
$type && strpos($fetch_last_content_type, "$type") === false) {
443 if (curl_errno($ch) != 0) {
444 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
446 $fetch_last_error = "HTTP Code: $http_code";
448 $fetch_last_error_content = $contents;
458 $fetch_curl_used = false;
460 if ($login && $pass){
461 $url_parts = array();
463 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
465 $pass = urlencode($pass);
467 if ($url_parts[1] && $url_parts[2]) {
468 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
472 // TODO: should this support POST requests or not? idk
474 if (!$post_query && $timestamp) {
475 $context = stream_context_create(array(
478 'ignore_errors' => true,
479 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
480 'protocol_version'=> 1.1,
481 'header' => "If-Modified-Since: ".gmdate("D, d M Y H:i:s \\G\\M\\T\r\n", $timestamp)
484 $context = stream_context_create(array(
487 'ignore_errors' => true,
488 'timeout' => $timeout ?
$timeout : FILE_FETCH_TIMEOUT
,
489 'protocol_version'=> 1.1
493 $old_error = error_get_last();
495 $data = @file_get_contents
($url, false, $context);
497 if (isset($http_response_header) && is_array($http_response_header)) {
498 foreach ($http_response_header as $h) {
499 if (substr(strtolower($h), 0, 13) == 'content-type:') {
500 $fetch_last_content_type = substr($h, 14);
501 // don't abort here b/c there might be more than one
502 // e.g. if we were being redirected -- last one is the right one
505 if (substr(strtolower($h), 0, 7) == 'http/1.') {
506 $fetch_last_error_code = (int) substr($h, 9, 3);
511 if ($fetch_last_error_code != 200) {
512 $error = error_get_last();
514 if ($error['message'] != $old_error['message']) {
515 $fetch_last_error = $error["message"];
517 $fetch_last_error = "HTTP Code: $fetch_last_error_code";
520 $fetch_last_error_content = $data;
530 * Try to determine the favicon URL for a feed.
531 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
532 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
534 * @param string $url A feed or page URL
536 * @return mixed The favicon URL, or false if none was found.
538 function get_favicon_url($url) {
540 $favicon_url = false;
542 if ($html = @fetch_file_contents
($url)) {
544 libxml_use_internal_errors(true);
546 $doc = new DOMDocument();
547 $doc->loadHTML($html);
548 $xpath = new DOMXPath($doc);
550 $base = $xpath->query('/html/head/base');
551 foreach ($base as $b) {
552 $url = $b->getAttribute("href");
556 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
557 if (count($entries) > 0) {
558 foreach ($entries as $entry) {
559 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
566 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
569 } // function get_favicon_url
571 function initialize_user_prefs($uid, $profile = false) {
573 $uid = db_escape_string($uid);
577 $profile_qpart = "AND profile IS NULL";
579 $profile_qpart = "AND profile = '$profile'";
582 if (get_schema_version() < 63) $profile_qpart = "";
586 $result = db_query("SELECT pref_name,def_value FROM ttrss_prefs");
588 $u_result = db_query("SELECT pref_name
589 FROM ttrss_user_prefs WHERE owner_uid = '$uid' $profile_qpart");
591 $active_prefs = array();
593 while ($line = db_fetch_assoc($u_result)) {
594 array_push($active_prefs, $line["pref_name"]);
597 while ($line = db_fetch_assoc($result)) {
598 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
599 // print "adding " . $line["pref_name"] . "<br>";
601 $line["def_value"] = db_escape_string($line["def_value"]);
602 $line["pref_name"] = db_escape_string($line["pref_name"]);
604 if (get_schema_version() < 63) {
605 db_query("INSERT INTO ttrss_user_prefs
606 (owner_uid,pref_name,value) VALUES
607 ('$uid', '".$line["pref_name"]."','".$line["def_value"]."')");
610 db_query("INSERT INTO ttrss_user_prefs
611 (owner_uid,pref_name,value, profile) VALUES
612 ('$uid', '".$line["pref_name"]."','".$line["def_value"]."', $profile)");
622 function get_ssl_certificate_id() {
623 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
624 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
625 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
626 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
627 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
629 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
630 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
631 $_SERVER["SSL_CLIENT_V_START"] .
632 $_SERVER["SSL_CLIENT_V_END"] .
633 $_SERVER["SSL_CLIENT_S_DN"]);
638 function authenticate_user($login, $password, $check_only = false) {
640 if (!SINGLE_USER_MODE
) {
643 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_AUTH_USER
) as $plugin) {
645 $user_id = (int) $plugin->authenticate($login, $password);
648 $_SESSION["auth_module"] = strtolower(get_class($plugin));
653 if ($user_id && !$check_only) {
656 $_SESSION["uid"] = $user_id;
657 $_SESSION["version"] = VERSION_STATIC
;
659 $result = db_query("SELECT login,access_level,pwd_hash FROM ttrss_users
660 WHERE id = '$user_id'");
662 $_SESSION["name"] = db_fetch_result($result, 0, "login");
663 $_SESSION["access_level"] = db_fetch_result($result, 0, "access_level");
664 $_SESSION["csrf_token"] = uniqid_short();
666 db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
669 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
670 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
671 $_SESSION["pwd_hash"] = db_fetch_result($result, 0, "pwd_hash");
673 $_SESSION["last_version_check"] = time();
675 initialize_user_prefs($_SESSION["uid"]);
684 $_SESSION["uid"] = 1;
685 $_SESSION["name"] = "admin";
686 $_SESSION["access_level"] = 10;
688 $_SESSION["hide_hello"] = true;
689 $_SESSION["hide_logout"] = true;
691 $_SESSION["auth_module"] = false;
693 if (!$_SESSION["csrf_token"]) {
694 $_SESSION["csrf_token"] = uniqid_short();
697 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
699 initialize_user_prefs($_SESSION["uid"]);
705 function make_password($length = 8) {
708 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
712 while ($i < $length) {
713 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
715 if (!strstr($password, $char)) {
723 // this is called after user is created to initialize default feeds, labels
726 // user preferences are checked on every login, not here
728 function initialize_user($uid) {
730 db_query("insert into ttrss_feeds (owner_uid,title,feed_url)
731 values ('$uid', 'Tiny Tiny RSS: Forum',
732 'http://tt-rss.org/forum/rss.php')");
735 function logout_user() {
737 if (isset($_COOKIE[session_name()])) {
738 setcookie(session_name(), '', time()-42000, '/');
742 function validate_csrf($csrf_token) {
743 return $csrf_token == $_SESSION['csrf_token'];
746 function load_user_plugins($owner_uid, $pluginhost = false) {
748 if (!$pluginhost) $pluginhost = PluginHost
::getInstance();
750 if ($owner_uid && SCHEMA_VERSION
>= 100) {
751 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
753 $pluginhost->load($plugins, PluginHost
::KIND_USER
, $owner_uid);
755 if (get_schema_version() > 100) {
756 $pluginhost->load_data();
761 function login_sequence() {
762 if (SINGLE_USER_MODE
) {
764 authenticate_user("admin", null);
766 load_user_plugins($_SESSION["uid"]);
768 if (!validate_session()) $_SESSION["uid"] = false;
770 if (!$_SESSION["uid"]) {
772 if (AUTH_AUTO_LOGIN
&& authenticate_user(null, null)) {
773 $_SESSION["ref_schema_version"] = get_schema_version(true);
775 authenticate_user(null, null, true);
778 if (!$_SESSION["uid"]) {
780 setcookie(session_name(), '', time()-42000, '/');
787 /* bump login timestamp */
788 db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
790 $_SESSION["last_login_update"] = time();
793 if ($_SESSION["uid"]) {
795 load_user_plugins($_SESSION["uid"]);
799 db_query("DELETE FROM ttrss_counters_cache WHERE owner_uid = ".
800 $_SESSION["uid"] . " AND
801 (SELECT COUNT(id) FROM ttrss_feeds WHERE
802 ttrss_feeds.id = feed_id) = 0");
804 db_query("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ".
805 $_SESSION["uid"] . " AND
806 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
807 ttrss_feed_categories.id = feed_id) = 0");
814 function truncate_string($str, $max_len, $suffix = '…') {
815 if (mb_strlen($str, "utf-8") > $max_len) {
816 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
823 function truncate_middle($str, $max_len, $suffix = '…') {
824 if (strlen($str) > $max_len) {
825 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
831 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
834 $source_tz = new DateTimeZone($source_tz);
835 } catch (Exception
$e) {
836 $source_tz = new DateTimeZone('UTC');
840 $dest_tz = new DateTimeZone($dest_tz);
841 } catch (Exception
$e) {
842 $dest_tz = new DateTimeZone('UTC');
845 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
846 return $dt->format('U') +
$dest_tz->getOffset($dt);
849 function make_local_datetime($timestamp, $long, $owner_uid = false,
850 $no_smart_dt = false, $eta_min = false) {
852 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
853 if (!$timestamp) $timestamp = '1970-01-01 0:00';
858 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
860 $timestamp = substr($timestamp, 0, 19);
862 # We store date in UTC internally
863 $dt = new DateTime($timestamp, $utc_tz);
865 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
867 if ($user_tz_string != 'Automatic') {
870 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
871 } catch (Exception
$e) {
875 $tz_offset = $user_tz->getOffset($dt);
877 $tz_offset = (int) -$_SESSION["clientTzOffset"];
880 $user_timestamp = $dt->format('U') +
$tz_offset;
883 return smart_date_time($user_timestamp,
884 $tz_offset, $owner_uid, $eta_min);
887 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
889 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
891 return date($format, $user_timestamp);
895 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
896 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
898 if ($eta_min && time() +
$tz_offset - $timestamp < 3600) {
899 return T_sprintf("%d min", date("i", time() +
$tz_offset - $timestamp));
900 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() +
$tz_offset)) {
901 return date("G:i", $timestamp);
902 } else if (date("Y", $timestamp) == date("Y", time() +
$tz_offset)) {
903 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
904 return date($format, $timestamp);
906 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
907 return date($format, $timestamp);
911 function sql_bool_to_bool($s) {
912 if ($s == "t" ||
$s == "1" ||
strtolower($s) == "true") {
919 function bool_to_sql_bool($s) {
927 // Session caching removed due to causing wrong redirects to upgrade
928 // script when get_schema_version() is called on an obsolete session
929 // created on a previous schema version.
930 function get_schema_version($nocache = false) {
931 global $schema_version;
933 if (!$schema_version && !$nocache) {
934 $result = db_query("SELECT schema_version FROM ttrss_version");
935 $version = db_fetch_result($result, 0, "schema_version");
936 $schema_version = $version;
939 return $schema_version;
943 function sanity_check() {
944 require_once 'errors.php';
948 $schema_version = get_schema_version(true);
950 if ($schema_version != SCHEMA_VERSION
) {
954 if (DB_TYPE
== "mysql") {
955 $result = db_query("SELECT true", false);
956 if (db_num_rows($result) != 1) {
961 if (db_escape_string("testTEST") != "testTEST") {
965 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
968 function file_is_locked($filename) {
969 if (file_exists(LOCK_DIRECTORY
. "/$filename")) {
970 if (function_exists('flock')) {
971 $fp = @fopen
(LOCK_DIRECTORY
. "/$filename", "r");
973 if (flock($fp, LOCK_EX | LOCK_NB
)) {
984 return true; // consider the file always locked and skip the test
991 function make_lockfile($filename) {
992 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
994 if ($fp && flock($fp, LOCK_EX | LOCK_NB
)) {
995 $stat_h = fstat($fp);
996 $stat_f = stat(LOCK_DIRECTORY
. "/$filename");
998 if (strtoupper(substr(PHP_OS
, 0, 3)) !== 'WIN') {
999 if ($stat_h["ino"] != $stat_f["ino"] ||
1000 $stat_h["dev"] != $stat_f["dev"]) {
1006 if (function_exists('posix_getpid')) {
1007 fwrite($fp, posix_getpid() . "\n");
1015 function make_stampfile($filename) {
1016 $fp = fopen(LOCK_DIRECTORY
. "/$filename", "w");
1018 if (flock($fp, LOCK_EX | LOCK_NB
)) {
1019 fwrite($fp, time() . "\n");
1020 flock($fp, LOCK_UN
);
1028 function sql_random_function() {
1029 if (DB_TYPE
== "mysql") {
1036 function getFeedUnread($feed, $is_cat = false) {
1037 return Feeds
::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1041 /*function get_pgsql_version() {
1042 $result = db_query("SELECT version() AS version");
1043 $version = explode(" ", db_fetch_result($result, 0, "version"));
1047 function checkbox_to_sql_bool($val) {
1048 return ($val == "on") ?
"true" : "false";
1051 /*function getFeedCatTitle($id) {
1053 return __("Special");
1054 } else if ($id < LABEL_BASE_INDEX) {
1055 return __("Labels");
1056 } else if ($id > 0) {
1057 $result = db_query("SELECT ttrss_feed_categories.title
1058 FROM ttrss_feeds, ttrss_feed_categories WHERE ttrss_feeds.id = '$id' AND
1059 cat_id = ttrss_feed_categories.id");
1060 if (db_num_rows($result) == 1) {
1061 return db_fetch_result($result, 0, "title");
1063 return __("Uncategorized");
1066 return "getFeedCatTitle($id) failed";
1071 function uniqid_short() {
1072 return uniqid(base_convert(rand(), 10, 36));
1075 function make_init_params() {
1078 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1079 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1080 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1081 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1083 $params[strtolower($param)] = (int) get_pref($param);
1086 $params["icons_url"] = ICONS_URL
;
1087 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME
;
1088 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1089 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1090 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1091 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1092 $params["label_base_index"] = (int) LABEL_BASE_INDEX
;
1094 $theme = get_pref( "USER_CSS_THEME", false, false);
1095 $params["theme"] = theme_valid("$theme") ?
$theme : "";
1097 $params["plugins"] = implode(", ", PluginHost
::getInstance()->get_plugin_names());
1099 $params["php_platform"] = PHP_OS
;
1100 $params["php_version"] = PHP_VERSION
;
1102 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1104 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1105 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
1107 $max_feed_id = db_fetch_result($result, 0, "mid");
1108 $num_feeds = db_fetch_result($result, 0, "nf");
1110 $params["max_feed_id"] = (int) $max_feed_id;
1111 $params["num_feeds"] = (int) $num_feeds;
1113 $params["hotkeys"] = get_hotkeys_map();
1115 $params["csrf_token"] = $_SESSION["csrf_token"];
1116 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1118 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE
;
1120 $params["icon_alert"] = base64_img("images/alert.png");
1121 $params["icon_information"] = base64_img("images/information.png");
1122 $params["icon_cross"] = base64_img("images/cross.png");
1123 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1128 function get_hotkeys_info() {
1130 __("Navigation") => array(
1131 "next_feed" => __("Open next feed"),
1132 "prev_feed" => __("Open previous feed"),
1133 "next_article" => __("Open next article"),
1134 "prev_article" => __("Open previous article"),
1135 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1136 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1137 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1138 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1139 "search_dialog" => __("Show search dialog")),
1140 __("Article") => array(
1141 "toggle_mark" => __("Toggle starred"),
1142 "toggle_publ" => __("Toggle published"),
1143 "toggle_unread" => __("Toggle unread"),
1144 "edit_tags" => __("Edit tags"),
1145 "open_in_new_window" => __("Open in new window"),
1146 "catchup_below" => __("Mark below as read"),
1147 "catchup_above" => __("Mark above as read"),
1148 "article_scroll_down" => __("Scroll down"),
1149 "article_scroll_up" => __("Scroll up"),
1150 "select_article_cursor" => __("Select article under cursor"),
1151 "email_article" => __("Email article"),
1152 "close_article" => __("Close/collapse article"),
1153 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1154 "toggle_widescreen" => __("Toggle widescreen mode"),
1155 "toggle_embed_original" => __("Toggle embed original")),
1156 __("Article selection") => array(
1157 "select_all" => __("Select all articles"),
1158 "select_unread" => __("Select unread"),
1159 "select_marked" => __("Select starred"),
1160 "select_published" => __("Select published"),
1161 "select_invert" => __("Invert selection"),
1162 "select_none" => __("Deselect everything")),
1163 __("Feed") => array(
1164 "feed_refresh" => __("Refresh current feed"),
1165 "feed_unhide_read" => __("Un/hide read feeds"),
1166 "feed_subscribe" => __("Subscribe to feed"),
1167 "feed_edit" => __("Edit feed"),
1168 "feed_catchup" => __("Mark as read"),
1169 "feed_reverse" => __("Reverse headlines"),
1170 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1171 "feed_debug_update" => __("Debug feed update"),
1172 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1173 "catchup_all" => __("Mark all feeds as read"),
1174 "cat_toggle_collapse" => __("Un/collapse current category"),
1175 "toggle_combined_mode" => __("Toggle combined mode"),
1176 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1177 __("Go to") => array(
1178 "goto_all" => __("All articles"),
1179 "goto_fresh" => __("Fresh"),
1180 "goto_marked" => __("Starred"),
1181 "goto_published" => __("Published"),
1182 "goto_tagcloud" => __("Tag cloud"),
1183 "goto_prefs" => __("Preferences")),
1184 __("Other") => array(
1185 "create_label" => __("Create label"),
1186 "create_filter" => __("Create filter"),
1187 "collapse_sidebar" => __("Un/collapse sidebar"),
1188 "help_dialog" => __("Show help dialog"))
1191 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_INFO
) as $plugin) {
1192 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1198 function get_hotkeys_map() {
1200 // "navigation" => array(
1203 "n" => "next_article",
1204 "p" => "prev_article",
1205 "(38)|up" => "prev_article",
1206 "(40)|down" => "next_article",
1207 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1208 // "^(40)|Ctrl-down" => "next_article_noscroll",
1209 "(191)|/" => "search_dialog",
1210 // "article" => array(
1211 "s" => "toggle_mark",
1212 "*s" => "toggle_publ",
1213 "u" => "toggle_unread",
1214 "*t" => "edit_tags",
1215 "o" => "open_in_new_window",
1216 "c p" => "catchup_below",
1217 "c n" => "catchup_above",
1218 "*n" => "article_scroll_down",
1219 "*p" => "article_scroll_up",
1220 "*(38)|Shift+up" => "article_scroll_up",
1221 "*(40)|Shift+down" => "article_scroll_down",
1222 "a *w" => "toggle_widescreen",
1223 "a e" => "toggle_embed_original",
1224 "e" => "email_article",
1225 "a q" => "close_article",
1226 // "article_selection" => array(
1227 "a a" => "select_all",
1228 "a u" => "select_unread",
1229 "a *u" => "select_marked",
1230 "a p" => "select_published",
1231 "a i" => "select_invert",
1232 "a n" => "select_none",
1234 "f r" => "feed_refresh",
1235 "f a" => "feed_unhide_read",
1236 "f s" => "feed_subscribe",
1237 "f e" => "feed_edit",
1238 "f q" => "feed_catchup",
1239 "f x" => "feed_reverse",
1240 "f g" => "feed_toggle_vgroup",
1241 "f *d" => "feed_debug_update",
1242 "f *g" => "feed_debug_viewfeed",
1243 "f *c" => "toggle_combined_mode",
1244 "f c" => "toggle_cdm_expanded",
1245 "*q" => "catchup_all",
1246 "x" => "cat_toggle_collapse",
1248 "g a" => "goto_all",
1249 "g f" => "goto_fresh",
1250 "g s" => "goto_marked",
1251 "g p" => "goto_published",
1252 "g t" => "goto_tagcloud",
1253 "g *p" => "goto_prefs",
1254 // "other" => array(
1255 "(9)|Tab" => "select_article_cursor", // tab
1256 "c l" => "create_label",
1257 "c f" => "create_filter",
1258 "c s" => "collapse_sidebar",
1259 "^(191)|Ctrl+/" => "help_dialog",
1262 if (get_pref('COMBINED_DISPLAY_MODE')) {
1263 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1264 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1267 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_HOTKEY_MAP
) as $plugin) {
1268 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1271 $prefixes = array();
1273 foreach (array_keys($hotkeys) as $hotkey) {
1274 $pair = explode(" ", $hotkey, 2);
1276 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1277 array_push($prefixes, $pair[0]);
1281 return array($prefixes, $hotkeys);
1284 function check_for_update() {
1285 if (defined("GIT_VERSION_TIMESTAMP")) {
1286 $content = @fetch_file_contents
(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1289 $content = json_decode($content, true);
1291 if ($content && isset($content["changeset"])) {
1292 if ((int)GIT_VERSION_TIMESTAMP
< (int)$content["changeset"]["timestamp"] &&
1293 GIT_VERSION_HEAD
!= $content["changeset"]["id"]) {
1295 return $content["changeset"]["id"];
1304 function make_runtime_info($disable_update_check = false) {
1307 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1308 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
1310 $max_feed_id = db_fetch_result($result, 0, "mid");
1311 $num_feeds = db_fetch_result($result, 0, "nf");
1313 $data["max_feed_id"] = (int) $max_feed_id;
1314 $data["num_feeds"] = (int) $num_feeds;
1316 $data['last_article_id'] = Article
::getLastArticleId();
1317 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1319 $data['dep_ts'] = calculate_dep_timestamp();
1320 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1323 if (CHECK_FOR_UPDATES
&& !$disable_update_check && $_SESSION["last_version_check"] +
86400 +
rand(-1000, 1000) < time()) {
1324 $update_result = @check_for_update
();
1326 $data["update_result"] = $update_result;
1328 $_SESSION["last_version_check"] = time();
1331 if (file_exists(LOCK_DIRECTORY
. "/update_daemon.lock")) {
1333 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1335 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1337 $stamp = (int) @file_get_contents
(LOCK_DIRECTORY
. "/update_daemon.stamp");
1340 $stamp_delta = time() - $stamp;
1342 if ($stamp_delta > 1800) {
1346 $_SESSION["daemon_stamp_check"] = time();
1349 $data['daemon_stamp_ok'] = $stamp_check;
1351 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1353 $data['daemon_stamp'] = $stamp_fmt;
1361 function search_to_sql($search, $search_language) {
1363 $keywords = str_getcsv(trim($search), " ");
1364 $query_keywords = array();
1365 $search_words = array();
1366 $search_query_leftover = array();
1368 if ($search_language)
1369 $search_language = db_escape_string(mb_strtolower($search_language));
1371 $search_language = "english";
1373 foreach ($keywords as $k) {
1374 if (strpos($k, "-") === 0) {
1381 $commandpair = explode(":", mb_strtolower($k), 2);
1383 switch ($commandpair[0]) {
1385 if ($commandpair[1]) {
1386 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
1387 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1389 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1390 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1391 array_push($search_words, $k);
1395 if ($commandpair[1]) {
1396 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
1397 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1399 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1400 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1401 array_push($search_words, $k);
1405 if ($commandpair[1]) {
1406 if ($commandpair[1] == "true")
1407 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1408 else if ($commandpair[1] == "false")
1409 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1411 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
1412 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1414 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1415 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1416 if (!$not) array_push($search_words, $k);
1421 if ($commandpair[1]) {
1422 if ($commandpair[1] == "true")
1423 array_push($query_keywords, "($not (marked = true))");
1425 array_push($query_keywords, "($not (marked = false))");
1427 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1428 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1429 if (!$not) array_push($search_words, $k);
1433 if ($commandpair[1]) {
1434 if ($commandpair[1] == "true")
1435 array_push($query_keywords, "($not (published = true))");
1437 array_push($query_keywords, "($not (published = false))");
1440 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1441 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1442 if (!$not) array_push($search_words, $k);
1446 if ($commandpair[1]) {
1447 if ($commandpair[1] == "true")
1448 array_push($query_keywords, "($not (unread = true))");
1450 array_push($query_keywords, "($not (unread = false))");
1453 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1454 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1455 if (!$not) array_push($search_words, $k);
1459 if (strpos($k, "@") === 0) {
1461 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1462 $orig_ts = strtotime(substr($k, 1));
1463 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1465 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1467 array_push($query_keywords, "(".SUBSTRING_FOR_DATE
."(updated,1,LENGTH('$k')) $not = '$k')");
1470 if (DB_TYPE
== "pgsql") {
1471 $k = mb_strtolower($k);
1472 array_push($search_query_leftover, $not ?
"!$k" : $k);
1474 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1475 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1478 if (!$not) array_push($search_words, $k);
1483 if (count($search_query_leftover) > 0) {
1484 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
1486 if (DB_TYPE
== "pgsql") {
1487 array_push($query_keywords,
1488 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
1493 $search_query_part = implode("AND", $query_keywords);
1495 return array($search_query_part, $search_words);
1498 function iframe_whitelisted($entry) {
1499 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1501 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST
);
1504 foreach ($whitelist as $w) {
1505 if ($src == $w ||
$src == "www.$w")
1513 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1514 if (!$owner) $owner = $_SESSION["uid"];
1516 $res = trim($str); if (!$res) return '';
1518 $charset_hack = '<head>
1519 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1522 $res = trim($res); if (!$res) return '';
1524 libxml_use_internal_errors(true);
1526 $doc = new DOMDocument();
1527 $doc->loadHTML($charset_hack . $res);
1528 $xpath = new DOMXPath($doc);
1530 $ttrss_uses_https = parse_url(get_self_url_prefix(), PHP_URL_SCHEME
) === 'https';
1531 $rewrite_base_url = $site_url ?
$site_url : SELF_URL_PATH
;
1533 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1535 foreach ($entries as $entry) {
1537 if ($entry->hasAttribute('href')) {
1538 $entry->setAttribute('href',
1539 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1541 $entry->setAttribute('rel', 'noopener noreferrer');
1544 if ($entry->hasAttribute('src')) {
1545 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1546 $cached_filename = CACHE_DIR
. '/images/' . sha1($src);
1548 if (file_exists($cached_filename)) {
1550 // this is strictly cosmetic
1551 if ($entry->tagName
== 'img') {
1553 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "video") {
1555 } else if ($entry->parentNode
&& $entry->parentNode
->tagName
== "audio") {
1561 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1563 if ($entry->hasAttribute('srcset')) {
1564 $entry->removeAttribute('srcset');
1567 if ($entry->hasAttribute('sizes')) {
1568 $entry->removeAttribute('sizes');
1572 $entry->setAttribute('src', $src);
1575 if ($entry->nodeName
== 'img') {
1577 if ($entry->hasAttribute('src')) {
1578 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME
) === 'https';
1580 if ($ttrss_uses_https && !$is_https_url) {
1582 if ($entry->hasAttribute('srcset')) {
1583 $entry->removeAttribute('srcset');
1586 if ($entry->hasAttribute('sizes')) {
1587 $entry->removeAttribute('sizes');
1592 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1593 $force_remove_images ||
$_SESSION["bw_limit"]) {
1595 $p = $doc->createElement('p');
1597 $a = $doc->createElement('a');
1598 $a->setAttribute('href', $entry->getAttribute('src'));
1600 $a->appendChild(new DOMText($entry->getAttribute('src')));
1601 $a->setAttribute('target', '_blank');
1602 $a->setAttribute('rel', 'noopener noreferrer');
1604 $p->appendChild($a);
1606 $entry->parentNode
->replaceChild($p, $entry);
1610 if (strtolower($entry->nodeName
) == "a") {
1611 $entry->setAttribute("target", "_blank");
1612 $entry->setAttribute("rel", "noopener noreferrer");
1616 $entries = $xpath->query('//iframe');
1617 foreach ($entries as $entry) {
1618 if (!iframe_whitelisted($entry)) {
1619 $entry->setAttribute('sandbox', 'allow-scripts');
1621 if ($_SERVER['HTTPS'] == "on") {
1622 $entry->setAttribute("src",
1623 str_replace("http://", "https://",
1624 $entry->getAttribute("src")));
1629 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1630 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1631 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1632 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1633 'dt', 'em', 'footer', 'figure', 'figcaption',
1634 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'html', 'i',
1635 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1636 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1637 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1638 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1639 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1641 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1643 $disallowed_attributes = array('id', 'style', 'class');
1645 foreach (PluginHost
::getInstance()->get_hooks(PluginHost
::HOOK_SANITIZE
) as $plugin) {
1646 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1647 if (is_array($retval)) {
1649 $allowed_elements = $retval[1];
1650 $disallowed_attributes = $retval[2];
1656 $doc->removeChild($doc->firstChild
); //remove doctype
1657 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1659 if ($highlight_words) {
1660 foreach ($highlight_words as $word) {
1662 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1664 $elements = $xpath->query("//*/text()");
1666 foreach ($elements as $child) {
1668 $fragment = $doc->createDocumentFragment();
1669 $text = $child->textContent
;
1671 while (($pos = mb_stripos($text, $word)) !== false) {
1672 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1673 $word = mb_substr($text, $pos, mb_strlen($word));
1674 $highlight = $doc->createElement('span');
1675 $highlight->appendChild(new DomText($word));
1676 $highlight->setAttribute('class', 'highlight');
1677 $fragment->appendChild($highlight);
1678 $text = mb_substr($text, $pos +
mb_strlen($word));
1681 if (!empty($text)) $fragment->appendChild(new DomText($text));
1683 $child->parentNode
->replaceChild($fragment, $child);
1688 $res = $doc->saveHTML();
1690 /* strip everything outside of <body>...</body> */
1692 $res_frag = array();
1693 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1694 return $res_frag[1];
1700 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1701 $xpath = new DOMXPath($doc);
1702 $entries = $xpath->query('//*');
1704 foreach ($entries as $entry) {
1705 if (!in_array($entry->nodeName
, $allowed_elements)) {
1706 $entry->parentNode
->removeChild($entry);
1709 if ($entry->hasAttributes()) {
1710 $attrs_to_remove = array();
1712 foreach ($entry->attributes
as $attr) {
1714 if (strpos($attr->nodeName
, 'on') === 0) {
1715 array_push($attrs_to_remove, $attr);
1718 if ($attr->nodeName
== 'href' && stripos($attr->value
, 'javascript:') === 0) {
1719 array_push($attrs_to_remove, $attr);
1722 if (in_array($attr->nodeName
, $disallowed_attributes)) {
1723 array_push($attrs_to_remove, $attr);
1727 foreach ($attrs_to_remove as $attr) {
1728 $entry->removeAttributeNode($attr);
1736 function trim_array($array) {
1738 array_walk($tmp, 'trim');
1742 function tag_is_valid($tag) {
1743 if ($tag == '') return false;
1744 if (is_numeric($tag)) return false;
1745 if (mb_strlen($tag) > 250) return false;
1747 if (!$tag) return false;
1752 function render_login_form() {
1753 header('Cache-Control: public');
1755 require_once "login_form.php";
1759 function T_sprintf() {
1760 $args = func_get_args();
1761 return vsprintf(__(array_shift($args)), $args);
1764 function print_checkpoint($n, $s) {
1765 $ts = microtime(true);
1766 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1770 function sanitize_tag($tag) {
1773 $tag = mb_strtolower($tag, 'utf-8');
1775 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1777 if (DB_TYPE
== "mysql") {
1778 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1784 function get_self_url_prefix() {
1785 if (strrpos(SELF_URL_PATH
, "/") === strlen(SELF_URL_PATH
)-1) {
1786 return substr(SELF_URL_PATH
, 0, strlen(SELF_URL_PATH
)-1);
1788 return SELF_URL_PATH
;
1792 function encrypt_password($pass, $salt = '', $mode2 = false) {
1793 if ($salt && $mode2) {
1794 return "MODE2:" . hash('sha256', $salt . $pass);
1796 return "SHA1X:" . sha1("$salt:$pass");
1798 return "SHA1:" . sha1($pass);
1800 } // function encrypt_password
1802 function load_filters($feed_id, $owner_uid) {
1805 $cat_id = (int)Feeds
::getFeedCategory($feed_id);
1808 $null_cat_qpart = "cat_id IS NULL OR";
1810 $null_cat_qpart = "";
1812 $result = db_query("SELECT * FROM ttrss_filters2 WHERE
1813 owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title");
1815 $check_cats = join(",", array_merge(
1816 Feeds
::getParentCategories($cat_id, $owner_uid),
1819 while ($line = db_fetch_assoc($result)) {
1820 $filter_id = $line["id"];
1822 $result2 = db_query("SELECT
1823 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, t.name AS type_name
1824 FROM ttrss_filters2_rules AS r,
1825 ttrss_filter_types AS t
1827 ($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats)) AND
1828 (feed_id IS NULL OR feed_id = '$feed_id') AND
1829 filter_type = t.id AND filter_id = '$filter_id'");
1834 while ($rule_line = db_fetch_assoc($result2)) {
1835 # print_r($rule_line);
1838 $rule["reg_exp"] = $rule_line["reg_exp"];
1839 $rule["type"] = $rule_line["type_name"];
1840 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1842 array_push($rules, $rule);
1845 $result2 = db_query("SELECT a.action_param,t.name AS type_name
1846 FROM ttrss_filters2_actions AS a,
1847 ttrss_filter_actions AS t
1849 action_id = t.id AND filter_id = '$filter_id'");
1851 while ($action_line = db_fetch_assoc($result2)) {
1852 # print_r($action_line);
1855 $action["type"] = $action_line["type_name"];
1856 $action["param"] = $action_line["action_param"];
1858 array_push($actions, $action);
1863 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1864 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1865 $filter["rules"] = $rules;
1866 $filter["actions"] = $actions;
1868 if (count($rules) > 0 && count($actions) > 0) {
1869 array_push($filters, $filter);
1876 function get_score_pic($score) {
1878 return "score_high.png";
1879 } else if ($score > 0) {
1880 return "score_half_high.png";
1881 } else if ($score < -100) {
1882 return "score_low.png";
1883 } else if ($score < 0) {
1884 return "score_half_low.png";
1886 return "score_neutral.png";
1890 function feed_has_icon($id) {
1891 return is_file(ICONS_DIR
. "/$id.ico") && filesize(ICONS_DIR
. "/$id.ico") > 0;
1894 function init_plugins() {
1895 PluginHost
::getInstance()->load(PLUGINS
, PluginHost
::KIND_ALL
);
1900 function add_feed_category($feed_cat, $parent_cat_id = false) {
1902 if (!$feed_cat) return false;
1906 if ($parent_cat_id) {
1907 $parent_qpart = "parent_cat = '$parent_cat_id'";
1908 $parent_insert = "'$parent_cat_id'";
1910 $parent_qpart = "parent_cat IS NULL";
1911 $parent_insert = "NULL";
1914 $feed_cat = mb_substr($feed_cat, 0, 250);
1917 "SELECT id FROM ttrss_feed_categories
1918 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1920 if (db_num_rows($result) == 0) {
1923 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1924 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
1935 * Fixes incomplete URLs by prepending "http://".
1936 * Also replaces feed:// with http://, and
1937 * prepends a trailing slash if the url is a domain name only.
1939 * @param string $url Possibly incomplete URL
1941 * @return string Fixed URL.
1943 function fix_url($url) {
1945 // support schema-less urls
1946 if (strpos($url, '//') === 0) {
1947 $url = 'https:' . $url;
1950 if (strpos($url, '://') === false) {
1951 $url = 'http://' . $url;
1952 } else if (substr($url, 0, 5) == 'feed:') {
1953 $url = 'http:' . substr($url, 5);
1956 //prepend slash if the URL has no slash in it
1957 // "http://www.example" -> "http://www.example/"
1958 if (strpos($url, '/', strpos($url, ':') +
3) === false) {
1962 //convert IDNA hostname to punycode if possible
1963 if (function_exists("idn_to_ascii")) {
1964 $parts = parse_url($url);
1965 if (mb_detect_encoding($parts['host']) != 'ASCII')
1967 $parts['host'] = idn_to_ascii($parts['host']);
1968 $url = build_url($parts);
1972 if ($url != "http:///")
1978 function validate_feed_url($url) {
1979 $parts = parse_url($url);
1981 return ($parts['scheme'] == 'http' ||
$parts['scheme'] == 'feed' ||
$parts['scheme'] == 'https');
1985 /* function save_email_address($email) {
1986 // FIXME: implement persistent storage of emails
1988 if (!$_SESSION['stored_emails'])
1989 $_SESSION['stored_emails'] = array();
1991 if (!in_array($email, $_SESSION['stored_emails']))
1992 array_push($_SESSION['stored_emails'], $email);
1996 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
1998 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2000 $sql_is_cat = bool_to_sql_bool($is_cat);
2002 $result = db_query("SELECT access_key FROM ttrss_access_keys
2003 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
2004 AND owner_uid = " . $owner_uid);
2006 if (db_num_rows($result) == 1) {
2007 return db_fetch_result($result, 0, "access_key");
2009 $key = db_escape_string(uniqid_short());
2011 $result = db_query("INSERT INTO ttrss_access_keys
2012 (access_key, feed_id, is_cat, owner_uid)
2013 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
2020 function get_feeds_from_html($url, $content)
2022 $url = fix_url($url);
2023 $baseUrl = substr($url, 0, strrpos($url, '/') +
1);
2025 libxml_use_internal_errors(true);
2027 $doc = new DOMDocument();
2028 $doc->loadHTML($content);
2029 $xpath = new DOMXPath($doc);
2030 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2031 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2032 $feedUrls = array();
2033 foreach ($entries as $entry) {
2034 if ($entry->hasAttribute('href')) {
2035 $title = $entry->getAttribute('title');
2037 $title = $entry->getAttribute('type');
2039 $feedUrl = rewrite_relative_url(
2040 $baseUrl, $entry->getAttribute('href')
2042 $feedUrls[$feedUrl] = $title;
2048 function is_html($content) {
2049 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2052 function url_is_html($url, $login = false, $pass = false) {
2053 return is_html(fetch_file_contents($url, false, $login, $pass));
2056 function build_url($parts) {
2057 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2060 function cleanup_url_path($path) {
2061 $path = str_replace("/./", "/", $path);
2062 $path = str_replace("//", "/", $path);
2068 * Converts a (possibly) relative URL to a absolute one.
2070 * @param string $url Base URL (i.e. from where the document is)
2071 * @param string $rel_url Possibly relative URL in the document
2073 * @return string Absolute URL
2075 function rewrite_relative_url($url, $rel_url) {
2076 if (strpos($rel_url, "://") !== false) {
2078 } else if (strpos($rel_url, "//") === 0) {
2079 # protocol-relative URL (rare but they exist)
2081 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2082 # magnet:, feed:, etc
2084 } else if (strpos($rel_url, "/") === 0) {
2085 $parts = parse_url($url);
2086 $parts['path'] = $rel_url;
2087 $parts['path'] = cleanup_url_path($parts['path']);
2089 return build_url($parts);
2092 $parts = parse_url($url);
2093 if (!isset($parts['path'])) {
2094 $parts['path'] = '/';
2096 $dir = $parts['path'];
2097 if (substr($dir, -1) !== '/') {
2098 $dir = dirname($parts['path']);
2099 $dir !== '/' && $dir .= '/';
2101 $parts['path'] = $dir . $rel_url;
2102 $parts['path'] = cleanup_url_path($parts['path']);
2104 return build_url($parts);
2108 function cleanup_tags($days = 14, $limit = 1000) {
2110 if (DB_TYPE
== "pgsql") {
2111 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2112 } else if (DB_TYPE
== "mysql") {
2113 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2118 while ($limit > 0) {
2121 $query = "SELECT ttrss_tags.id AS id
2122 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2123 WHERE post_int_id = int_id AND $interval_query AND
2124 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2126 $result = db_query($query);
2130 while ($line = db_fetch_assoc($result)) {
2131 array_push($ids, $line['id']);
2134 if (count($ids) > 0) {
2135 $ids = join(",", $ids);
2137 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2138 $tags_deleted +
= db_affected_rows($tmp_result);
2143 $limit -= $limit_part;
2146 return $tags_deleted;
2149 function print_user_stylesheet() {
2150 $value = get_pref('USER_STYLESHEET');
2153 print "<style type=\"text/css\">";
2154 print str_replace("<br/>", "\n", $value);
2160 function filter_to_sql($filter, $owner_uid) {
2163 if (DB_TYPE
== "pgsql")
2166 $reg_qpart = "REGEXP";
2168 foreach ($filter["rules"] AS $rule) {
2169 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2170 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2171 $rule['reg_exp']) !== FALSE;
2173 if ($regexp_valid) {
2175 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2177 switch ($rule["type"]) {
2179 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2180 $rule['reg_exp'] . "')";
2183 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2184 $rule['reg_exp'] . "')";
2187 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2188 $rule['reg_exp'] . "') OR LOWER(" .
2189 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2192 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2193 $rule['reg_exp'] . "')";
2196 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2197 $rule['reg_exp'] . "')";
2200 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2201 $rule['reg_exp'] . "')";
2205 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2207 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2208 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2211 if (isset($rule["cat_id"])) {
2213 if ($rule["cat_id"] > 0) {
2214 $children = Feeds
::getChildCategories($rule["cat_id"], $owner_uid);
2215 array_push($children, $rule["cat_id"]);
2217 $children = join(",", $children);
2219 $cat_qpart = "cat_id IN ($children)";
2221 $cat_qpart = "cat_id IS NULL";
2224 $qpart .= " AND $cat_qpart";
2227 $qpart .= " AND feed_id IS NOT NULL";
2229 array_push($query, "($qpart)");
2234 if (count($query) > 0) {
2235 $fullquery = "(" . join($filter["match_any_rule"] ?
"OR" : "AND", $query) . ")";
2237 $fullquery = "(false)";
2240 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2245 if (!function_exists('gzdecode')) {
2246 function gzdecode($string) { // no support for 2nd argument
2247 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2248 base64_encode($string));
2252 function get_random_bytes($length) {
2253 if (function_exists('openssl_random_pseudo_bytes')) {
2254 return openssl_random_pseudo_bytes($length);
2258 for ($i = 0; $i < $length; $i++
)
2259 $output .= chr(mt_rand(0, 255));
2265 function read_stdin() {
2266 $fp = fopen("php://stdin", "r");
2269 $line = trim(fgets($fp));
2277 function implements_interface($class, $interface) {
2278 return in_array($interface, class_implements($class));
2281 function get_minified_js($files) {
2282 require_once 'lib/jshrink/Minifier.php';
2286 foreach ($files as $js) {
2287 if (!isset($_GET['debug'])) {
2288 $cached_file = CACHE_DIR
. "/js/".basename($js).".js";
2290 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2292 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2294 if ($header && $contents) {
2295 list($htag, $hversion) = explode(":", $header);
2297 if ($htag == "tt-rss" && $hversion == VERSION
) {
2304 $minified = JShrink\Minifier
::minify(file_get_contents("js/$js.js"));
2305 file_put_contents($cached_file, "tt-rss:" . VERSION
. "\n" . $minified);
2309 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2316 function calculate_dep_timestamp() {
2317 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2321 foreach ($files as $file) {
2322 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2328 function T_js_decl($s1, $s2) {
2330 $s1 = preg_replace("/\n/", "", $s1);
2331 $s2 = preg_replace("/\n/", "", $s2);
2333 $s1 = preg_replace("/\"/", "\\\"", $s1);
2334 $s2 = preg_replace("/\"/", "\\\"", $s2);
2336 return "T_messages[\"$s1\"] = \"$s2\";\n";
2340 function init_js_translations() {
2342 print 'var T_messages = new Object();
2345 if (T_messages[msg]) {
2346 return T_messages[msg];
2352 function ngettext(msg1, msg2, n) {
2353 return __((parseInt(n) > 1) ? msg2 : msg1);
2356 $l10n = _get_reader();
2358 for ($i = 0; $i < $l10n->total
; $i++
) {
2359 $orig = $l10n->get_original_string($i);
2360 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2361 $key = explode(chr(0), $orig);
2362 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2363 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2365 $translation = __($orig);
2366 print T_js_decl($orig, $translation);
2371 function get_theme_path($theme) {
2372 $check = "themes/$theme";
2373 if (file_exists($check)) return $check;
2375 $check = "themes.local/$theme";
2376 if (file_exists($check)) return $check;
2379 function theme_valid($theme) {
2380 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2382 if (in_array($theme, $bundled_themes)) return true;
2384 $file = "themes/" . basename($theme);
2386 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2388 if (file_exists($file) && is_readable($file)) {
2389 $fh = fopen($file, "r");
2392 $header = fgets($fh);
2395 return strpos($header, "supports-version:" . VERSION_STATIC
) !== FALSE;
2403 * @SuppressWarnings(unused)
2405 function error_json($code) {
2406 require_once "errors.php";
2408 @$message = $ERRORS[$code];
2410 return json_encode(array("error" =>
2411 array("code" => $code, "message" => $message)));
2415 /*function abs_to_rel_path($dir) {
2416 $tmp = str_replace(dirname(__DIR__), "", $dir);
2418 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2423 function get_upload_error_message($code) {
2426 0 => __('There is no error, the file uploaded with success'),
2427 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2428 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2429 3 => __('The uploaded file was only partially uploaded'),
2430 4 => __('No file was uploaded'),
2431 6 => __('Missing a temporary folder'),
2432 7 => __('Failed to write file to disk.'),
2433 8 => __('A PHP extension stopped the file upload.'),
2436 return $errors[$code];
2439 function base64_img($filename) {
2440 if (file_exists($filename)) {
2441 $ext = pathinfo($filename, PATHINFO_EXTENSION
);
2443 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));