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