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