]> git.wh0rd.org - tt-rss.git/blame - include/functions.php
use xhrPost is even more places!
[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"),
1167 "toggle_combined_mode" => __("Toggle combined mode"),
1168 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1169 __("Go to") => array(
1170 "goto_all" => __("All articles"),
1171 "goto_fresh" => __("Fresh"),
1172 "goto_marked" => __("Starred"),
1173 "goto_published" => __("Published"),
1174 "goto_tagcloud" => __("Tag cloud"),
1175 "goto_prefs" => __("Preferences")),
1176 __("Other") => array(
1177 "create_label" => __("Create label"),
1178 "create_filter" => __("Create filter"),
1179 "collapse_sidebar" => __("Un/collapse sidebar"),
1180 "help_dialog" => __("Show help dialog"))
1181 );
1182
1183 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1184 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1185 }
1186
1187 return $hotkeys;
1188 }
1189
1190 function get_hotkeys_map() {
1191 $hotkeys = array(
1192 // "navigation" => array(
1193 "k" => "next_feed",
1194 "j" => "prev_feed",
1195 "n" => "next_article",
1196 "p" => "prev_article",
1197 "(38)|up" => "prev_article",
1198 "(40)|down" => "next_article",
1199 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1200 // "^(40)|Ctrl-down" => "next_article_noscroll",
1201 "(191)|/" => "search_dialog",
1202 // "article" => array(
1203 "s" => "toggle_mark",
1204 "*s" => "toggle_publ",
1205 "u" => "toggle_unread",
1206 "*t" => "edit_tags",
1207 "o" => "open_in_new_window",
1208 "c p" => "catchup_below",
1209 "c n" => "catchup_above",
1210 "*n" => "article_scroll_down",
1211 "*p" => "article_scroll_up",
1212 "*(38)|Shift+up" => "article_scroll_up",
1213 "*(40)|Shift+down" => "article_scroll_down",
1214 "a *w" => "toggle_widescreen",
1215 "a e" => "toggle_embed_original",
1216 "e" => "email_article",
1217 "a q" => "close_article",
1218 // "article_selection" => array(
1219 "a a" => "select_all",
1220 "a u" => "select_unread",
1221 "a *u" => "select_marked",
1222 "a p" => "select_published",
1223 "a i" => "select_invert",
1224 "a n" => "select_none",
1225 // "feed" => array(
1226 "f r" => "feed_refresh",
1227 "f a" => "feed_unhide_read",
1228 "f s" => "feed_subscribe",
1229 "f e" => "feed_edit",
1230 "f q" => "feed_catchup",
1231 "f x" => "feed_reverse",
1232 "f g" => "feed_toggle_vgroup",
1233 "f *d" => "feed_debug_update",
1234 "f *g" => "feed_debug_viewfeed",
1235 "f *c" => "toggle_combined_mode",
1236 "f c" => "toggle_cdm_expanded",
1237 "*q" => "catchup_all",
1238 "x" => "cat_toggle_collapse",
1239 // "goto" => array(
1240 "g a" => "goto_all",
1241 "g f" => "goto_fresh",
1242 "g s" => "goto_marked",
1243 "g p" => "goto_published",
1244 "g t" => "goto_tagcloud",
1245 "g *p" => "goto_prefs",
1246 // "other" => array(
1247 "(9)|Tab" => "select_article_cursor", // tab
1248 "c l" => "create_label",
1249 "c f" => "create_filter",
1250 "c s" => "collapse_sidebar",
1251 "^(191)|Ctrl+/" => "help_dialog",
1252 );
1253
1254 if (get_pref('COMBINED_DISPLAY_MODE')) {
1255 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1256 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1257 }
1258
1259 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1260 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1261 }
1262
1263 $prefixes = array();
1264
1265 foreach (array_keys($hotkeys) as $hotkey) {
1266 $pair = explode(" ", $hotkey, 2);
1267
1268 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1269 array_push($prefixes, $pair[0]);
1270 }
1271 }
1272
1273 return array($prefixes, $hotkeys);
1274 }
1275
1276 function check_for_update() {
1277 if (defined("GIT_VERSION_TIMESTAMP")) {
1278 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1279
1280 if ($content) {
1281 $content = json_decode($content, true);
1282
1283 if ($content && isset($content["changeset"])) {
1284 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
1285 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
1286
1287 return $content["changeset"]["id"];
1288 }
1289 }
1290 }
1291 }
1292
1293 return "";
1294 }
1295
1296 function make_runtime_info($disable_update_check = false) {
1297 $data = array();
1298
8adb3ec4
AD
1299 $pdo = Db::pdo();
1300
1301 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1302 ttrss_feeds WHERE owner_uid = ?");
1303 $sth->execute([$_SESSION['uid']]);
1304 $row = $sth->fetch();
aeb1abed 1305
8adb3ec4
AD
1306 $max_feed_id = $row['mid'];
1307 $num_feeds = $row['nf'];
aeb1abed
AD
1308
1309 $data["max_feed_id"] = (int) $max_feed_id;
1310 $data["num_feeds"] = (int) $num_feeds;
1311
1312 $data['last_article_id'] = Article::getLastArticleId();
1313 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1314
1315 $data['dep_ts'] = calculate_dep_timestamp();
1316 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1317
5e78b0c2 1318 $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
aeb1abed
AD
1319
1320 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1321 $update_result = @check_for_update();
1322
1323 $data["update_result"] = $update_result;
1324
1325 $_SESSION["last_version_check"] = time();
1326 }
1327
1328 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1329
1330 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1331
1332 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1333
1334 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1335
1336 if ($stamp) {
1337 $stamp_delta = time() - $stamp;
1338
1339 if ($stamp_delta > 1800) {
1340 $stamp_check = 0;
1341 } else {
1342 $stamp_check = 1;
1343 $_SESSION["daemon_stamp_check"] = time();
1344 }
1345
1346 $data['daemon_stamp_ok'] = $stamp_check;
1347
1348 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1349
1350 $data['daemon_stamp'] = $stamp_fmt;
1351 }
1352 }
1353 }
1354
1355 return $data;
1356 }
1357
1358 function search_to_sql($search, $search_language) {
1359
1360 $keywords = str_getcsv(trim($search), " ");
1361 $query_keywords = array();
1362 $search_words = array();
1363 $search_query_leftover = array();
1364
cab58c44 1365 $pdo = Db::pdo();
f0dbfedc 1366
aeb1abed 1367 if ($search_language)
cab58c44 1368 $search_language = $pdo->quote(mb_strtolower($search_language));
aeb1abed 1369 else
9274109c 1370 $search_language = $pdo->quote("english");
aeb1abed
AD
1371
1372 foreach ($keywords as $k) {
1373 if (strpos($k, "-") === 0) {
1374 $k = substr($k, 1);
1375 $not = "NOT";
1376 } else {
1377 $not = "";
1378 }
1379
1380 $commandpair = explode(":", mb_strtolower($k), 2);
1381
1382 switch ($commandpair[0]) {
1383 case "title":
1384 if ($commandpair[1]) {
a2d77092
AD
1385 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1386 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
aeb1abed
AD
1387 } else {
1388 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1e78803c 1389 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1390 array_push($search_words, $k);
1391 }
1392 break;
1393 case "author":
1394 if ($commandpair[1]) {
a2d77092
AD
1395 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1396 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
aeb1abed
AD
1397 } else {
1398 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1e78803c 1399 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1400 array_push($search_words, $k);
1401 }
1402 break;
1403 case "note":
1404 if ($commandpair[1]) {
1405 if ($commandpair[1] == "true")
1406 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1407 else if ($commandpair[1] == "false")
1408 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1409 else
a2d77092
AD
1410 array_push($query_keywords, "($not (LOWER(note) LIKE ".
1411 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
aeb1abed 1412 } else {
1e78803c
AD
1413 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1414 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1415 if (!$not) array_push($search_words, $k);
1416 }
1417 break;
1418 case "star":
1419
1420 if ($commandpair[1]) {
1421 if ($commandpair[1] == "true")
1422 array_push($query_keywords, "($not (marked = true))");
1423 else
1424 array_push($query_keywords, "($not (marked = false))");
1425 } else {
1e78803c
AD
1426 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1427 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1428 if (!$not) array_push($search_words, $k);
1429 }
1430 break;
1431 case "pub":
1432 if ($commandpair[1]) {
1433 if ($commandpair[1] == "true")
1434 array_push($query_keywords, "($not (published = true))");
1435 else
1436 array_push($query_keywords, "($not (published = false))");
1437
1438 } else {
1439 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1e78803c 1440 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1441 if (!$not) array_push($search_words, $k);
1442 }
1443 break;
1444 case "unread":
1445 if ($commandpair[1]) {
1446 if ($commandpair[1] == "true")
1447 array_push($query_keywords, "($not (unread = true))");
1448 else
1449 array_push($query_keywords, "($not (unread = false))");
1450
1451 } else {
1e78803c
AD
1452 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1453 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1454 if (!$not) array_push($search_words, $k);
1455 }
1456 break;
1457 default:
1458 if (strpos($k, "@") === 0) {
1459
1460 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1461 $orig_ts = strtotime(substr($k, 1));
1462 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1463
1464 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1465
1466 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
1467 } else {
1468
1469 if (DB_TYPE == "pgsql") {
1470 $k = mb_strtolower($k);
1471 array_push($search_query_leftover, $not ? "!$k" : $k);
1472 } else {
1e78803c
AD
1473 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1474 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
aeb1abed
AD
1475 }
1476
1477 if (!$not) array_push($search_words, $k);
1478 }
1479 }
1480 }
1481
1482 if (count($search_query_leftover) > 0) {
cab58c44 1483 $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
aeb1abed
AD
1484
1485 if (DB_TYPE == "pgsql") {
1486 array_push($query_keywords,
1e78803c 1487 "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
aeb1abed
AD
1488 }
1489
1490 }
1491
1492 $search_query_part = implode("AND", $query_keywords);
1493
1494 return array($search_query_part, $search_words);
1495 }
1496
1497 function iframe_whitelisted($entry) {
1498 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1499
1500 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1501
1502 if ($src) {
1503 foreach ($whitelist as $w) {
1504 if ($src == $w || $src == "www.$w")
1505 return true;
1506 }
1507 }
1508
1509 return false;
1510 }
1511
2aef804f
AD
1512 // check for locally cached (media) URLs and rewrite to local versions
1513 // this is called separately after sanitize() and plugin render article hooks to allow
1514 // plugins work on original source URLs used before caching
aeb1abed 1515
2aef804f 1516 function rewrite_cached_urls($str) {
aeb1abed
AD
1517 $charset_hack = '<head>
1518 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1519 </head>';
1520
2aef804f 1521 $res = trim($str); if (!$res) return '';
aeb1abed
AD
1522
1523 $doc = new DOMDocument();
1524 $doc->loadHTML($charset_hack . $res);
1525 $xpath = new DOMXPath($doc);
1526
02bb26a9 1527 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
aeb1abed 1528
2aef804f 1529 $need_saving = false;
aeb1abed
AD
1530
1531 foreach ($entries as $entry) {
1532
02bb26a9 1533 if ($entry->hasAttribute('src') || $entry->hasAttribute('poster')) {
2aef804f
AD
1534
1535 // should be already absolutized because this is called after sanitize()
02bb26a9 1536 $src = $entry->hasAttribute('poster') ? $entry->getAttribute('poster') : $entry->getAttribute('src');
aeb1abed
AD
1537 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
1538
1539 if (file_exists($cached_filename)) {
1540
1541 // this is strictly cosmetic
1542 if ($entry->tagName == 'img') {
1543 $suffix = ".png";
1544 } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
1545 $suffix = ".mp4";
1546 } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
1547 $suffix = ".ogg";
1548 } else {
1549 $suffix = "";
1550 }
1551
1552 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1553
02bb26a9
AD
1554 if ($entry->hasAttribute('poster'))
1555 $entry->setAttribute('poster', $src);
1556 else
1557 $entry->setAttribute('src', $src);
1558
2aef804f 1559 $need_saving = true;
aeb1abed 1560 }
2aef804f
AD
1561 }
1562 }
1563
1564 if ($need_saving) {
1565 $doc->removeChild($doc->firstChild); //remove doctype
1566 $res = $doc->saveHTML();
1567 }
1568
1569 return $res;
1570 }
1571
1572 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1573 if (!$owner) $owner = $_SESSION["uid"];
1574
1575 $res = trim($str); if (!$res) return '';
1576
1577 $charset_hack = '<head>
1578 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1579 </head>';
1580
1581 $res = trim($res); if (!$res) return '';
1582
1583 libxml_use_internal_errors(true);
1584
1585 $doc = new DOMDocument();
1586 $doc->loadHTML($charset_hack . $res);
1587 $xpath = new DOMXPath($doc);
1588
1589 $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
1590
1591 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1592
1593 foreach ($entries as $entry) {
1594
1595 if ($entry->hasAttribute('href')) {
1596 $entry->setAttribute('href',
1597 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1598
1599 $entry->setAttribute('rel', 'noopener noreferrer');
1600 }
1601
1602 if ($entry->hasAttribute('src')) {
1603 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1604
1605 // cache stuff has gone to rewrite_cached_urls()
aeb1abed
AD
1606
1607 $entry->setAttribute('src', $src);
1608 }
1609
1610 if ($entry->nodeName == 'img') {
7651b6e2 1611 $entry->setAttribute('referrerpolicy', 'no-referrer');
aeb1abed 1612
8babb8e7
AD
1613 $entry->removeAttribute('width');
1614 $entry->removeAttribute('height');
1615
aeb1abed
AD
1616 if ($entry->hasAttribute('src')) {
1617 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1618
9f7bd151 1619 if (is_prefix_https() && !$is_https_url) {
aeb1abed
AD
1620
1621 if ($entry->hasAttribute('srcset')) {
1622 $entry->removeAttribute('srcset');
1623 }
1624
1625 if ($entry->hasAttribute('sizes')) {
1626 $entry->removeAttribute('sizes');
1627 }
1628 }
1629 }
d2e1e60e
AD
1630 }
1631
1632 if ($entry->hasAttribute('src') &&
1633 ($owner && get_pref("STRIP_IMAGES", $owner)) || $force_remove_images || $_SESSION["bw_limit"]) {
1634
1635 $p = $doc->createElement('p');
1636
1637 $a = $doc->createElement('a');
1638 $a->setAttribute('href', $entry->getAttribute('src'));
1639
1640 $a->appendChild(new DOMText($entry->getAttribute('src')));
1641 $a->setAttribute('target', '_blank');
1642 $a->setAttribute('rel', 'noopener noreferrer');
aeb1abed 1643
d2e1e60e 1644 $p->appendChild($a);
aeb1abed 1645
d2e1e60e 1646 if ($entry->nodeName == 'source') {
aeb1abed 1647
d2e1e60e
AD
1648 if ($entry->parentNode && $entry->parentNode->parentNode)
1649 $entry->parentNode->parentNode->replaceChild($p, $entry->parentNode);
aeb1abed 1650
d2e1e60e 1651 } else if ($entry->nodeName == 'img') {
aeb1abed 1652
d2e1e60e
AD
1653 if ($entry->parentNode)
1654 $entry->parentNode->replaceChild($p, $entry);
aeb1abed 1655
aeb1abed
AD
1656 }
1657 }
1658
1659 if (strtolower($entry->nodeName) == "a") {
1660 $entry->setAttribute("target", "_blank");
1661 $entry->setAttribute("rel", "noopener noreferrer");
1662 }
1663 }
1664
1665 $entries = $xpath->query('//iframe');
1666 foreach ($entries as $entry) {
1667 if (!iframe_whitelisted($entry)) {
1668 $entry->setAttribute('sandbox', 'allow-scripts');
1669 } else {
9f7bd151 1670 if (is_prefix_https()) {
aeb1abed
AD
1671 $entry->setAttribute("src",
1672 str_replace("http://", "https://",
1673 $entry->getAttribute("src")));
1674 }
1675 }
1676 }
1677
905ff10d 1678 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
aeb1abed
AD
1679 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1680 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1681 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1682 'dt', 'em', 'footer', 'figure', 'figcaption',
6eeeec48 1683 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
aeb1abed
AD
1684 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1685 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1686 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1687 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1688 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1689
1690 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1691
1692 $disallowed_attributes = array('id', 'style', 'class');
1693
1694 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1695 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1696 if (is_array($retval)) {
1697 $doc = $retval[0];
1698 $allowed_elements = $retval[1];
1699 $disallowed_attributes = $retval[2];
1700 } else {
1701 $doc = $retval;
1702 }
1703 }
1704
1705 $doc->removeChild($doc->firstChild); //remove doctype
1706 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1707
1708 if ($highlight_words) {
1709 foreach ($highlight_words as $word) {
1710
1711 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1712
1713 $elements = $xpath->query("//*/text()");
1714
1715 foreach ($elements as $child) {
1716
1717 $fragment = $doc->createDocumentFragment();
1718 $text = $child->textContent;
1719
1720 while (($pos = mb_stripos($text, $word)) !== false) {
1721 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1722 $word = mb_substr($text, $pos, mb_strlen($word));
1723 $highlight = $doc->createElement('span');
1724 $highlight->appendChild(new DomText($word));
1725 $highlight->setAttribute('class', 'highlight');
1726 $fragment->appendChild($highlight);
1727 $text = mb_substr($text, $pos + mb_strlen($word));
1728 }
1729
1730 if (!empty($text)) $fragment->appendChild(new DomText($text));
1731
1732 $child->parentNode->replaceChild($fragment, $child);
1733 }
1734 }
1735 }
1736
1737 $res = $doc->saveHTML();
1738
1739 /* strip everything outside of <body>...</body> */
1740
1741 $res_frag = array();
1742 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1743 return $res_frag[1];
1744 } else {
1745 return $res;
1746 }
1747 }
1748
1749 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1750 $xpath = new DOMXPath($doc);
1751 $entries = $xpath->query('//*');
1752
1753 foreach ($entries as $entry) {
1754 if (!in_array($entry->nodeName, $allowed_elements)) {
1755 $entry->parentNode->removeChild($entry);
1756 }
1757
1758 if ($entry->hasAttributes()) {
1759 $attrs_to_remove = array();
1760
1761 foreach ($entry->attributes as $attr) {
1762
1763 if (strpos($attr->nodeName, 'on') === 0) {
1764 array_push($attrs_to_remove, $attr);
1765 }
1766
1767 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1768 array_push($attrs_to_remove, $attr);
1769 }
1770
1771 if (in_array($attr->nodeName, $disallowed_attributes)) {
1772 array_push($attrs_to_remove, $attr);
1773 }
1774 }
1775
1776 foreach ($attrs_to_remove as $attr) {
1777 $entry->removeAttributeNode($attr);
1778 }
1779 }
1780 }
1781
1782 return $doc;
1783 }
1784
1785 function trim_array($array) {
1786 $tmp = $array;
1787 array_walk($tmp, 'trim');
1788 return $tmp;
1789 }
1790
1791 function tag_is_valid($tag) {
2eaf2a1f
AD
1792 if (!$tag || is_numeric($tag) || mb_strlen($tag) > 250)
1793 return false;
aeb1abed
AD
1794
1795 return true;
1796 }
1797
1798 function render_login_form() {
1799 header('Cache-Control: public');
1800
1801 require_once "login_form.php";
1802 exit;
1803 }
1804
1805 function T_sprintf() {
1806 $args = func_get_args();
1807 return vsprintf(__(array_shift($args)), $args);
1808 }
1809
1810 function print_checkpoint($n, $s) {
1811 $ts = microtime(true);
1812 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1813 return $ts;
1814 }
1815
1816 function sanitize_tag($tag) {
1817 $tag = trim($tag);
1818
1819 $tag = mb_strtolower($tag, 'utf-8');
1820
1821 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1822
1823 if (DB_TYPE == "mysql") {
1824 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1825 }
1826
1827 return $tag;
1828 }
1829
9f7bd151 1830 function is_server_https() {
e234ac8d 1831 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
9f7bd151
AD
1832 }
1833
1834 function is_prefix_https() {
1835 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1836 }
1837
b2d42e96 1838 // this returns SELF_URL_PATH sans ending slash
aeb1abed
AD
1839 function get_self_url_prefix() {
1840 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1841 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1842 } else {
1843 return SELF_URL_PATH;
1844 }
1845 }
1846
aeb1abed
AD
1847 function encrypt_password($pass, $salt = '', $mode2 = false) {
1848 if ($salt && $mode2) {
1849 return "MODE2:" . hash('sha256', $salt . $pass);
1850 } else if ($salt) {
1851 return "SHA1X:" . sha1("$salt:$pass");
1852 } else {
1853 return "SHA1:" . sha1($pass);
1854 }
1855 } // function encrypt_password
1856
1857 function load_filters($feed_id, $owner_uid) {
1858 $filters = array();
1859
c949a928 1860 $feed_id = (int) $feed_id;
0086a897 1861 $cat_id = (int)Feeds::getFeedCategory($feed_id);
aeb1abed
AD
1862
1863 if ($cat_id == 0)
1864 $null_cat_qpart = "cat_id IS NULL OR";
1865 else
1866 $null_cat_qpart = "";
1867
8adb3ec4
AD
1868 $pdo = Db::pdo();
1869
1870 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1871 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1872 $sth->execute([$owner_uid]);
aeb1abed 1873
02f3992a 1874 $check_cats = array_merge(
aeb1abed 1875 Feeds::getParentCategories($cat_id, $owner_uid),
02f3992a
AD
1876 [$cat_id]);
1877
1878 $check_cats_str = join(",", $check_cats);
1879 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
aeb1abed 1880
8adb3ec4 1881 while ($line = $sth->fetch()) {
aeb1abed
AD
1882 $filter_id = $line["id"];
1883
7f4a4045 1884 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
02f3992a 1885
8adb3ec4 1886 $sth2 = $pdo->prepare("SELECT
02f3992a 1887 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
aeb1abed
AD
1888 FROM ttrss_filters2_rules AS r,
1889 ttrss_filter_types AS t
1890 WHERE
7f4a4045 1891 (match_on IS NOT NULL OR
02f3992a 1892 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
8adb3ec4
AD
1893 (feed_id IS NULL OR feed_id = ?))) AND
1894 filter_type = t.id AND filter_id = ?");
1895 $sth2->execute([$feed_id, $filter_id]);
aeb1abed
AD
1896
1897 $rules = array();
1898 $actions = array();
1899
8adb3ec4 1900 while ($rule_line = $sth2->fetch()) {
aeb1abed
AD
1901 # print_r($rule_line);
1902
7f4a4045
AD
1903 if ($rule_line["match_on"]) {
1904 $match_on = json_decode($rule_line["match_on"], true);
aeb1abed 1905
7f4a4045 1906 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
aeb1abed 1907
7f4a4045
AD
1908 $rule = array();
1909 $rule["reg_exp"] = $rule_line["reg_exp"];
1910 $rule["type"] = $rule_line["type_name"];
1911 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
02f3992a 1912
7f4a4045
AD
1913 array_push($rules, $rule);
1914 } else if (!$match_any_rule) {
1915 // this filter contains a rule that doesn't match to this feed/category combination
1916 // thus filter has to be rejected
aeb1abed 1917
7f4a4045
AD
1918 $rules = [];
1919 break;
1920 }
aeb1abed 1921
7f4a4045 1922 } else {
0bf7e007 1923
7f4a4045
AD
1924 $rule = array();
1925 $rule["reg_exp"] = $rule_line["reg_exp"];
1926 $rule["type"] = $rule_line["type_name"];
1927 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
02f3992a 1928
7f4a4045
AD
1929 array_push($rules, $rule);
1930 }
aeb1abed
AD
1931 }
1932
02f3992a 1933 if (count($rules) > 0) {
7f4a4045
AD
1934 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1935 FROM ttrss_filters2_actions AS a,
1936 ttrss_filter_actions AS t
1937 WHERE
1938 action_id = t.id AND filter_id = ?");
1939 $sth2->execute([$filter_id]);
02f3992a 1940
7f4a4045
AD
1941 while ($action_line = $sth2->fetch()) {
1942 # print_r($action_line);
02f3992a 1943
7f4a4045
AD
1944 $action = array();
1945 $action["type"] = $action_line["type_name"];
1946 $action["param"] = $action_line["action_param"];
02f3992a 1947
7f4a4045
AD
1948 array_push($actions, $action);
1949 }
1950 }
aeb1abed
AD
1951
1952 $filter = array();
1953 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1954 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1955 $filter["rules"] = $rules;
1956 $filter["actions"] = $actions;
1957
1958 if (count($rules) > 0 && count($actions) > 0) {
1959 array_push($filters, $filter);
1960 }
1961 }
1962
1963 return $filters;
1964 }
1965
1966 function get_score_pic($score) {
1967 if ($score > 100) {
1968 return "score_high.png";
1969 } else if ($score > 0) {
1970 return "score_half_high.png";
1971 } else if ($score < -100) {
1972 return "score_low.png";
1973 } else if ($score < 0) {
1974 return "score_half_low.png";
1975 } else {
1976 return "score_neutral.png";
1977 }
1978 }
1979
aeb1abed
AD
1980 function init_plugins() {
1981 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1982
1983 return true;
1984 }
1985
1986 function add_feed_category($feed_cat, $parent_cat_id = false) {
1987
1988 if (!$feed_cat) return false;
1989
aeb1abed 1990 $feed_cat = mb_substr($feed_cat, 0, 250);
ecf6baaa 1991 if (!$parent_cat_id) $parent_cat_id = null;
aeb1abed 1992
8adb3ec4 1993 $pdo = Db::pdo();
c949a928
AD
1994 $tr_in_progress = false;
1995
1996 try {
1997 $pdo->beginTransaction();
1998 } catch (Exception $e) {
1999 $tr_in_progress = true;
2000 }
8adb3ec4
AD
2001
2002 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
f0dbfedc 2003 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
ecf6baaa 2004 AND title = :title AND owner_uid = :uid");
b78a6f08 2005 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
aeb1abed 2006
ecf6baaa 2007 if (!$sth->fetch()) {
aeb1abed 2008
b78a6f08
AD
2009 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2010 VALUES (?, ?, ?)");
2011 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
aeb1abed 2012
c949a928 2013 if (!$tr_in_progress) $pdo->commit();
aeb1abed
AD
2014
2015 return true;
2016 }
2017
7f4a4045 2018 $pdo->commit();
fdda3e4e 2019
aeb1abed
AD
2020 return false;
2021 }
2022
2023 /**
2024 * Fixes incomplete URLs by prepending "http://".
2025 * Also replaces feed:// with http://, and
2026 * prepends a trailing slash if the url is a domain name only.
2027 *
2028 * @param string $url Possibly incomplete URL
2029 *
2030 * @return string Fixed URL.
2031 */
2032 function fix_url($url) {
2033
2034 // support schema-less urls
2035 if (strpos($url, '//') === 0) {
2036 $url = 'https:' . $url;
2037 }
2038
2039 if (strpos($url, '://') === false) {
2040 $url = 'http://' . $url;
2041 } else if (substr($url, 0, 5) == 'feed:') {
2042 $url = 'http:' . substr($url, 5);
2043 }
2044
2045 //prepend slash if the URL has no slash in it
2046 // "http://www.example" -> "http://www.example/"
2047 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2048 $url .= '/';
2049 }
2050
2051 //convert IDNA hostname to punycode if possible
2052 if (function_exists("idn_to_ascii")) {
2053 $parts = parse_url($url);
2054 if (mb_detect_encoding($parts['host']) != 'ASCII')
2055 {
2056 $parts['host'] = idn_to_ascii($parts['host']);
2057 $url = build_url($parts);
2058 }
2059 }
2060
2061 if ($url != "http:///")
2062 return $url;
2063 else
2064 return '';
2065 }
2066
2067 function validate_feed_url($url) {
2068 $parts = parse_url($url);
2069
2070 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2071
2072 }
2073
2074 /* function save_email_address($email) {
2075 // FIXME: implement persistent storage of emails
2076
2077 if (!$_SESSION['stored_emails'])
2078 $_SESSION['stored_emails'] = array();
2079
2080 if (!in_array($email, $_SESSION['stored_emails']))
2081 array_push($_SESSION['stored_emails'], $email);
2082 } */
2083
2084
2085 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2086
2087 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2088
1271407e
AD
2089 $is_cat = bool_to_sql_bool($is_cat);
2090
a21f7495 2091 $pdo = Db::pdo();
aeb1abed 2092
1271407e
AD
2093 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2094 WHERE feed_id = ? AND is_cat = ?
a21f7495 2095 AND owner_uid = ?");
fc0a3050 2096 $sth->execute([$feed_id, $is_cat, $owner_uid]);
aeb1abed 2097
a21f7495
AD
2098 if ($row = $sth->fetch()) {
2099 return $row["access_key"];
aeb1abed 2100 } else {
a21f7495 2101 $key = uniqid_short();
aeb1abed 2102
a21f7495 2103 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
aeb1abed 2104 (access_key, feed_id, is_cat, owner_uid)
a21f7495
AD
2105 VALUES (?, ?, ?, ?)");
2106
fc0a3050 2107 $sth->execute([$key, $feed_id, $is_cat, $owner_uid]);
aeb1abed
AD
2108
2109 return $key;
2110 }
aeb1abed
AD
2111 }
2112
2113 function get_feeds_from_html($url, $content)
2114 {
2115 $url = fix_url($url);
2116 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2117
2118 libxml_use_internal_errors(true);
2119
2120 $doc = new DOMDocument();
2121 $doc->loadHTML($content);
2122 $xpath = new DOMXPath($doc);
2123 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2124 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2125 $feedUrls = array();
2126 foreach ($entries as $entry) {
2127 if ($entry->hasAttribute('href')) {
2128 $title = $entry->getAttribute('title');
2129 if ($title == '') {
2130 $title = $entry->getAttribute('type');
2131 }
2132 $feedUrl = rewrite_relative_url(
2133 $baseUrl, $entry->getAttribute('href')
2134 );
2135 $feedUrls[$feedUrl] = $title;
2136 }
2137 }
2138 return $feedUrls;
2139 }
2140
2141 function is_html($content) {
2142 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2143 }
2144
2145 function url_is_html($url, $login = false, $pass = false) {
2146 return is_html(fetch_file_contents($url, false, $login, $pass));
2147 }
2148
2149 function build_url($parts) {
2150 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2151 }
2152
2153 function cleanup_url_path($path) {
2154 $path = str_replace("/./", "/", $path);
2155 $path = str_replace("//", "/", $path);
2156
2157 return $path;
2158 }
2159
2160 /**
2161 * Converts a (possibly) relative URL to a absolute one.
2162 *
2163 * @param string $url Base URL (i.e. from where the document is)
2164 * @param string $rel_url Possibly relative URL in the document
2165 *
2166 * @return string Absolute URL
2167 */
2168 function rewrite_relative_url($url, $rel_url) {
2169 if (strpos($rel_url, "://") !== false) {
2170 return $rel_url;
2171 } else if (strpos($rel_url, "//") === 0) {
2172 # protocol-relative URL (rare but they exist)
2173 return $rel_url;
2174 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2175 # magnet:, feed:, etc
2176 return $rel_url;
2177 } else if (strpos($rel_url, "/") === 0) {
2178 $parts = parse_url($url);
2179 $parts['path'] = $rel_url;
2180 $parts['path'] = cleanup_url_path($parts['path']);
2181
2182 return build_url($parts);
2183
2184 } else {
2185 $parts = parse_url($url);
2186 if (!isset($parts['path'])) {
2187 $parts['path'] = '/';
2188 }
2189 $dir = $parts['path'];
2190 if (substr($dir, -1) !== '/') {
2191 $dir = dirname($parts['path']);
2192 $dir !== '/' && $dir .= '/';
2193 }
2194 $parts['path'] = $dir . $rel_url;
2195 $parts['path'] = cleanup_url_path($parts['path']);
2196
2197 return build_url($parts);
2198 }
2199 }
2200
2201 function cleanup_tags($days = 14, $limit = 1000) {
2202
7f4a4045 2203 $days = (int) $days;
a21f7495 2204
aeb1abed
AD
2205 if (DB_TYPE == "pgsql") {
2206 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2207 } else if (DB_TYPE == "mysql") {
2208 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2209 }
2210
2211 $tags_deleted = 0;
2212
7f4a4045 2213 $pdo = Db::pdo();
a21f7495 2214
7f4a4045 2215 while ($limit > 0) {
aeb1abed
AD
2216 $limit_part = 500;
2217
a21f7495 2218 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
aeb1abed
AD
2219 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2220 WHERE post_int_id = int_id AND $interval_query AND
a21f7495
AD
2221 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2222 $sth->execute([$limit]);
aeb1abed
AD
2223
2224 $ids = array();
2225
a21f7495 2226 while ($line = $sth->fetch()) {
aeb1abed
AD
2227 array_push($ids, $line['id']);
2228 }
2229
2230 if (count($ids) > 0) {
2231 $ids = join(",", $ids);
2232
a21f7495
AD
2233 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2234 $tags_deleted = $usth->rowCount();
aeb1abed
AD
2235 } else {
2236 break;
2237 }
2238
2239 $limit -= $limit_part;
2240 }
2241
2242 return $tags_deleted;
2243 }
2244
2245 function print_user_stylesheet() {
2246 $value = get_pref('USER_STYLESHEET');
2247
2248 if ($value) {
2249 print "<style type=\"text/css\">";
2250 print str_replace("<br/>", "\n", $value);
2251 print "</style>";
2252 }
2253
2254 }
2255
2256 function filter_to_sql($filter, $owner_uid) {
2257 $query = array();
2258
e4befe6b
AD
2259 $pdo = Db::pdo();
2260
aeb1abed
AD
2261 if (DB_TYPE == "pgsql")
2262 $reg_qpart = "~";
2263 else
2264 $reg_qpart = "REGEXP";
2265
2266 foreach ($filter["rules"] AS $rule) {
2267 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2268 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2269 $rule['reg_exp']) !== FALSE;
2270
2271 if ($regexp_valid) {
2272
e4befe6b 2273 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
aeb1abed
AD
2274
2275 switch ($rule["type"]) {
2276 case "title":
2277 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2278 $rule['reg_exp'] . "')";
2279 break;
2280 case "content":
2281 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2282 $rule['reg_exp'] . "')";
2283 break;
2284 case "both":
2285 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2286 $rule['reg_exp'] . "') OR LOWER(" .
2287 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2288 break;
2289 case "tag":
2290 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2291 $rule['reg_exp'] . "')";
2292 break;
2293 case "link":
2294 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2295 $rule['reg_exp'] . "')";
2296 break;
2297 case "author":
2298 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2299 $rule['reg_exp'] . "')";
2300 break;
2301 }
2302
2303 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2304
2305 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
e4befe6b 2306 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
aeb1abed
AD
2307 }
2308
2309 if (isset($rule["cat_id"])) {
2310
2311 if ($rule["cat_id"] > 0) {
2312 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2313 array_push($children, $rule["cat_id"]);
bed2d6e0 2314 $children = array_map("intval", $children);
aeb1abed
AD
2315
2316 $children = join(",", $children);
2317
2318 $cat_qpart = "cat_id IN ($children)";
2319 } else {
2320 $cat_qpart = "cat_id IS NULL";
2321 }
2322
2323 $qpart .= " AND $cat_qpart";
2324 }
2325
2326 $qpart .= " AND feed_id IS NOT NULL";
2327
2328 array_push($query, "($qpart)");
2329
2330 }
2331 }
2332
2333 if (count($query) > 0) {
2334 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2335 } else {
2336 $fullquery = "(false)";
2337 }
2338
2339 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2340
2341 return $fullquery;
2342 }
2343
2344 if (!function_exists('gzdecode')) {
2345 function gzdecode($string) { // no support for 2nd argument
2346 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2347 base64_encode($string));
2348 }
2349 }
2350
2351 function get_random_bytes($length) {
2352 if (function_exists('openssl_random_pseudo_bytes')) {
2353 return openssl_random_pseudo_bytes($length);
2354 } else {
2355 $output = "";
2356
2357 for ($i = 0; $i < $length; $i++)
2358 $output .= chr(mt_rand(0, 255));
2359
2360 return $output;
2361 }
2362 }
2363
2364 function read_stdin() {
2365 $fp = fopen("php://stdin", "r");
2366
2367 if ($fp) {
2368 $line = trim(fgets($fp));
2369 fclose($fp);
2370 return $line;
2371 }
2372
2373 return null;
2374 }
2375
aeb1abed
AD
2376 function implements_interface($class, $interface) {
2377 return in_array($interface, class_implements($class));
2378 }
2379
2380 function get_minified_js($files) {
aeb1abed
AD
2381
2382 $rv = '';
2383
2384 foreach ($files as $js) {
2385 if (!isset($_GET['debug'])) {
c4a08e4f 2386 $cached_file = CACHE_DIR . "/js/".basename($js);
aeb1abed 2387
c4a08e4f 2388 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
aeb1abed
AD
2389
2390 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2391
2392 if ($header && $contents) {
2393 list($htag, $hversion) = explode(":", $header);
2394
2395 if ($htag == "tt-rss" && $hversion == VERSION) {
2396 $rv .= $contents;
2397 continue;
2398 }
2399 }
2400 }
2401
c4a08e4f 2402 $minified = JShrink\Minifier::minify(file_get_contents("js/$js"));
aeb1abed
AD
2403 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2404 $rv .= $minified;
2405
2406 } else {
c4a08e4f 2407 $rv .= file_get_contents("js/$js"); // no cache in debug mode
aeb1abed
AD
2408 }
2409 }
2410
2411 return $rv;
2412 }
2413
2414 function calculate_dep_timestamp() {
2415 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2416
2417 $max_ts = -1;
2418
2419 foreach ($files as $file) {
2420 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2421 }
2422
2423 return $max_ts;
2424 }
2425
2426 function T_js_decl($s1, $s2) {
2427 if ($s1 && $s2) {
2428 $s1 = preg_replace("/\n/", "", $s1);
2429 $s2 = preg_replace("/\n/", "", $s2);
2430
2431 $s1 = preg_replace("/\"/", "\\\"", $s1);
2432 $s2 = preg_replace("/\"/", "\\\"", $s2);
2433
2434 return "T_messages[\"$s1\"] = \"$s2\";\n";
2435 }
2436 }
2437
2438 function init_js_translations() {
2439
2440 print 'var T_messages = new Object();
b2d42e96 2441
aeb1abed
AD
2442 function __(msg) {
2443 if (T_messages[msg]) {
2444 return T_messages[msg];
2445 } else {
2446 return msg;
2447 }
2448 }
b2d42e96 2449
aeb1abed
AD
2450 function ngettext(msg1, msg2, n) {
2451 return __((parseInt(n) > 1) ? msg2 : msg1);
2452 }';
2453
2454 $l10n = _get_reader();
2455
2456 for ($i = 0; $i < $l10n->total; $i++) {
2457 $orig = $l10n->get_original_string($i);
2458 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2459 $key = explode(chr(0), $orig);
2460 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2461 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2462 } else {
2463 $translation = __($orig);
2464 print T_js_decl($orig, $translation);
2465 }
2466 }
2467 }
2468
aeb1abed 2469 function get_theme_path($theme) {
bfebf57c
AD
2470 if ($theme == "default.php")
2471 return "css/default.css";
2472
aeb1abed
AD
2473 $check = "themes/$theme";
2474 if (file_exists($check)) return $check;
2475
2476 $check = "themes.local/$theme";
2477 if (file_exists($check)) return $check;
2478 }
2479
2480 function theme_valid($theme) {
2481 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2482
2483 if (in_array($theme, $bundled_themes)) return true;
2484
2485 $file = "themes/" . basename($theme);
2486
2487 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2488
2489 if (file_exists($file) && is_readable($file)) {
2490 $fh = fopen($file, "r");
2491
2492 if ($fh) {
2493 $header = fgets($fh);
2494 fclose($fh);
2495
2496 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2497 }
2498 }
2499
2500 return false;
2501 }
2502
2503 /**
2504 * @SuppressWarnings(unused)
2505 */
2506 function error_json($code) {
2507 require_once "errors.php";
2508
2509 @$message = $ERRORS[$code];
2510
2511 return json_encode(array("error" =>
2512 array("code" => $code, "message" => $message)));
2513
2514 }
2515
904aff76 2516 /*function abs_to_rel_path($dir) {
aeb1abed
AD
2517 $tmp = str_replace(dirname(__DIR__), "", $dir);
2518
2519 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2520
2521 return $tmp;
904aff76 2522 }*/
aeb1abed
AD
2523
2524 function get_upload_error_message($code) {
2525
2526 $errors = array(
2527 0 => __('There is no error, the file uploaded with success'),
2528 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2529 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2530 3 => __('The uploaded file was only partially uploaded'),
2531 4 => __('No file was uploaded'),
2532 6 => __('Missing a temporary folder'),
2533 7 => __('Failed to write file to disk.'),
2534 8 => __('A PHP extension stopped the file upload.'),
2535 );
2536
2537 return $errors[$code];
2538 }
2539
2540 function base64_img($filename) {
2541 if (file_exists($filename)) {
2542 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2543
2544 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2545 } else {
2546 return "";
2547 }
2548 }
2549
8b73bd28
AD
2550 /* this is essentially a wrapper for readfile() which allows plugins to hook
2551 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2552
2553 hook function should return true if request was handled (or at least attempted to)
2554
2555 note that this can be called without user context so the plugin to handle this
2556 should be loaded systemwide in config.php */
2557 function send_local_file($filename) {
2558 if (file_exists($filename)) {
f3068c63
AD
2559
2560 if (is_writable($filename)) touch($filename);
2561
8b73bd28
AD
2562 $tmppluginhost = new PluginHost();
2563
2564 $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
2565 $tmppluginhost->load_data();
2566
2567 foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
2568 if ($plugin->hook_send_local_file($filename)) return true;
2569 }
2570
2571 $mimetype = mime_content_type($filename);
88adf3da
AD
2572
2573 // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2574 // video files are detected as octet-stream by mime_content_type()
2575
2576 if ($mimetype == "application/octet-stream")
2577 $mimetype = "video/mp4";
2578
8b73bd28
AD
2579 header("Content-type: $mimetype");
2580
2581 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2582 header("Last-Modified: $stamp", true);
2583
a1b86519 2584 return readfile($filename);
8b73bd28
AD
2585 } else {
2586 return false;
2587 }
2588 }
2589
0b68b162 2590 function check_mysql_tables() {
a21f7495 2591 $pdo = Db::pdo();
0b68b162 2592
a21f7495
AD
2593 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2594 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2595 $sth->execute([DB_NAME]);
0b68b162
AD
2596
2597 $bad_tables = [];
2598
a21f7495 2599 while ($line = $sth->fetch()) {
0b68b162
AD
2600 array_push($bad_tables, $line);
2601 }
2602
2603 return $bad_tables;
2604 }
2605
2cf93c04
AD
2606 function validate_field($string, $allowed, $default = "") {
2607 if (in_array($string, $allowed))
2608 return $string;
2609 else
2610 return $default;
2611 }
2612
7f4a4045
AD
2613 function arr_qmarks($arr) {
2614 return str_repeat('?,', count($arr) - 1) . '?';
2615 }