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