]> git.wh0rd.org - tt-rss.git/blame - include/functions.php
Add proper support for proxies
[tt-rss.git] / include / functions.php
CommitLineData
1d3a17c7 1<?php
6e658547 2 define('EXPECTED_CONFIG_VERSION', 26);
ecd2e414 3 define('SCHEMA_VERSION', 133);
545ca067 4
f822a8e5 5 define('LABEL_BASE_INDEX', -1024);
a413f53e 6 define('PLUGIN_FEED_BASE_INDEX', -128);
f822a8e5 7
e57a1507
AD
8 define('COOKIE_LIFETIME_LONG', 86400*365);
9
23d2471c 10 $fetch_last_error = false;
7a01dc77 11 $fetch_last_error_code = false;
ef39be2b 12 $fetch_last_content_type = false;
cf0231f9 13 $fetch_last_error_content = false; // curl only for the time being
3f6f0857 14 $fetch_curl_used = false;
4f71d743 15 $suppress_debugging = false;
23d2471c 16
584411fe
AD
17 libxml_disable_entity_loader(true);
18
e6905f7f
AD
19 // separate test because this is included before sanity checks
20 if (function_exists("mb_internal_encoding")) mb_internal_encoding("UTF-8");
21
324944f3 22 date_default_timezone_set('UTC');
8a7f5767
CW
23 if (defined('E_DEPRECATED')) {
24 error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
25 } else {
26 error_reporting(E_ALL & ~E_NOTICE);
27 }
cce28758 28
40d13c28 29 require_once 'config.php';
cc17c205 30
046ec657
BK
31 /**
32 * Define a constant if not already defined
046ec657
BK
33 */
34 function define_default($name, $value) {
046ec657
BK
35 defined($name) or define($name, $value);
36 }
37
6fd03996 38 /* Some tunables you can override in config.php using define(): */
9ef8798b
BK
39
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
4fd07908 50 define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
6fd03996 51 // stop updating feeds if users haven't logged in for X days
4fd07908 52 define_default('DAEMON_FEED_LIMIT', 500);
6fd03996 53 // feed limit for one update batch
4fd07908 54 define_default('DAEMON_SLEEP_INTERVAL', 120);
6fd03996
AD
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
7c4d7bce 60 define_default('MAX_CONDITIONAL_INTERVAL', 3600*12);
e50c8eaa
AD
61 // max interval between forced unconditional updates for servers
62 // not complying with http if-modified-since (seconds)
6fd03996
AD
63
64 /* tunables end here */
4fd07908 65
fc2b26a6
AD
66 if (DB_TYPE == "pgsql") {
67 define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
68 } else {
69 define('SUBSTRING_FOR_DATE', 'SUBSTRING');
70 }
71
9632f884
AD
72 /**
73 * Return available translations names.
8d505d78 74 *
9632f884
AD
75 * @access public
76 * @return array A array of available translations.
77 */
f8c612d4 78 function get_translations() {
6a214f92 79 $tr = array(
8d505d78 80 "auto" => "Detect automatically",
ca6ef932 81 "ar_SA" => "العربيّة (Arabic)",
51faa115 82 "bg_BG" => "Bulgarian",
c2aa0593 83 "da_DA" => "Dansk",
a3162add 84 "ca_CA" => "Català",
a06b79c4 85 "cs_CZ" => "Česky",
6a214f92 86 "en_US" => "English",
c2aa0593 87 "el_GR" => "Ελληνικά",
1f16ede0 88 "es_ES" => "Español (España)",
c2aa0593 89 "es_LA" => "Español",
a927fe7b 90 "de_DE" => "Deutsch",
6a214f92 91 "fr_FR" => "Français",
e78fd196 92 "hu_HU" => "Magyar (Hungarian)",
bb5d3960 93 "it_IT" => "Italiano",
1d004f12 94 "ja_JP" => "日本語 (Japanese)",
7b6c1ca7 95 "lv_LV" => "Latviešu",
592535d7 96 "nb_NO" => "Norwegian bokmål",
9e7f1f12 97 "nl_NL" => "Dutch",
ea45791a 98 "pl_PL" => "Polski",
d3b923c9 99 "ru_RU" => "Русский",
9a063469 100 "pt_BR" => "Portuguese/Brazil",
1c776ade 101 "pt_PT" => "Portuguese/Portugal",
5d608138 102 "zh_CN" => "Simplified Chinese",
8dc5e7f0 103 "zh_TW" => "Traditional Chinese",
2324f153 104 "sv_SE" => "Svenska",
76d78eb2 105 "fi_FI" => "Suomi",
42a5abdc 106 "tr_TR" => "Türkçe");
f8c612d4
AD
107
108 return $tr;
109 }
110
7b26a148
AD
111 require_once "lib/accept-to-gettext.php";
112 require_once "lib/gettext/gettext.inc";
aba609e0 113
7b26a148 114 function startup_gettext() {
8d505d78 115
7b26a148
AD
116 # Get locale from Accept-Language header
117 $lang = al2gt(array_keys(get_translations()), "text/html");
89cb787e 118
7b26a148
AD
119 if (defined('_TRANSLATION_OVERRIDE_DEFAULT')) {
120 $lang = _TRANSLATION_OVERRIDE_DEFAULT;
121 }
89cb787e 122
b18d109f
AD
123 if ($_SESSION["uid"] && get_schema_version() >= 120) {
124 $pref_lang = get_pref("USER_LANGUAGE", $_SESSION["uid"]);
1ee4900a 125
c4db796f 126 if ($pref_lang && $pref_lang != 'auto') {
7b149552
AD
127 $lang = $pref_lang;
128 }
7b26a148 129 }
7c33dbd4 130
7b26a148
AD
131 if ($lang) {
132 if (defined('LC_MESSAGES')) {
133 _setlocale(LC_MESSAGES, $lang);
134 } else if (defined('LC_ALL')) {
135 _setlocale(LC_ALL, $lang);
8d039718 136 }
aba609e0 137
d98e76d9 138 _bindtextdomain("messages", "locale");
865220a4 139
7b26a148
AD
140 _textdomain("messages");
141 _bind_textdomain_codeset("messages", "UTF-8");
865220a4 142 }
7b26a148
AD
143 }
144
b619ff15 145 require_once 'db-prefs.php';
8911ac8b 146 require_once 'version.php';
9549e33c 147 require_once 'controls.php';
40d13c28 148
fb850eec 149 define('SELF_USER_AGENT', 'Tiny Tiny RSS/' . VERSION . ' (http://tt-rss.org/)');
500943a4
AD
150 ini_set('user_agent', SELF_USER_AGENT);
151
7d96bfcd
AD
152 $schema_version = false;
153
4f71d743
AD
154 function _debug_suppress($suppress) {
155 global $suppress_debugging;
156
157 $suppress_debugging = $suppress;
158 }
159
45004d43
AD
160 /**
161 * Print a timestamped debug message.
8d505d78 162 *
45004d43
AD
163 * @param string $msg The debug message.
164 * @return void
165 */
68cccafc 166 function _debug($msg, $show = true) {
4f71d743
AD
167 global $suppress_debugging;
168
169 //echo "[$suppress_debugging] $msg $show\n";
170
171 if ($suppress_debugging) return false;
9ec10352 172
6f9e33e4 173 $ts = strftime("%H:%M:%S", time());
2a6a9395
AD
174 if (function_exists('posix_getpid')) {
175 $ts = "$ts/" . posix_getpid();
176 }
2191eb7a 177
68cccafc 178 if ($show && !(defined('QUIET') && QUIET)) {
2191eb7a
AD
179 print "[$ts] $msg\n";
180 }
181
182 if (defined('LOGFILE')) {
183 $fp = fopen(LOGFILE, 'a+');
184
185 if ($fp) {
a33558a6
AD
186 $locked = false;
187
188 if (function_exists("flock")) {
189 $tries = 0;
190
191 // try to lock logfile for writing
192 while ($tries < 5 && !$locked = flock($fp, LOCK_EX | LOCK_NB)) {
193 sleep(1);
194 ++$tries;
195 }
196
197 if (!$locked) {
198 fclose($fp);
199 return;
200 }
201 }
202
2191eb7a 203 fputs($fp, "[$ts] $msg\n");
a33558a6
AD
204
205 if (function_exists("flock")) {
206 flock($fp, LOCK_UN);
207 }
208
2191eb7a
AD
209 fclose($fp);
210 }
211 }
212
45004d43 213 } // function _debug
6f9e33e4 214
9632f884
AD
215 /**
216 * Purge a feed old posts.
8d505d78 217 *
9632f884
AD
218 * @param mixed $link A database connection.
219 * @param mixed $feed_id The id of the purged feed.
220 * @param mixed $purge_interval Olderness of purged posts.
221 * @param boolean $debug Set to True to enable the debug. False by default.
222 * @access public
223 * @return void
224 */
a42c55f0 225 function purge_feed($feed_id, $purge_interval, $debug = false) {
ad507f85 226
a42c55f0 227 if (!$purge_interval) $purge_interval = feed_purge_interval($feed_id);
8d505d78 228
8adb3ec4 229 $pdo = Db::pdo();
4c193675 230
8adb3ec4 231 $sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
4d13514d 232 $sth->execute([$feed_id]);
07d0efe9
AD
233
234 $owner_uid = false;
235
8adb3ec4
AD
236 if ($row = $sth->fetch()) {
237 $owner_uid = $row["owner_uid"];
07d0efe9
AD
238 }
239
ab954dff
AD
240 if ($purge_interval == -1 || !$purge_interval) {
241 if ($owner_uid) {
2ed0d6c4 242 CCache::update($feed_id, $owner_uid);
ab954dff
AD
243 }
244 return;
245 }
246
07d0efe9
AD
247 if (!$owner_uid) return;
248
3907ef71 249 if (FORCE_ARTICLE_PURGE == 0) {
a42c55f0 250 $purge_unread = get_pref("PURGE_UNREAD_ARTICLES",
3907ef71
AD
251 $owner_uid, false);
252 } else {
253 $purge_unread = true;
254 $purge_interval = FORCE_ARTICLE_PURGE;
255 }
07d0efe9 256
8adb3ec4
AD
257 if (!$purge_unread)
258 $query_limit = " unread = false AND ";
259 else
260 $query_limit = "";
07d0efe9 261
cab58c44
AD
262 $purge_interval = (int) $purge_interval;
263
fefa6ca3 264 if (DB_TYPE == "pgsql") {
8adb3ec4 265 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
6b3160cf
AD
266 USING ttrss_entries
267 WHERE ttrss_entries.id = ref_id AND
268 marked = false AND
8adb3ec4 269 feed_id = ? AND
6b3160cf 270 $query_limit
cab58c44
AD
271 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
272 $sth->execute([$feed_id]);
ad507f85 273
fefa6ca3 274 } else {
8adb3ec4 275 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
8d505d78
AD
276 USING ttrss_user_entries, ttrss_entries
277 WHERE ttrss_entries.id = ref_id AND
278 marked = false AND
8adb3ec4 279 feed_id = ? AND
07d0efe9 280 $query_limit
cab58c44
AD
281 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
282 $sth->execute([$feed_id]);
8adb3ec4 283
ad507f85
AD
284 }
285
8adb3ec4 286 $rows = $sth->rowCount();
3f6f0857 287
2ed0d6c4 288 CCache::update($feed_id, $owner_uid);
ced46404 289
ad507f85 290 if ($debug) {
6f9e33e4 291 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
fefa6ca3 292 }
2ea09bde
AD
293
294 return $rows;
9632f884 295 } // function purge_feed
fefa6ca3 296
a42c55f0 297 function feed_purge_interval($feed_id) {
07d0efe9 298
8adb3ec4
AD
299 $pdo = DB::pdo();
300
301 $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
302 WHERE id = ?");
303 $sth->execute([$feed_id]);
07d0efe9 304
8adb3ec4
AD
305 if ($row = $sth->fetch()) {
306 $purge_interval = $row["purge_interval"];
307 $owner_uid = $row["owner_uid"];
07d0efe9 308
6322ac79 309 if ($purge_interval == 0) $purge_interval = get_pref(
863be6ca 310 'PURGE_OLD_DAYS', $owner_uid);
07d0efe9
AD
311
312 return $purge_interval;
313
314 } else {
315 return -1;
316 }
317 }
318
465fb16d
AD
319 // TODO: multiple-argument way is deprecated, first parameter is a hash now
320 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
321 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
8d505d78 322
23d2471c 323 global $fetch_last_error;
7a01dc77 324 global $fetch_last_error_code;
cf0231f9 325 global $fetch_last_error_content;
ef39be2b 326 global $fetch_last_content_type;
153cb6d3 327 global $fetch_last_modified;
3f6f0857 328 global $fetch_curl_used;
33373eca 329
24c7e413
AD
330 $fetch_last_error = false;
331 $fetch_last_error_code = -1;
332 $fetch_last_error_content = "";
333 $fetch_last_content_type = "";
334 $fetch_curl_used = false;
153cb6d3 335 $fetch_last_modified = "";
24c7e413 336
c71add38 337 if (!is_array($options)) {
465fb16d
AD
338
339 // falling back on compatibility shim
153cb6d3 340 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
e934d63e
AD
341 $tmp = [];
342
343 for ($i = 0; $i < func_num_args(); $i++) {
344 $tmp[$option_names[$i]] = func_get_arg($i);
345 }
346
347 $options = $tmp;
348
349 /*$options = array(
465fb16d
AD
350 "url" => func_get_arg(0),
351 "type" => @func_get_arg(1),
352 "login" => @func_get_arg(2),
353 "pass" => @func_get_arg(3),
354 "post_query" => @func_get_arg(4),
355 "timeout" => @func_get_arg(5),
356 "timestamp" => @func_get_arg(6),
357 "useragent" => @func_get_arg(7)
e934d63e 358 ); */
465fb16d
AD
359 }
360
e3bc4591
AD
361 $url = $options["url"];
362 $type = isset($options["type"]) ? $options["type"] : false;
363 $login = isset($options["login"]) ? $options["login"] : false;
465fb16d 364 $pass = isset($options["pass"]) ? $options["pass"] : false;
e3bc4591
AD
365 $post_query = isset($options["post_query"]) ? $options["post_query"] : false;
366 $timeout = isset($options["timeout"]) ? $options["timeout"] : false;
153cb6d3 367 $last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
e3bc4591 368 $useragent = isset($options["useragent"]) ? $options["useragent"] : false;
fabfb9fc 369 $followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
465fb16d 370
32703cc6 371 $url = ltrim($url, ' ');
2375b6b6 372 $url = str_replace(' ', '%20', $url);
23d2471c 373
9fd58133
AD
374 if (strpos($url, "//") === 0)
375 $url = 'http:' . $url;
376
312742db 377 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
3f6f0857
AD
378
379 $fetch_curl_used = true;
b799dc8b 380
4c467026 381 $ch = curl_init($url);
a1af1574 382
153cb6d3 383 if ($last_modified && !$post_query) {
7a01dc77 384 curl_setopt($ch, CURLOPT_HTTPHEADER,
153cb6d3 385 array("If-Modified-Since: $last_modified"));
7a01dc77
AD
386 }
387
8401101d
BK
388 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
389 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : FILE_FETCH_TIMEOUT);
fabfb9fc 390 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
a1af1574
AD
391 curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
392 curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
393 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
153cb6d3 394 curl_setopt($ch, CURLOPT_HEADER, true);
5f6804bc 395 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
f826070c
AD
396 curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent :
397 SELF_USER_AGENT);
3f6f0857 398 curl_setopt($ch, CURLOPT_ENCODING, "");
1fd733c8 399 //curl_setopt($ch, CURLOPT_REFERER, $url);
0f6b9263 400
4c467026 401 if (!ini_get("open_basedir")) {
0f6b9263
AD
402 curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
403 }
8d505d78 404
ea55f2e1 405 if (defined('PROXY')) {
406 curl_setopt($ch, CURLOPT_PROXY, PROXY);
05f14a7d
AD
407 }
408
ae5f7bb1
AD
409 if ($post_query) {
410 curl_setopt($ch, CURLOPT_POST, true);
411 curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
412 }
413
8d505d78
AD
414 if ($login && $pass)
415 curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
a1af1574 416
153cb6d3
AD
417 $ret = @curl_exec($ch);
418
419 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
420 $headers = explode("\r\n", substr($ret, 0, $headers_length));
421 $contents = substr($ret, $headers_length);
422
423 foreach ($headers as $header) {
9d930af9
AD
424 if (strstr($header, ": ") !== FALSE) {
425 list ($key, $value) = explode(": ", $header);
426
427 if (strtolower($key) == "last-modified") {
428 $fetch_last_modified = $value;
429 }
430 }
431
432 if (substr(strtolower($header), 0, 7) == 'http/1.') {
433 $fetch_last_error_code = (int) substr($header, 9, 3);
434 $fetch_last_error = $header;
435 }
153cb6d3 436 }
268a06dc 437
48b657fc
AD
438 if (curl_errno($ch) === 23 || curl_errno($ch) === 61) {
439 curl_setopt($ch, CURLOPT_ENCODING, 'none');
440 $contents = @curl_exec($ch);
fb850eec
AD
441 }
442
8d505d78 443 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
ef39be2b 444 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
4065b60b 445
7a01dc77
AD
446 $fetch_last_error_code = $http_code;
447
ef39be2b 448 if ($http_code != 200 || $type && strpos($fetch_last_content_type, "$type") === false) {
9d930af9 449
fb850eec 450 if (curl_errno($ch) != 0) {
9d930af9 451 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
fb850eec 452 }
9d930af9 453
cf0231f9 454 $fetch_last_error_content = $contents;
fb850eec 455 curl_close($ch);
a1af1574
AD
456 return false;
457 }
4065b60b 458
153cb6d3
AD
459 if (!$contents) {
460 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
461 curl_close($ch);
462 return false;
463 }
464
fb850eec
AD
465 curl_close($ch);
466
a1af1574 467 return $contents;
4065b60b 468 } else {
3f6f0857
AD
469
470 $fetch_curl_used = false;
471
d3911f80 472 if ($login && $pass){
8d505d78
AD
473 $url_parts = array();
474
475 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
476
d3911f80
AD
477 $pass = urlencode($pass);
478
8d505d78
AD
479 if ($url_parts[1] && $url_parts[2]) {
480 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
481 }
482 }
483
3bba9c39
AD
484 // TODO: should this support POST requests or not? idk
485
ea55f2e1 486 $context_options = array(
487 'http' => array(
488 'method' => 'GET',
489 'ignore_errors' => true,
490 'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
491 'protocol_version'=> 1.1)
492 );
493
153cb6d3 494 if (!$post_query && $last_modified) {
ea55f2e1 495 $context_options['http']['header'] = "If-Modified-Since: $last_modified\r\n";
496 }
497
498 if (defined('PROXY')) {
499 $context_options['http']['proxy'] = PROXY;
7475580b 500 }
01311d86 501
ea55f2e1 502 $context = stream_context_create($context_options);
503
6122c449
AD
504 $old_error = error_get_last();
505
01311d86 506 $data = @file_get_contents($url, false, $context);
23d2471c 507
37ddf5b7 508 if (isset($http_response_header) && is_array($http_response_header)) {
9d930af9
AD
509 foreach ($http_response_header as $header) {
510 if (strstr($header, ": ") !== FALSE) {
511 list ($key, $value) = explode(": ", $header);
512
513 $key = strtolower($key);
514
515 if ($key == 'content-type') {
516 $fetch_last_content_type = $value;
517 // don't abort here b/c there might be more than one
518 // e.g. if we were being redirected -- last one is the right one
519 } else if ($key == 'last-modified') {
520 $fetch_last_modified = $value;
521 }
522 }
523
524 if (substr(strtolower($header), 0, 7) == 'http/1.') {
525 $fetch_last_error_code = (int) substr($header, 9, 3);
526 $fetch_last_error = $header;
01311d86 527 }
ef39be2b
MB
528 }
529 }
530
24c7e413 531 if ($fetch_last_error_code != 200) {
23d2471c 532 $error = error_get_last();
6122c449
AD
533
534 if ($error['message'] != $old_error['message']) {
9d930af9 535 $fetch_last_error .= "; " . $error["message"];
6122c449 536 }
24c7e413
AD
537
538 $fetch_last_error_content = $data;
539
540 return false;
23d2471c
AD
541 }
542 return $data;
4065b60b
AD
543 }
544
545 }
78800912 546
9632f884
AD
547 /**
548 * Try to determine the favicon URL for a feed.
549 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
550 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
8d505d78 551 *
9632f884
AD
552 * @param string $url A feed or page URL
553 * @access public
554 * @return mixed The favicon URL, or false if none was found.
555 */
1bd11fdf 556 function get_favicon_url($url) {
99331724 557
1bd11fdf 558 $favicon_url = false;
ed214298 559
4065b60b 560 if ($html = @fetch_file_contents($url)) {
78800912 561
ed214298 562 libxml_use_internal_errors(true);
c798704b 563
ed214298
AD
564 $doc = new DOMDocument();
565 $doc->loadHTML($html);
566 $xpath = new DOMXPath($doc);
717f5e64 567
701c5a7e 568 $base = $xpath->query('/html/head/base[@href]');
a712429e 569 foreach ($base as $b) {
241f69e4 570 $url = rewrite_relative_url($url, $b->getAttribute("href"));
a712429e
AD
571 break;
572 }
573
1bd11fdf 574 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
ed214298
AD
575 if (count($entries) > 0) {
576 foreach ($entries as $entry) {
1bd11fdf
AD
577 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
578 break;
ed214298 579 }
8d505d78 580 }
4065b60b 581 }
c798704b 582
1bd11fdf
AD
583 if (!$favicon_url)
584 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
585
586 return $favicon_url;
587 } // function get_favicon_url
588
a42c55f0 589 function initialize_user_prefs($uid, $profile = false) {
ff485f1d 590
6322ac79 591 if (get_schema_version() < 63) $profile_qpart = "";
f9aa6a89 592
8adb3ec4 593 $pdo = DB::pdo();
fbe7cb0a 594 $in_nested_tr = false;
ff485f1d 595
fbe7cb0a
AD
596 try {
597 $pdo->beginTransaction();
598 } catch (Exception $e) {
599 $in_nested_tr = true;
600 }
fdda3e4e 601
8adb3ec4 602 $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
8d505d78 603
8adb3ec4
AD
604 $profile = $profile ? $profile : null;
605
606 $u_sth = $pdo->prepare("SELECT pref_name
8aa568b3 607 FROM ttrss_user_prefs WHERE owner_uid = :uid AND
cc9450c3 608 (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
b78a6f08 609 $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
ff485f1d
AD
610
611 $active_prefs = array();
612
8adb3ec4 613 while ($line = $u_sth->fetch()) {
8d505d78 614 array_push($active_prefs, $line["pref_name"]);
ff485f1d
AD
615 }
616
8adb3ec4 617 while ($line = $sth->fetch()) {
ff485f1d
AD
618 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
619// print "adding " . $line["pref_name"] . "<br>";
620
6322ac79 621 if (get_schema_version() < 63) {
8adb3ec4 622 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
8d505d78 623 (owner_uid,pref_name,value) VALUES
8adb3ec4
AD
624 (?, ?, ?)");
625 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
f9aa6a89
AD
626
627 } else {
8adb3ec4 628 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
8d505d78 629 (owner_uid,pref_name,value, profile) VALUES
8adb3ec4
AD
630 (?, ?, ?, ?)");
631 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
f9aa6a89 632 }
ff485f1d
AD
633
634 }
635 }
636
fbe7cb0a 637 if (!$in_nested_tr) $pdo->commit();
ff485f1d
AD
638
639 }
956c7629 640
8de8bfb8
AD
641 function get_ssl_certificate_id() {
642 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
643 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
644 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
645 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
646 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
647 }
ac617ebc
GG
648 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
649 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
650 $_SERVER["SSL_CLIENT_V_START"] .
651 $_SERVER["SSL_CLIENT_V_END"] .
652 $_SERVER["SSL_CLIENT_S_DN"]);
653 }
8de8bfb8
AD
654 return "";
655 }
656
a42c55f0 657 function authenticate_user($login, $password, $check_only = false) {
c8437f35 658
131b01b3 659 if (!SINGLE_USER_MODE) {
0d421af8 660 $user_id = false;
0f28f81f 661
1ffe3391 662 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
0f28f81f
AD
663
664 $user_id = (int) $plugin->authenticate($login, $password);
665
666 if ($user_id) {
667 $_SESSION["auth_module"] = strtolower(get_class($plugin));
668 break;
669 }
461766f3
AD
670 }
671
0d421af8 672 if ($user_id && !$check_only) {
70400171
AD
673 @session_start();
674
0d421af8 675 $_SESSION["uid"] = $user_id;
3472c4c5 676 $_SESSION["version"] = VERSION_STATIC;
0d421af8 677
8adb3ec4
AD
678 $pdo = DB::pdo();
679 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
680 WHERE id = ?");
681 $sth->execute([$user_id]);
682 $row = $sth->fetch();
8d505d78 683
8adb3ec4
AD
684 $_SESSION["name"] = $row["login"];
685 $_SESSION["access_level"] = $row["access_level"];
3ceb893f 686 $_SESSION["csrf_token"] = uniqid_short();
8d505d78 687
8adb3ec4
AD
688 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
689 $usth->execute([$user_id]);
8d505d78 690
131b01b3 691 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
837ec70e 692 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
8adb3ec4 693 $_SESSION["pwd_hash"] = $row["pwd_hash"];
91c5f229
AD
694
695 $_SESSION["last_version_check"] = time();
8d505d78 696
a42c55f0 697 initialize_user_prefs($_SESSION["uid"]);
8d505d78 698
131b01b3
AD
699 return true;
700 }
8d505d78 701
131b01b3 702 return false;
503eb349 703
131b01b3 704 } else {
503eb349 705
131b01b3
AD
706 $_SESSION["uid"] = 1;
707 $_SESSION["name"] = "admin";
787e5ebc 708 $_SESSION["access_level"] = 10;
21e42e5f 709
0d421af8
AD
710 $_SESSION["hide_hello"] = true;
711 $_SESSION["hide_logout"] = true;
712
d5fd183d
AD
713 $_SESSION["auth_module"] = false;
714
21e42e5f 715 if (!$_SESSION["csrf_token"]) {
3ceb893f 716 $_SESSION["csrf_token"] = uniqid_short();
21e42e5f 717 }
f557cd78 718
0bbba72d 719 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
8d505d78 720
a42c55f0 721 initialize_user_prefs($_SESSION["uid"]);
8d505d78 722
c8437f35
AD
723 return true;
724 }
c8437f35
AD
725 }
726
e6532439
AD
727 // this is used for user http parameters unless HTML code is actually needed
728 function clean($param) {
729 if (is_array($param)) {
56c22162 730 return array_map("strip_tags", $param);
e6532439
AD
731 } else if (is_string($param)) {
732 return strip_tags($param);
733 } else {
734 return $param;
735 }
736 }
737
e6cb77a0
AD
738 function make_password($length = 8) {
739
85db6213
AD
740 $password = "";
741 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
742
743 $i = 0;
744
745 while ($i < $length) {
746 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
747
748 if (!strstr($password, $char)) {
749 $password .= $char;
750 $i++;
751 }
752 }
753 return $password;
e6cb77a0
AD
754 }
755
756 // this is called after user is created to initialize default feeds, labels
757 // or whatever else
8d505d78 758
e6cb77a0
AD
759 // user preferences are checked on every login, not here
760
a42c55f0 761 function initialize_user($uid) {
e6cb77a0 762
8adb3ec4
AD
763 $pdo = DB::pdo();
764
765 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
766 values (?, 'Tiny Tiny RSS: Forum',
f0855b88 767 'http://tt-rss.org/forum/rss.php')");
8adb3ec4 768 $sth->execute([$uid]);
3b0feb9b 769 }
e6cb77a0 770
b8aa49bc 771 function logout_user() {
5ccc1cf5
AD
772 session_destroy();
773 if (isset($_COOKIE[session_name()])) {
774 setcookie(session_name(), '', time()-42000, '/');
775 }
b8aa49bc
AD
776 }
777
8484ce22
AD
778 function validate_csrf($csrf_token) {
779 return $csrf_token == $_SESSION['csrf_token'];
780 }
781
5cbd1fe8
AD
782 function load_user_plugins($owner_uid, $pluginhost = false) {
783
784 if (!$pluginhost) $pluginhost = PluginHost::getInstance();
785
6d45a152 786 if ($owner_uid && SCHEMA_VERSION >= 100) {
a42c55f0 787 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
de612e7a 788
5cbd1fe8 789 $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
e9c04fd4 790
6322ac79 791 if (get_schema_version() > 100) {
5cbd1fe8 792 $pluginhost->load_data();
e9c04fd4 793 }
de612e7a
AD
794 }
795 }
796
6322ac79 797 function login_sequence() {
8adb3ec4
AD
798 $pdo = Db::pdo();
799
97acbaf1 800 if (SINGLE_USER_MODE) {
25db6c51 801 @session_start();
a42c55f0 802 authenticate_user("admin", null);
68349f55 803 startup_gettext();
a42c55f0 804 load_user_plugins($_SESSION["uid"]);
97acbaf1 805 } else {
6322ac79 806 if (!validate_session()) $_SESSION["uid"] = false;
d0eef2a3
AD
807
808 if (!$_SESSION["uid"]) {
97acbaf1 809
a42c55f0
AD
810 if (AUTH_AUTO_LOGIN && authenticate_user(null, null)) {
811 $_SESSION["ref_schema_version"] = get_schema_version(true);
97acbaf1 812 } else {
a42c55f0 813 authenticate_user(null, null, true);
97acbaf1
AD
814 }
815
d0eef2a3 816 if (!$_SESSION["uid"]) {
d0eef2a3
AD
817 @session_destroy();
818 setcookie(session_name(), '', time()-42000, '/');
9ce7a554 819
6322ac79 820 render_login_form();
d0eef2a3
AD
821 exit;
822 }
4ad99f23 823
97acbaf1
AD
824 } else {
825 /* bump login timestamp */
8adb3ec4
AD
826 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
827 $sth->execute([$_SESSION['uid']]);
828
06b0777f 829 $_SESSION["last_login_update"] = time();
01a87dff
AD
830 }
831
de612e7a 832 if ($_SESSION["uid"]) {
7b149552 833 startup_gettext();
a42c55f0 834 load_user_plugins($_SESSION["uid"]);
b1b1d25f
AD
835
836 /* cleanup ccache */
837
8adb3ec4
AD
838 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
839 AND
b1b1d25f
AD
840 (SELECT COUNT(id) FROM ttrss_feeds WHERE
841 ttrss_feeds.id = feed_id) = 0");
842
a21f7495 843 $sth->execute([$_SESSION['uid']]);
8adb3ec4
AD
844
845 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
846 AND
b1b1d25f
AD
847 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
848 ttrss_feed_categories.id = feed_id) = 0");
849
a21f7495 850 $sth->execute([$_SESSION['uid']]);
de612e7a 851 }
b1b1d25f 852
b8aa49bc 853 }
afc3cf55 854 }
3547842a 855
411fe209 856 function truncate_string($str, $max_len, $suffix = '&hellip;') {
878a0083 857 if (mb_strlen($str, "utf-8") > $max_len) {
411fe209 858 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
3547842a
AD
859 } else {
860 return $str;
861 }
862 }
54a60e1a 863
cc43e19b
AD
864 // is not utf8 clean
865 function truncate_middle($str, $max_len, $suffix = '&hellip;') {
866 if (strlen($str) > $max_len) {
867 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
868 } else {
869 return $str;
870 }
871 }
872
ab4b768f
AD
873 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
874
875 try {
876 $source_tz = new DateTimeZone($source_tz);
877 } catch (Exception $e) {
878 $source_tz = new DateTimeZone('UTC');
879 }
880
881 try {
882 $dest_tz = new DateTimeZone($dest_tz);
883 } catch (Exception $e) {
884 $dest_tz = new DateTimeZone('UTC');
885 }
886
887 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
888 return $dt->format('U') + $dest_tz->getOffset($dt);
889 }
890
a42c55f0 891 function make_local_datetime($timestamp, $long, $owner_uid = false,
b6714c77 892 $no_smart_dt = false, $eta_min = false) {
324944f3
AD
893
894 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
895 if (!$timestamp) $timestamp = '1970-01-01 0:00';
896
7d96bfcd 897 global $utc_tz;
5ddef5ba
AD
898 global $user_tz;
899
900 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
324944f3 901
90e5f4f1
AD
902 $timestamp = substr($timestamp, 0, 19);
903
7d96bfcd
AD
904 # We store date in UTC internally
905 $dt = new DateTime($timestamp, $utc_tz);
906
5ddef5ba 907 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
7d96bfcd 908
6bfc97da 909 if ($user_tz_string != 'Automatic') {
324944f3 910
6bfc97da
AD
911 try {
912 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
913 } catch (Exception $e) {
914 $user_tz = $utc_tz;
915 }
916
917 $tz_offset = $user_tz->getOffset($dt);
918 } else {
96f0cbe3 919 $tz_offset = (int) -$_SESSION["clientTzOffset"];
6bfc97da 920 }
5ddef5ba 921
7d96bfcd 922 $user_timestamp = $dt->format('U') + $tz_offset;
324944f3 923
1dc52ae7 924 if (!$no_smart_dt) {
a42c55f0 925 return smart_date_time($user_timestamp,
b6714c77 926 $tz_offset, $owner_uid, $eta_min);
324944f3
AD
927 } else {
928 if ($long)
a42c55f0 929 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
324944f3 930 else
a42c55f0 931 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
324944f3
AD
932
933 return date($format, $user_timestamp);
934 }
935 }
936
b6714c77 937 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
2a5c136e
AD
938 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
939
97aa917c 940 if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
46973af5
AD
941 return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
942 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
be773442 943 return date("G:i", $timestamp);
2a5c136e 944 } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
a42c55f0 945 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
2a5c136e 946 return date($format, $timestamp);
be773442 947 } else {
a42c55f0 948 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
2a5c136e 949 return date($format, $timestamp);
be773442
AD
950 }
951 }
952
e3c99f3b 953 function sql_bool_to_bool($s) {
0002e598 954 return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
e3c99f3b 955 }
8d505d78 956
badac687 957 function bool_to_sql_bool($s) {
76fc7a2d 958 return $s ? 1 : 0;
badac687 959 }
e3c99f3b 960
fcfa9ef1
AD
961 // Session caching removed due to causing wrong redirects to upgrade
962 // script when get_schema_version() is called on an obsolete session
963 // created on a previous schema version.
a42c55f0 964 function get_schema_version($nocache = false) {
7d96bfcd
AD
965 global $schema_version;
966
8adb3ec4
AD
967 $pdo = DB::pdo();
968
e2cf81e2 969 if (!$schema_version && !$nocache) {
8adb3ec4
AD
970 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
971 $version = $row["schema_version"];
7d96bfcd 972 $schema_version = $version;
199db684 973 return $version;
7d96bfcd
AD
974 } else {
975 return $schema_version;
976 }
e4c51a6c
AD
977 }
978
6322ac79 979 function sanity_check() {
31303c6b 980 require_once 'errors.php';
cacc1877 981 global $ERRORS;
ebb948c2 982
6043fb7e 983 $error_code = 0;
a42c55f0 984 $schema_version = get_schema_version(true);
6043fb7e
AD
985
986 if ($schema_version != SCHEMA_VERSION) {
987 $error_code = 5;
988 }
989
ebb948c2 990 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
6043fb7e
AD
991 }
992
27981ca3 993 function file_is_locked($filename) {
8ff2a86c
AD
994 if (file_exists(LOCK_DIRECTORY . "/$filename")) {
995 if (function_exists('flock')) {
996 $fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
997 if ($fp) {
998 if (flock($fp, LOCK_EX | LOCK_NB)) {
999 flock($fp, LOCK_UN);
1000 fclose($fp);
1001 return false;
1002 }
31a6d42d 1003 fclose($fp);
8ff2a86c
AD
1004 return true;
1005 } else {
31a6d42d
AD
1006 return false;
1007 }
27981ca3 1008 }
8ff2a86c
AD
1009 return true; // consider the file always locked and skip the test
1010 } else {
1011 return false;
27981ca3 1012 }
27981ca3
AD
1013 }
1014
8ff2a86c 1015
fcb4c0c9 1016 function make_lockfile($filename) {
cfa43e02 1017 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
fcb4c0c9 1018
a44bfcfd 1019 if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
58fc7095
AD
1020 $stat_h = fstat($fp);
1021 $stat_f = stat(LOCK_DIRECTORY . "/$filename");
1022
1fcebfb3
AD
1023 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
1024 if ($stat_h["ino"] != $stat_f["ino"] ||
1025 $stat_h["dev"] != $stat_f["dev"]) {
1026
1027 return false;
1028 }
58fc7095
AD
1029 }
1030
4c59adb1
AD
1031 if (function_exists('posix_getpid')) {
1032 fwrite($fp, posix_getpid() . "\n");
1033 }
fcb4c0c9
AD
1034 return $fp;
1035 } else {
1036 return false;
1037 }
1038 }
1039
bf7fcde8 1040 function make_stampfile($filename) {
cfa43e02 1041 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
bf7fcde8 1042
8e00ae9b 1043 if (flock($fp, LOCK_EX | LOCK_NB)) {
bf7fcde8 1044 fwrite($fp, time() . "\n");
8e00ae9b 1045 flock($fp, LOCK_UN);
bf7fcde8
AD
1046 fclose($fp);
1047 return true;
1048 } else {
1049 return false;
1050 }
1051 }
1052
894ebcf5 1053 function sql_random_function() {
8c0496f7 1054 if (DB_TYPE == "mysql") {
894ebcf5
AD
1055 return "RAND()";
1056 } else {
1057 return "RANDOM()";
1058 }
1059 }
1060
ff5cc7d7 1061 function getFeedUnread($feed, $is_cat = false) {
86a8351c 1062 return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
ff5cc7d7
AD
1063 }
1064
ff5cc7d7 1065 function checkbox_to_sql_bool($val) {
8ff3cbb3 1066 return ($val == "on") ? 1 : 0;
ff5cc7d7
AD
1067 }
1068
3ceb893f
AD
1069 function uniqid_short() {
1070 return uniqid(base_convert(rand(), 10, 36));
1071 }
1072
aeb1abed
AD
1073 function make_init_params() {
1074 $params = array();
1075
1076 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1077 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1078 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1079 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1080
1081 $params[strtolower($param)] = (int) get_pref($param);
1082 }
1083
1084 $params["icons_url"] = ICONS_URL;
1085 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
1086 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1087 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1088 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1089 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
7c0eb1b6 1090 $params["is_default_pw"] = Pref_Prefs::isdefaultpassword();
aeb1abed
AD
1091 $params["label_base_index"] = (int) LABEL_BASE_INDEX;
1092
1093 $theme = get_pref( "USER_CSS_THEME", false, false);
1094 $params["theme"] = theme_valid("$theme") ? $theme : "";
1095
1096 $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
1097
1098 $params["php_platform"] = PHP_OS;
1099 $params["php_version"] = PHP_VERSION;
1100
1101 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1102
8adb3ec4
AD
1103 $pdo = Db::pdo();
1104
1105 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1106 ttrss_feeds WHERE owner_uid = ?");
1107 $sth->execute([$_SESSION['uid']]);
1108 $row = $sth->fetch();
aeb1abed 1109
8adb3ec4
AD
1110 $max_feed_id = $row["mid"];
1111 $num_feeds = $row["nf"];
aeb1abed
AD
1112
1113 $params["max_feed_id"] = (int) $max_feed_id;
1114 $params["num_feeds"] = (int) $num_feeds;
1115
1116 $params["hotkeys"] = get_hotkeys_map();
1117
1118 $params["csrf_token"] = $_SESSION["csrf_token"];
1119 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1120
1121 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
1122
1123 $params["icon_alert"] = base64_img("images/alert.png");
1124 $params["icon_information"] = base64_img("images/information.png");
1125 $params["icon_cross"] = base64_img("images/cross.png");
1126 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1127
5e78b0c2
AD
1128 $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1129
aeb1abed
AD
1130 return $params;
1131 }
1132
1133 function get_hotkeys_info() {
1134 $hotkeys = array(
1135 __("Navigation") => array(
1136 "next_feed" => __("Open next feed"),
1137 "prev_feed" => __("Open previous feed"),
1138 "next_article" => __("Open next article"),
1139 "prev_article" => __("Open previous article"),
1140 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1141 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1142 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1143 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1144 "search_dialog" => __("Show search dialog")),
1145 __("Article") => array(
1146 "toggle_mark" => __("Toggle starred"),
1147 "toggle_publ" => __("Toggle published"),
1148 "toggle_unread" => __("Toggle unread"),
1149 "edit_tags" => __("Edit tags"),
1150 "open_in_new_window" => __("Open in new window"),
1151 "catchup_below" => __("Mark below as read"),
1152 "catchup_above" => __("Mark above as read"),
1153 "article_scroll_down" => __("Scroll down"),
1154 "article_scroll_up" => __("Scroll up"),
1155 "select_article_cursor" => __("Select article under cursor"),
1156 "email_article" => __("Email article"),
1157 "close_article" => __("Close/collapse article"),
1158 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1159 "toggle_widescreen" => __("Toggle widescreen mode"),
1160 "toggle_embed_original" => __("Toggle embed original")),
1161 __("Article selection") => array(
1162 "select_all" => __("Select all articles"),
1163 "select_unread" => __("Select unread"),
1164 "select_marked" => __("Select starred"),
1165 "select_published" => __("Select published"),
1166 "select_invert" => __("Invert selection"),
1167 "select_none" => __("Deselect everything")),
1168 __("Feed") => array(
1169 "feed_refresh" => __("Refresh current feed"),
1170 "feed_unhide_read" => __("Un/hide read feeds"),
1171 "feed_subscribe" => __("Subscribe to feed"),
1172 "feed_edit" => __("Edit feed"),
1173 "feed_catchup" => __("Mark as read"),
1174 "feed_reverse" => __("Reverse headlines"),
1175 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1176 "feed_debug_update" => __("Debug feed update"),
1177 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1178 "catchup_all" => __("Mark all feeds as read"),
1179 "cat_toggle_collapse" => __("Un/collapse current category"),
1180 "toggle_combined_mode" => __("Toggle combined mode"),
1181 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1182 __("Go to") => array(
1183 "goto_all" => __("All articles"),
1184 "goto_fresh" => __("Fresh"),
1185 "goto_marked" => __("Starred"),
1186 "goto_published" => __("Published"),
1187 "goto_tagcloud" => __("Tag cloud"),
1188 "goto_prefs" => __("Preferences")),
1189 __("Other") => array(
1190 "create_label" => __("Create label"),
1191 "create_filter" => __("Create filter"),
1192 "collapse_sidebar" => __("Un/collapse sidebar"),
1193 "help_dialog" => __("Show help dialog"))
1194 );
1195
1196 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1197 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1198 }
1199
1200 return $hotkeys;
1201 }
1202
1203 function get_hotkeys_map() {
1204 $hotkeys = array(
1205 // "navigation" => array(
1206 "k" => "next_feed",
1207 "j" => "prev_feed",
1208 "n" => "next_article",
1209 "p" => "prev_article",
1210 "(38)|up" => "prev_article",
1211 "(40)|down" => "next_article",
1212 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1213 // "^(40)|Ctrl-down" => "next_article_noscroll",
1214 "(191)|/" => "search_dialog",
1215 // "article" => array(
1216 "s" => "toggle_mark",
1217 "*s" => "toggle_publ",
1218 "u" => "toggle_unread",
1219 "*t" => "edit_tags",
1220 "o" => "open_in_new_window",
1221 "c p" => "catchup_below",
1222 "c n" => "catchup_above",
1223 "*n" => "article_scroll_down",
1224 "*p" => "article_scroll_up",
1225 "*(38)|Shift+up" => "article_scroll_up",
1226 "*(40)|Shift+down" => "article_scroll_down",
1227 "a *w" => "toggle_widescreen",
1228 "a e" => "toggle_embed_original",
1229 "e" => "email_article",
1230 "a q" => "close_article",
1231 // "article_selection" => array(
1232 "a a" => "select_all",
1233 "a u" => "select_unread",
1234 "a *u" => "select_marked",
1235 "a p" => "select_published",
1236 "a i" => "select_invert",
1237 "a n" => "select_none",
1238 // "feed" => array(
1239 "f r" => "feed_refresh",
1240 "f a" => "feed_unhide_read",
1241 "f s" => "feed_subscribe",
1242 "f e" => "feed_edit",
1243 "f q" => "feed_catchup",
1244 "f x" => "feed_reverse",
1245 "f g" => "feed_toggle_vgroup",
1246 "f *d" => "feed_debug_update",
1247 "f *g" => "feed_debug_viewfeed",
1248 "f *c" => "toggle_combined_mode",
1249 "f c" => "toggle_cdm_expanded",
1250 "*q" => "catchup_all",
1251 "x" => "cat_toggle_collapse",
1252 // "goto" => array(
1253 "g a" => "goto_all",
1254 "g f" => "goto_fresh",
1255 "g s" => "goto_marked",
1256 "g p" => "goto_published",
1257 "g t" => "goto_tagcloud",
1258 "g *p" => "goto_prefs",
1259 // "other" => array(
1260 "(9)|Tab" => "select_article_cursor", // tab
1261 "c l" => "create_label",
1262 "c f" => "create_filter",
1263 "c s" => "collapse_sidebar",
1264 "^(191)|Ctrl+/" => "help_dialog",
1265 );
1266
1267 if (get_pref('COMBINED_DISPLAY_MODE')) {
1268 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1269 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1270 }
1271
1272 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1273 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1274 }
1275
1276 $prefixes = array();
1277
1278 foreach (array_keys($hotkeys) as $hotkey) {
1279 $pair = explode(" ", $hotkey, 2);
1280
1281 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1282 array_push($prefixes, $pair[0]);
1283 }
1284 }
1285
1286 return array($prefixes, $hotkeys);
1287 }
1288
1289 function check_for_update() {
1290 if (defined("GIT_VERSION_TIMESTAMP")) {
1291 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1292
1293 if ($content) {
1294 $content = json_decode($content, true);
1295
1296 if ($content && isset($content["changeset"])) {
1297 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
1298 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
1299
1300 return $content["changeset"]["id"];
1301 }
1302 }
1303 }
1304 }
1305
1306 return "";
1307 }
1308
1309 function make_runtime_info($disable_update_check = false) {
1310 $data = array();
1311
8adb3ec4
AD
1312 $pdo = Db::pdo();
1313
1314 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1315 ttrss_feeds WHERE owner_uid = ?");
1316 $sth->execute([$_SESSION['uid']]);
1317 $row = $sth->fetch();
aeb1abed 1318
8adb3ec4
AD
1319 $max_feed_id = $row['mid'];
1320 $num_feeds = $row['nf'];
aeb1abed
AD
1321
1322 $data["max_feed_id"] = (int) $max_feed_id;
1323 $data["num_feeds"] = (int) $num_feeds;
1324
1325 $data['last_article_id'] = Article::getLastArticleId();
1326 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1327
1328 $data['dep_ts'] = calculate_dep_timestamp();
1329 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1330
5e78b0c2 1331 $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
aeb1abed
AD
1332
1333 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1334 $update_result = @check_for_update();
1335
1336 $data["update_result"] = $update_result;
1337
1338 $_SESSION["last_version_check"] = time();
1339 }
1340
1341 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1342
1343 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1344
1345 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1346
1347 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1348
1349 if ($stamp) {
1350 $stamp_delta = time() - $stamp;
1351
1352 if ($stamp_delta > 1800) {
1353 $stamp_check = 0;
1354 } else {
1355 $stamp_check = 1;
1356 $_SESSION["daemon_stamp_check"] = time();
1357 }
1358
1359 $data['daemon_stamp_ok'] = $stamp_check;
1360
1361 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1362
1363 $data['daemon_stamp'] = $stamp_fmt;
1364 }
1365 }
1366 }
1367
1368 return $data;
1369 }
1370
1371 function search_to_sql($search, $search_language) {
1372
1373 $keywords = str_getcsv(trim($search), " ");
1374 $query_keywords = array();
1375 $search_words = array();
1376 $search_query_leftover = array();
1377
cab58c44
AD
1378 $pdo = Db::pdo();
1379
aeb1abed 1380 if ($search_language)
cab58c44 1381 $search_language = $pdo->quote(mb_strtolower($search_language));
aeb1abed 1382 else
9274109c 1383 $search_language = $pdo->quote("english");
aeb1abed
AD
1384
1385 foreach ($keywords as $k) {
1386 if (strpos($k, "-") === 0) {
1387 $k = substr($k, 1);
1388 $not = "NOT";
1389 } else {
1390 $not = "";
1391 }
1392
1393 $commandpair = explode(":", mb_strtolower($k), 2);
1394
1395 switch ($commandpair[0]) {
1396 case "title":
1397 if ($commandpair[1]) {
a2d77092
AD
1398 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1399 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
aeb1abed
AD
1400 } else {
1401 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1e78803c 1402 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1403 array_push($search_words, $k);
1404 }
1405 break;
1406 case "author":
1407 if ($commandpair[1]) {
a2d77092
AD
1408 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1409 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
aeb1abed
AD
1410 } else {
1411 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1e78803c 1412 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1413 array_push($search_words, $k);
1414 }
1415 break;
1416 case "note":
1417 if ($commandpair[1]) {
1418 if ($commandpair[1] == "true")
1419 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1420 else if ($commandpair[1] == "false")
1421 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1422 else
a2d77092
AD
1423 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1424 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
aeb1abed 1425 } else {
1e78803c
AD
1426 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1427 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1428 if (!$not) array_push($search_words, $k);
1429 }
1430 break;
1431 case "star":
1432
1433 if ($commandpair[1]) {
1434 if ($commandpair[1] == "true")
1435 array_push($query_keywords, "($not (marked = true))");
1436 else
1437 array_push($query_keywords, "($not (marked = false))");
1438 } else {
1e78803c
AD
1439 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1440 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1441 if (!$not) array_push($search_words, $k);
1442 }
1443 break;
1444 case "pub":
1445 if ($commandpair[1]) {
1446 if ($commandpair[1] == "true")
1447 array_push($query_keywords, "($not (published = true))");
1448 else
1449 array_push($query_keywords, "($not (published = false))");
1450
1451 } else {
1452 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1e78803c 1453 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1454 if (!$not) array_push($search_words, $k);
1455 }
1456 break;
1457 case "unread":
1458 if ($commandpair[1]) {
1459 if ($commandpair[1] == "true")
1460 array_push($query_keywords, "($not (unread = true))");
1461 else
1462 array_push($query_keywords, "($not (unread = false))");
1463
1464 } else {
1e78803c
AD
1465 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1466 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1467 if (!$not) array_push($search_words, $k);
1468 }
1469 break;
1470 default:
1471 if (strpos($k, "@") === 0) {
1472
1473 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1474 $orig_ts = strtotime(substr($k, 1));
1475 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1476
1477 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1478
1479 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
1480 } else {
1481
1482 if (DB_TYPE == "pgsql") {
1483 $k = mb_strtolower($k);
1484 array_push($search_query_leftover, $not ? "!$k" : $k);
1485 } else {
1e78803c
AD
1486 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1487 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1488 }
1489
1490 if (!$not) array_push($search_words, $k);
1491 }
1492 }
1493 }
1494
1495 if (count($search_query_leftover) > 0) {
cab58c44 1496 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
aeb1abed
AD
1497
1498 if (DB_TYPE == "pgsql") {
1499 array_push($query_keywords,
1e78803c 1500 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
aeb1abed
AD
1501 }
1502
1503 }
1504
1505 $search_query_part = implode("AND", $query_keywords);
1506
1507 return array($search_query_part, $search_words);
1508 }
1509
1510 function iframe_whitelisted($entry) {
1511 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1512
1513 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1514
1515 if ($src) {
1516 foreach ($whitelist as $w) {
1517 if ($src == $w || $src == "www.$w")
1518 return true;
1519 }
1520 }
1521
1522 return false;
1523 }
1524
1525 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1526 if (!$owner) $owner = $_SESSION["uid"];
1527
1528 $res = trim($str); if (!$res) return '';
1529
1530 $charset_hack = '<head>
1531 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1532 </head>';
1533
1534 $res = trim($res); if (!$res) return '';
1535
1536 libxml_use_internal_errors(true);
1537
1538 $doc = new DOMDocument();
1539 $doc->loadHTML($charset_hack . $res);
1540 $xpath = new DOMXPath($doc);
1541
b2d42e96 1542 $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
aeb1abed
AD
1543
1544 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1545
1546 foreach ($entries as $entry) {
1547
1548 if ($entry->hasAttribute('href')) {
1549 $entry->setAttribute('href',
1550 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1551
1552 $entry->setAttribute('rel', 'noopener noreferrer');
1553 }
1554
1555 if ($entry->hasAttribute('src')) {
1556 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1557 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
1558
1559 if (file_exists($cached_filename)) {
1560
1561 // this is strictly cosmetic
1562 if ($entry->tagName == 'img') {
1563 $suffix = ".png";
1564 } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
1565 $suffix = ".mp4";
1566 } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
1567 $suffix = ".ogg";
1568 } else {
1569 $suffix = "";
1570 }
1571
1572 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1573
1574 if ($entry->hasAttribute('srcset')) {
1575 $entry->removeAttribute('srcset');
1576 }
1577
1578 if ($entry->hasAttribute('sizes')) {
1579 $entry->removeAttribute('sizes');
1580 }
1581 }
1582
1583 $entry->setAttribute('src', $src);
1584 }
1585
1586 if ($entry->nodeName == 'img') {
7651b6e2 1587 $entry->setAttribute('referrerpolicy', 'no-referrer');
aeb1abed
AD
1588
1589 if ($entry->hasAttribute('src')) {
1590 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1591
9f7bd151 1592 if (is_prefix_https() && !$is_https_url) {
aeb1abed
AD
1593
1594 if ($entry->hasAttribute('srcset')) {
1595 $entry->removeAttribute('srcset');
1596 }
1597
1598 if ($entry->hasAttribute('sizes')) {
1599 $entry->removeAttribute('sizes');
1600 }
1601 }
1602 }
1603
1604 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1605 $force_remove_images || $_SESSION["bw_limit"]) {
1606
1607 $p = $doc->createElement('p');
1608
1609 $a = $doc->createElement('a');
1610 $a->setAttribute('href', $entry->getAttribute('src'));
1611
1612 $a->appendChild(new DOMText($entry->getAttribute('src')));
1613 $a->setAttribute('target', '_blank');
1614 $a->setAttribute('rel', 'noopener noreferrer');
1615
1616 $p->appendChild($a);
1617
1618 $entry->parentNode->replaceChild($p, $entry);
1619 }
1620 }
1621
1622 if (strtolower($entry->nodeName) == "a") {
1623 $entry->setAttribute("target", "_blank");
1624 $entry->setAttribute("rel", "noopener noreferrer");
1625 }
1626 }
1627
1628 $entries = $xpath->query('//iframe');
1629 foreach ($entries as $entry) {
1630 if (!iframe_whitelisted($entry)) {
1631 $entry->setAttribute('sandbox', 'allow-scripts');
1632 } else {
9f7bd151 1633 if (is_prefix_https()) {
aeb1abed
AD
1634 $entry->setAttribute("src",
1635 str_replace("http://", "https://",
1636 $entry->getAttribute("src")));
1637 }
1638 }
1639 }
1640
1641 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1642 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1643 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1644 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1645 'dt', 'em', 'footer', 'figure', 'figcaption',
6eeeec48 1646 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
aeb1abed
AD
1647 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1648 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1649 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1650 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1651 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1652
1653 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1654
1655 $disallowed_attributes = array('id', 'style', 'class');
1656
1657 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1658 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1659 if (is_array($retval)) {
1660 $doc = $retval[0];
1661 $allowed_elements = $retval[1];
1662 $disallowed_attributes = $retval[2];
1663 } else {
1664 $doc = $retval;
1665 }
1666 }
1667
1668 $doc->removeChild($doc->firstChild); //remove doctype
1669 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1670
1671 if ($highlight_words) {
1672 foreach ($highlight_words as $word) {
1673
1674 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1675
1676 $elements = $xpath->query("//*/text()");
1677
1678 foreach ($elements as $child) {
1679
1680 $fragment = $doc->createDocumentFragment();
1681 $text = $child->textContent;
1682
1683 while (($pos = mb_stripos($text, $word)) !== false) {
1684 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1685 $word = mb_substr($text, $pos, mb_strlen($word));
1686 $highlight = $doc->createElement('span');
1687 $highlight->appendChild(new DomText($word));
1688 $highlight->setAttribute('class', 'highlight');
1689 $fragment->appendChild($highlight);
1690 $text = mb_substr($text, $pos + mb_strlen($word));
1691 }
1692
1693 if (!empty($text)) $fragment->appendChild(new DomText($text));
1694
1695 $child->parentNode->replaceChild($fragment, $child);
1696 }
1697 }
1698 }
1699
1700 $res = $doc->saveHTML();
1701
1702 /* strip everything outside of <body>...</body> */
1703
1704 $res_frag = array();
1705 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1706 return $res_frag[1];
1707 } else {
1708 return $res;
1709 }
1710 }
1711
1712 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1713 $xpath = new DOMXPath($doc);
1714 $entries = $xpath->query('//*');
1715
1716 foreach ($entries as $entry) {
1717 if (!in_array($entry->nodeName, $allowed_elements)) {
1718 $entry->parentNode->removeChild($entry);
1719 }
1720
1721 if ($entry->hasAttributes()) {
1722 $attrs_to_remove = array();
1723
1724 foreach ($entry->attributes as $attr) {
1725
1726 if (strpos($attr->nodeName, 'on') === 0) {
1727 array_push($attrs_to_remove, $attr);
1728 }
1729
1730 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1731 array_push($attrs_to_remove, $attr);
1732 }
1733
1734 if (in_array($attr->nodeName, $disallowed_attributes)) {
1735 array_push($attrs_to_remove, $attr);
1736 }
1737 }
1738
1739 foreach ($attrs_to_remove as $attr) {
1740 $entry->removeAttributeNode($attr);
1741 }
1742 }
1743 }
1744
1745 return $doc;
1746 }
1747
1748 function trim_array($array) {
1749 $tmp = $array;
1750 array_walk($tmp, 'trim');
1751 return $tmp;
1752 }
1753
1754 function tag_is_valid($tag) {
1755 if ($tag == '') return false;
1756 if (is_numeric($tag)) return false;
1757 if (mb_strlen($tag) > 250) return false;
1758
1759 if (!$tag) return false;
1760
1761 return true;
1762 }
1763
1764 function render_login_form() {
1765 header('Cache-Control: public');
1766
1767 require_once "login_form.php";
1768 exit;
1769 }
1770
1771 function T_sprintf() {
1772 $args = func_get_args();
1773 return vsprintf(__(array_shift($args)), $args);
1774 }
1775
1776 function print_checkpoint($n, $s) {
1777 $ts = microtime(true);
1778 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1779 return $ts;
1780 }
1781
1782 function sanitize_tag($tag) {
1783 $tag = trim($tag);
1784
1785 $tag = mb_strtolower($tag, 'utf-8');
1786
1787 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1788
1789 if (DB_TYPE == "mysql") {
1790 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1791 }
1792
1793 return $tag;
1794 }
1795
9f7bd151 1796 function is_server_https() {
e234ac8d 1797 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
9f7bd151
AD
1798 }
1799
1800 function is_prefix_https() {
1801 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1802 }
1803
b2d42e96 1804 // this returns SELF_URL_PATH sans ending slash
aeb1abed
AD
1805 function get_self_url_prefix() {
1806 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1807 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1808 } else {
1809 return SELF_URL_PATH;
1810 }
1811 }
1812
aeb1abed
AD
1813 function encrypt_password($pass, $salt = '', $mode2 = false) {
1814 if ($salt && $mode2) {
1815 return "MODE2:" . hash('sha256', $salt . $pass);
1816 } else if ($salt) {
1817 return "SHA1X:" . sha1("$salt:$pass");
1818 } else {
1819 return "SHA1:" . sha1($pass);
1820 }
1821 } // function encrypt_password
1822
1823 function load_filters($feed_id, $owner_uid) {
1824 $filters = array();
1825
c949a928 1826 $feed_id = (int) $feed_id;
0086a897 1827 $cat_id = (int)Feeds::getFeedCategory($feed_id);
aeb1abed
AD
1828
1829 if ($cat_id == 0)
1830 $null_cat_qpart = "cat_id IS NULL OR";
1831 else
1832 $null_cat_qpart = "";
1833
8adb3ec4
AD
1834 $pdo = Db::pdo();
1835
1836 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1837 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1838 $sth->execute([$owner_uid]);
aeb1abed 1839
02f3992a 1840 $check_cats = array_merge(
aeb1abed 1841 Feeds::getParentCategories($cat_id, $owner_uid),
02f3992a
AD
1842 [$cat_id]);
1843
1844 $check_cats_str = join(",", $check_cats);
1845 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
aeb1abed 1846
8adb3ec4 1847 while ($line = $sth->fetch()) {
aeb1abed
AD
1848 $filter_id = $line["id"];
1849
02f3992a
AD
1850 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1851
8adb3ec4 1852 $sth2 = $pdo->prepare("SELECT
02f3992a 1853 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
aeb1abed
AD
1854 FROM ttrss_filters2_rules AS r,
1855 ttrss_filter_types AS t
1856 WHERE
b2d42e96 1857 (match_on IS NOT NULL OR
02f3992a 1858 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
8adb3ec4
AD
1859 (feed_id IS NULL OR feed_id = ?))) AND
1860 filter_type = t.id AND filter_id = ?");
1861 $sth2->execute([$feed_id, $filter_id]);
aeb1abed
AD
1862
1863 $rules = array();
1864 $actions = array();
1865
8adb3ec4 1866 while ($rule_line = $sth2->fetch()) {
aeb1abed
AD
1867 # print_r($rule_line);
1868
02f3992a
AD
1869 if ($rule_line["match_on"]) {
1870 $match_on = json_decode($rule_line["match_on"], true);
aeb1abed 1871
02f3992a 1872 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
aeb1abed 1873
02f3992a
AD
1874 $rule = array();
1875 $rule["reg_exp"] = $rule_line["reg_exp"];
1876 $rule["type"] = $rule_line["type_name"];
1877 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1878
1879 array_push($rules, $rule);
1880 } else if (!$match_any_rule) {
1881 // this filter contains a rule that doesn't match to this feed/category combination
1882 // thus filter has to be rejected
aeb1abed 1883
02f3992a
AD
1884 $rules = [];
1885 break;
1886 }
aeb1abed 1887
02f3992a 1888 } else {
0bf7e007 1889
02f3992a
AD
1890 $rule = array();
1891 $rule["reg_exp"] = $rule_line["reg_exp"];
1892 $rule["type"] = $rule_line["type_name"];
1893 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1894
1895 array_push($rules, $rule);
1896 }
aeb1abed
AD
1897 }
1898
02f3992a 1899 if (count($rules) > 0) {
8adb3ec4 1900 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
02f3992a
AD
1901 FROM ttrss_filters2_actions AS a,
1902 ttrss_filter_actions AS t
1903 WHERE
8adb3ec4
AD
1904 action_id = t.id AND filter_id = ?");
1905 $sth2->execute([$filter_id]);
02f3992a 1906
8adb3ec4 1907 while ($action_line = $sth2->fetch()) {
02f3992a
AD
1908 # print_r($action_line);
1909
1910 $action = array();
1911 $action["type"] = $action_line["type_name"];
1912 $action["param"] = $action_line["action_param"];
1913
1914 array_push($actions, $action);
1915 }
1916 }
aeb1abed
AD
1917
1918 $filter = array();
1919 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1920 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1921 $filter["rules"] = $rules;
1922 $filter["actions"] = $actions;
1923
1924 if (count($rules) > 0 && count($actions) > 0) {
1925 array_push($filters, $filter);
1926 }
1927 }
1928
1929 return $filters;
1930 }
1931
1932 function get_score_pic($score) {
1933 if ($score > 100) {
1934 return "score_high.png";
1935 } else if ($score > 0) {
1936 return "score_half_high.png";
1937 } else if ($score < -100) {
1938 return "score_low.png";
1939 } else if ($score < 0) {
1940 return "score_half_low.png";
1941 } else {
1942 return "score_neutral.png";
1943 }
1944 }
1945
aeb1abed
AD
1946 function init_plugins() {
1947 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1948
1949 return true;
1950 }
1951
1952 function add_feed_category($feed_cat, $parent_cat_id = false) {
1953
1954 if (!$feed_cat) return false;
1955
aeb1abed 1956 $feed_cat = mb_substr($feed_cat, 0, 250);
ecf6baaa 1957 if (!$parent_cat_id) $parent_cat_id = null;
aeb1abed 1958
8adb3ec4 1959 $pdo = Db::pdo();
c949a928
AD
1960 $tr_in_progress = false;
1961
1962 try {
1963 $pdo->beginTransaction();
1964 } catch (Exception $e) {
1965 $tr_in_progress = true;
1966 }
8adb3ec4
AD
1967
1968 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
cc9450c3 1969 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
ecf6baaa 1970 AND title = :title AND owner_uid = :uid");
b78a6f08 1971 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
aeb1abed 1972
ecf6baaa 1973 if (!$sth->fetch()) {
aeb1abed 1974
b78a6f08
AD
1975 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1976 VALUES (?, ?, ?)");
1977 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
aeb1abed 1978
c949a928 1979 if (!$tr_in_progress) $pdo->commit();
aeb1abed
AD
1980
1981 return true;
1982 }
1983
fdda3e4e
AD
1984 $pdo->commit();
1985
aeb1abed
AD
1986 return false;
1987 }
1988
1989 /**
1990 * Fixes incomplete URLs by prepending "http://".
1991 * Also replaces feed:// with http://, and
1992 * prepends a trailing slash if the url is a domain name only.
1993 *
1994 * @param string $url Possibly incomplete URL
1995 *
1996 * @return string Fixed URL.
1997 */
1998 function fix_url($url) {
1999
2000 // support schema-less urls
2001 if (strpos($url, '//') === 0) {
2002 $url = 'https:' . $url;
2003 }
2004
2005 if (strpos($url, '://') === false) {
2006 $url = 'http://' . $url;
2007 } else if (substr($url, 0, 5) == 'feed:') {
2008 $url = 'http:' . substr($url, 5);
2009 }
2010
2011 //prepend slash if the URL has no slash in it
2012 // "http://www.example" -> "http://www.example/"
2013 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2014 $url .= '/';
2015 }
2016
2017 //convert IDNA hostname to punycode if possible
2018 if (function_exists("idn_to_ascii")) {
2019 $parts = parse_url($url);
2020 if (mb_detect_encoding($parts['host']) != 'ASCII')
2021 {
2022 $parts['host'] = idn_to_ascii($parts['host']);
2023 $url = build_url($parts);
2024 }
2025 }
2026
2027 if ($url != "http:///")
2028 return $url;
2029 else
2030 return '';
2031 }
2032
2033 function validate_feed_url($url) {
2034 $parts = parse_url($url);
2035
2036 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2037
2038 }
2039
2040 /* function save_email_address($email) {
2041 // FIXME: implement persistent storage of emails
2042
2043 if (!$_SESSION['stored_emails'])
2044 $_SESSION['stored_emails'] = array();
2045
2046 if (!in_array($email, $_SESSION['stored_emails']))
2047 array_push($_SESSION['stored_emails'], $email);
2048 } */
2049
2050
2051 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2052
2053 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2054
1271407e
AD
2055 $is_cat = bool_to_sql_bool($is_cat);
2056
a21f7495 2057 $pdo = Db::pdo();
aeb1abed 2058
1271407e
AD
2059 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2060 WHERE feed_id = ? AND is_cat = ?
a21f7495 2061 AND owner_uid = ?");
ed5cd6ea 2062 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
aeb1abed 2063
a21f7495
AD
2064 if ($row = $sth->fetch()) {
2065 return $row["access_key"];
aeb1abed 2066 } else {
a21f7495 2067 $key = uniqid_short();
aeb1abed 2068
a21f7495 2069 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
aeb1abed 2070 (access_key, feed_id, is_cat, owner_uid)
a21f7495
AD
2071 VALUES (?, ?, ?, ?)");
2072
7d960ce7 2073 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
aeb1abed
AD
2074
2075 return $key;
2076 }
aeb1abed
AD
2077 }
2078
2079 function get_feeds_from_html($url, $content)
2080 {
2081 $url = fix_url($url);
2082 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2083
2084 libxml_use_internal_errors(true);
2085
2086 $doc = new DOMDocument();
2087 $doc->loadHTML($content);
2088 $xpath = new DOMXPath($doc);
2089 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2090 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2091 $feedUrls = array();
2092 foreach ($entries as $entry) {
2093 if ($entry->hasAttribute('href')) {
2094 $title = $entry->getAttribute('title');
2095 if ($title == '') {
2096 $title = $entry->getAttribute('type');
2097 }
2098 $feedUrl = rewrite_relative_url(
2099 $baseUrl, $entry->getAttribute('href')
2100 );
2101 $feedUrls[$feedUrl] = $title;
2102 }
2103 }
2104 return $feedUrls;
2105 }
2106
2107 function is_html($content) {
2108 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2109 }
2110
2111 function url_is_html($url, $login = false, $pass = false) {
2112 return is_html(fetch_file_contents($url, false, $login, $pass));
2113 }
2114
2115 function build_url($parts) {
2116 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2117 }
2118
2119 function cleanup_url_path($path) {
2120 $path = str_replace("/./", "/", $path);
2121 $path = str_replace("//", "/", $path);
2122
2123 return $path;
2124 }
2125
2126 /**
2127 * Converts a (possibly) relative URL to a absolute one.
2128 *
2129 * @param string $url Base URL (i.e. from where the document is)
2130 * @param string $rel_url Possibly relative URL in the document
2131 *
2132 * @return string Absolute URL
2133 */
2134 function rewrite_relative_url($url, $rel_url) {
2135 if (strpos($rel_url, "://") !== false) {
2136 return $rel_url;
2137 } else if (strpos($rel_url, "//") === 0) {
2138 # protocol-relative URL (rare but they exist)
2139 return $rel_url;
2140 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2141 # magnet:, feed:, etc
2142 return $rel_url;
2143 } else if (strpos($rel_url, "/") === 0) {
2144 $parts = parse_url($url);
2145 $parts['path'] = $rel_url;
2146 $parts['path'] = cleanup_url_path($parts['path']);
2147
2148 return build_url($parts);
2149
2150 } else {
2151 $parts = parse_url($url);
2152 if (!isset($parts['path'])) {
2153 $parts['path'] = '/';
2154 }
2155 $dir = $parts['path'];
2156 if (substr($dir, -1) !== '/') {
2157 $dir = dirname($parts['path']);
2158 $dir !== '/' && $dir .= '/';
2159 }
2160 $parts['path'] = $dir . $rel_url;
2161 $parts['path'] = cleanup_url_path($parts['path']);
2162
2163 return build_url($parts);
2164 }
2165 }
2166
2167 function cleanup_tags($days = 14, $limit = 1000) {
2168
a21f7495
AD
2169 $days = (int) $days;
2170
aeb1abed
AD
2171 if (DB_TYPE == "pgsql") {
2172 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2173 } else if (DB_TYPE == "mysql") {
2174 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2175 }
2176
2177 $tags_deleted = 0;
2178
a21f7495
AD
2179 $pdo = Db::pdo();
2180
2181 while ($limit > 0) {
aeb1abed
AD
2182 $limit_part = 500;
2183
a21f7495 2184 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
aeb1abed
AD
2185 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2186 WHERE post_int_id = int_id AND $interval_query AND
a21f7495
AD
2187 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2188 $sth->execute([$limit]);
aeb1abed
AD
2189
2190 $ids = array();
2191
a21f7495 2192 while ($line = $sth->fetch()) {
aeb1abed
AD
2193 array_push($ids, $line['id']);
2194 }
2195
2196 if (count($ids) > 0) {
2197 $ids = join(",", $ids);
2198
a21f7495
AD
2199 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2200 $tags_deleted = $usth->rowCount();
aeb1abed
AD
2201 } else {
2202 break;
2203 }
2204
2205 $limit -= $limit_part;
2206 }
2207
2208 return $tags_deleted;
2209 }
2210
2211 function print_user_stylesheet() {
2212 $value = get_pref('USER_STYLESHEET');
2213
2214 if ($value) {
2215 print "<style type=\"text/css\">";
2216 print str_replace("<br/>", "\n", $value);
2217 print "</style>";
2218 }
2219
2220 }
2221
2222 function filter_to_sql($filter, $owner_uid) {
2223 $query = array();
2224
e4befe6b
AD
2225 $pdo = Db::pdo();
2226
aeb1abed
AD
2227 if (DB_TYPE == "pgsql")
2228 $reg_qpart = "~";
2229 else
2230 $reg_qpart = "REGEXP";
2231
2232 foreach ($filter["rules"] AS $rule) {
2233 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2234 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2235 $rule['reg_exp']) !== FALSE;
2236
2237 if ($regexp_valid) {
2238
e4befe6b 2239 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
aeb1abed
AD
2240
2241 switch ($rule["type"]) {
2242 case "title":
2243 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2244 $rule['reg_exp'] . "')";
2245 break;
2246 case "content":
2247 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2248 $rule['reg_exp'] . "')";
2249 break;
2250 case "both":
2251 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2252 $rule['reg_exp'] . "') OR LOWER(" .
2253 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2254 break;
2255 case "tag":
2256 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2257 $rule['reg_exp'] . "')";
2258 break;
2259 case "link":
2260 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2261 $rule['reg_exp'] . "')";
2262 break;
2263 case "author":
2264 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2265 $rule['reg_exp'] . "')";
2266 break;
2267 }
2268
2269 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2270
2271 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
e4befe6b 2272 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
aeb1abed
AD
2273 }
2274
2275 if (isset($rule["cat_id"])) {
2276
2277 if ($rule["cat_id"] > 0) {
2278 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2279 array_push($children, $rule["cat_id"]);
bed2d6e0 2280 $children = array_map("intval", $children);
aeb1abed
AD
2281
2282 $children = join(",", $children);
2283
2284 $cat_qpart = "cat_id IN ($children)";
2285 } else {
2286 $cat_qpart = "cat_id IS NULL";
2287 }
2288
2289 $qpart .= " AND $cat_qpart";
2290 }
2291
2292 $qpart .= " AND feed_id IS NOT NULL";
2293
2294 array_push($query, "($qpart)");
2295
2296 }
2297 }
2298
2299 if (count($query) > 0) {
2300 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2301 } else {
2302 $fullquery = "(false)";
2303 }
2304
2305 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2306
2307 return $fullquery;
2308 }
2309
2310 if (!function_exists('gzdecode')) {
2311 function gzdecode($string) { // no support for 2nd argument
2312 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2313 base64_encode($string));
2314 }
2315 }
2316
2317 function get_random_bytes($length) {
2318 if (function_exists('openssl_random_pseudo_bytes')) {
2319 return openssl_random_pseudo_bytes($length);
2320 } else {
2321 $output = "";
2322
2323 for ($i = 0; $i < $length; $i++)
2324 $output .= chr(mt_rand(0, 255));
2325
2326 return $output;
2327 }
2328 }
2329
2330 function read_stdin() {
2331 $fp = fopen("php://stdin", "r");
2332
2333 if ($fp) {
2334 $line = trim(fgets($fp));
2335 fclose($fp);
2336 return $line;
2337 }
2338
2339 return null;
2340 }
2341
aeb1abed
AD
2342 function implements_interface($class, $interface) {
2343 return in_array($interface, class_implements($class));
2344 }
2345
2346 function get_minified_js($files) {
2347 require_once 'lib/jshrink/Minifier.php';
2348
2349 $rv = '';
2350
2351 foreach ($files as $js) {
2352 if (!isset($_GET['debug'])) {
c4a08e4f 2353 $cached_file = CACHE_DIR . "/js/".basename($js);
aeb1abed 2354
c4a08e4f 2355 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
aeb1abed
AD
2356
2357 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2358
2359 if ($header && $contents) {
2360 list($htag, $hversion) = explode(":", $header);
2361
2362 if ($htag == "tt-rss" && $hversion == VERSION) {
2363 $rv .= $contents;
2364 continue;
2365 }
2366 }
2367 }
2368
c4a08e4f 2369 $minified = JShrink\Minifier::minify(file_get_contents("js/$js"));
aeb1abed
AD
2370 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2371 $rv .= $minified;
2372
2373 } else {
c4a08e4f 2374 $rv .= file_get_contents("js/$js"); // no cache in debug mode
aeb1abed
AD
2375 }
2376 }
2377
2378 return $rv;
2379 }
2380
2381 function calculate_dep_timestamp() {
2382 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2383
2384 $max_ts = -1;
2385
2386 foreach ($files as $file) {
2387 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2388 }
2389
2390 return $max_ts;
2391 }
2392
2393 function T_js_decl($s1, $s2) {
2394 if ($s1 && $s2) {
2395 $s1 = preg_replace("/\n/", "", $s1);
2396 $s2 = preg_replace("/\n/", "", $s2);
2397
2398 $s1 = preg_replace("/\"/", "\\\"", $s1);
2399 $s2 = preg_replace("/\"/", "\\\"", $s2);
2400
2401 return "T_messages[\"$s1\"] = \"$s2\";\n";
2402 }
2403 }
2404
2405 function init_js_translations() {
2406
2407 print 'var T_messages = new Object();
b2d42e96 2408
aeb1abed
AD
2409 function __(msg) {
2410 if (T_messages[msg]) {
2411 return T_messages[msg];
2412 } else {
2413 return msg;
2414 }
2415 }
b2d42e96 2416
aeb1abed
AD
2417 function ngettext(msg1, msg2, n) {
2418 return __((parseInt(n) > 1) ? msg2 : msg1);
2419 }';
2420
2421 $l10n = _get_reader();
2422
2423 for ($i = 0; $i < $l10n->total; $i++) {
2424 $orig = $l10n->get_original_string($i);
2425 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2426 $key = explode(chr(0), $orig);
2427 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2428 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2429 } else {
2430 $translation = __($orig);
2431 print T_js_decl($orig, $translation);
2432 }
2433 }
2434 }
2435
aeb1abed 2436 function get_theme_path($theme) {
bfebf57c
AD
2437 if ($theme == "default.php")
2438 return "css/default.css";
2439
aeb1abed
AD
2440 $check = "themes/$theme";
2441 if (file_exists($check)) return $check;
2442
2443 $check = "themes.local/$theme";
2444 if (file_exists($check)) return $check;
2445 }
2446
2447 function theme_valid($theme) {
2448 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2449
2450 if (in_array($theme, $bundled_themes)) return true;
2451
2452 $file = "themes/" . basename($theme);
2453
2454 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2455
2456 if (file_exists($file) && is_readable($file)) {
2457 $fh = fopen($file, "r");
2458
2459 if ($fh) {
2460 $header = fgets($fh);
2461 fclose($fh);
2462
2463 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2464 }
2465 }
2466
2467 return false;
2468 }
2469
2470 /**
2471 * @SuppressWarnings(unused)
2472 */
2473 function error_json($code) {
2474 require_once "errors.php";
2475
2476 @$message = $ERRORS[$code];
2477
2478 return json_encode(array("error" =>
2479 array("code" => $code, "message" => $message)));
2480
2481 }
2482
904aff76 2483 /*function abs_to_rel_path($dir) {
aeb1abed
AD
2484 $tmp = str_replace(dirname(__DIR__), "", $dir);
2485
2486 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2487
2488 return $tmp;
904aff76 2489 }*/
aeb1abed
AD
2490
2491 function get_upload_error_message($code) {
2492
2493 $errors = array(
2494 0 => __('There is no error, the file uploaded with success'),
2495 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2496 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2497 3 => __('The uploaded file was only partially uploaded'),
2498 4 => __('No file was uploaded'),
2499 6 => __('Missing a temporary folder'),
2500 7 => __('Failed to write file to disk.'),
2501 8 => __('A PHP extension stopped the file upload.'),
2502 );
2503
2504 return $errors[$code];
2505 }
2506
2507 function base64_img($filename) {
2508 if (file_exists($filename)) {
2509 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2510
2511 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2512 } else {
2513 return "";
2514 }
2515 }
2516
8b73bd28
AD
2517 /* this is essentially a wrapper for readfile() which allows plugins to hook
2518 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2519
2520 hook function should return true if request was handled (or at least attempted to)
2521
2522 note that this can be called without user context so the plugin to handle this
2523 should be loaded systemwide in config.php */
2524 function send_local_file($filename) {
2525 if (file_exists($filename)) {
2526 $tmppluginhost = new PluginHost();
2527
2528 $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
2529 $tmppluginhost->load_data();
2530
2531 foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
2532 if ($plugin->hook_send_local_file($filename)) return true;
2533 }
2534
2535 $mimetype = mime_content_type($filename);
2536 header("Content-type: $mimetype");
2537
2538 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2539 header("Last-Modified: $stamp", true);
2540
2541 return readfile($filename);
2542 } else {
2543 return false;
2544 }
2545 }
2546
0b68b162 2547 function check_mysql_tables() {
a21f7495 2548 $pdo = Db::pdo();
0b68b162 2549
a21f7495
AD
2550 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2551 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2552 $sth->execute([DB_NAME]);
0b68b162
AD
2553
2554 $bad_tables = [];
2555
a21f7495 2556 while ($line = $sth->fetch()) {
0b68b162
AD
2557 array_push($bad_tables, $line);
2558 }
2559
2560 return $bad_tables;
2561 }
2562
2cf93c04
AD
2563 function validate_field($string, $allowed, $default = "") {
2564 if (in_array($string, $allowed))
2565 return $string;
2566 else
2567 return $default;
2568 }
2569
90dafaa9
AD
2570 function arr_qmarks($arr) {
2571 return str_repeat('?,', count($arr) - 1) . '?';
2572 }