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