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