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