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