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