]> git.wh0rd.org - tt-rss.git/blame - include/functions.php
more pdo stuff
[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
AD
231 $sth = $pdo->prepare("SELECT owner_uid FROM ttrss_feeds WHERE id = ?");
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
fefa6ca3 262 if (DB_TYPE == "pgsql") {
8adb3ec4 263 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
6b3160cf
AD
264 USING ttrss_entries
265 WHERE ttrss_entries.id = ref_id AND
266 marked = false AND
8adb3ec4 267 feed_id = ? AND
6b3160cf 268 $query_limit
8adb3ec4
AD
269 ttrss_entries.date_updated < NOW() - INTERVAL ?");
270 $sth->execute([$feed_id, "$purge_interval days"]);
ad507f85 271
fefa6ca3 272 } else {
8adb3ec4 273 $sth = $pdo->prepare("DELETE FROM ttrss_user_entries
8d505d78
AD
274 USING ttrss_user_entries, ttrss_entries
275 WHERE ttrss_entries.id = ref_id AND
276 marked = false AND
8adb3ec4 277 feed_id = ? AND
07d0efe9 278 $query_limit
8adb3ec4
AD
279 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL ? DAY)");
280 $sth->execute([$feed_id, $purge_interval]);
281
ad507f85
AD
282 }
283
8adb3ec4 284 $rows = $sth->rowCount();
3f6f0857 285
2ed0d6c4 286 CCache::update($feed_id, $owner_uid);
ced46404 287
ad507f85 288 if ($debug) {
6f9e33e4 289 _debug("Purged feed $feed_id ($purge_interval): deleted $rows articles");
fefa6ca3 290 }
2ea09bde
AD
291
292 return $rows;
9632f884 293 } // function purge_feed
fefa6ca3 294
a42c55f0 295 function feed_purge_interval($feed_id) {
07d0efe9 296
8adb3ec4
AD
297 $pdo = DB::pdo();
298
299 $sth = $pdo->prepare("SELECT purge_interval, owner_uid FROM ttrss_feeds
300 WHERE id = ?");
301 $sth->execute([$feed_id]);
07d0efe9 302
8adb3ec4
AD
303 if ($row = $sth->fetch()) {
304 $purge_interval = $row["purge_interval"];
305 $owner_uid = $row["owner_uid"];
07d0efe9 306
6322ac79 307 if ($purge_interval == 0) $purge_interval = get_pref(
863be6ca 308 'PURGE_OLD_DAYS', $owner_uid);
07d0efe9
AD
309
310 return $purge_interval;
311
312 } else {
313 return -1;
314 }
315 }
316
465fb16d
AD
317 // TODO: multiple-argument way is deprecated, first parameter is a hash now
318 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
319 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
8d505d78 320
23d2471c 321 global $fetch_last_error;
7a01dc77 322 global $fetch_last_error_code;
cf0231f9 323 global $fetch_last_error_content;
ef39be2b 324 global $fetch_last_content_type;
153cb6d3 325 global $fetch_last_modified;
3f6f0857 326 global $fetch_curl_used;
33373eca 327
24c7e413
AD
328 $fetch_last_error = false;
329 $fetch_last_error_code = -1;
330 $fetch_last_error_content = "";
331 $fetch_last_content_type = "";
332 $fetch_curl_used = false;
153cb6d3 333 $fetch_last_modified = "";
24c7e413 334
c71add38 335 if (!is_array($options)) {
465fb16d
AD
336
337 // falling back on compatibility shim
153cb6d3 338 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent" ];
e934d63e
AD
339 $tmp = [];
340
341 for ($i = 0; $i < func_num_args(); $i++) {
342 $tmp[$option_names[$i]] = func_get_arg($i);
343 }
344
345 $options = $tmp;
346
347 /*$options = array(
465fb16d
AD
348 "url" => func_get_arg(0),
349 "type" => @func_get_arg(1),
350 "login" => @func_get_arg(2),
351 "pass" => @func_get_arg(3),
352 "post_query" => @func_get_arg(4),
353 "timeout" => @func_get_arg(5),
354 "timestamp" => @func_get_arg(6),
355 "useragent" => @func_get_arg(7)
e934d63e 356 ); */
465fb16d
AD
357 }
358
e3bc4591
AD
359 $url = $options["url"];
360 $type = isset($options["type"]) ? $options["type"] : false;
361 $login = isset($options["login"]) ? $options["login"] : false;
465fb16d 362 $pass = isset($options["pass"]) ? $options["pass"] : false;
e3bc4591
AD
363 $post_query = isset($options["post_query"]) ? $options["post_query"] : false;
364 $timeout = isset($options["timeout"]) ? $options["timeout"] : false;
153cb6d3 365 $last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
e3bc4591 366 $useragent = isset($options["useragent"]) ? $options["useragent"] : false;
fabfb9fc 367 $followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
465fb16d 368
32703cc6 369 $url = ltrim($url, ' ');
2375b6b6 370 $url = str_replace(' ', '%20', $url);
23d2471c 371
9fd58133
AD
372 if (strpos($url, "//") === 0)
373 $url = 'http:' . $url;
374
312742db 375 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
3f6f0857
AD
376
377 $fetch_curl_used = true;
b799dc8b 378
4c467026 379 $ch = curl_init($url);
a1af1574 380
153cb6d3 381 if ($last_modified && !$post_query) {
7a01dc77 382 curl_setopt($ch, CURLOPT_HTTPHEADER,
153cb6d3 383 array("If-Modified-Since: $last_modified"));
7a01dc77
AD
384 }
385
8401101d
BK
386 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
387 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : FILE_FETCH_TIMEOUT);
fabfb9fc 388 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
a1af1574
AD
389 curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
390 curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
391 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
153cb6d3 392 curl_setopt($ch, CURLOPT_HEADER, true);
5f6804bc 393 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
f826070c
AD
394 curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent :
395 SELF_USER_AGENT);
3f6f0857 396 curl_setopt($ch, CURLOPT_ENCODING, "");
1fd733c8 397 //curl_setopt($ch, CURLOPT_REFERER, $url);
0f6b9263 398
4c467026 399 if (!ini_get("open_basedir")) {
0f6b9263
AD
400 curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
401 }
8d505d78 402
05f14a7d
AD
403 if (defined('_CURL_HTTP_PROXY')) {
404 curl_setopt($ch, CURLOPT_PROXY, _CURL_HTTP_PROXY);
405 }
406
ae5f7bb1
AD
407 if ($post_query) {
408 curl_setopt($ch, CURLOPT_POST, true);
409 curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
410 }
411
8d505d78
AD
412 if ($login && $pass)
413 curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
a1af1574 414
153cb6d3
AD
415 $ret = @curl_exec($ch);
416
417 $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
418 $headers = explode("\r\n", substr($ret, 0, $headers_length));
419 $contents = substr($ret, $headers_length);
420
421 foreach ($headers as $header) {
9d930af9
AD
422 if (strstr($header, ": ") !== FALSE) {
423 list ($key, $value) = explode(": ", $header);
424
425 if (strtolower($key) == "last-modified") {
426 $fetch_last_modified = $value;
427 }
428 }
429
430 if (substr(strtolower($header), 0, 7) == 'http/1.') {
431 $fetch_last_error_code = (int) substr($header, 9, 3);
432 $fetch_last_error = $header;
433 }
153cb6d3 434 }
268a06dc 435
48b657fc
AD
436 if (curl_errno($ch) === 23 || curl_errno($ch) === 61) {
437 curl_setopt($ch, CURLOPT_ENCODING, 'none');
438 $contents = @curl_exec($ch);
fb850eec
AD
439 }
440
8d505d78 441 $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
ef39be2b 442 $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
4065b60b 443
7a01dc77
AD
444 $fetch_last_error_code = $http_code;
445
ef39be2b 446 if ($http_code != 200 || $type && strpos($fetch_last_content_type, "$type") === false) {
9d930af9 447
fb850eec 448 if (curl_errno($ch) != 0) {
9d930af9 449 $fetch_last_error .= "; " . curl_errno($ch) . " " . curl_error($ch);
fb850eec 450 }
9d930af9 451
cf0231f9 452 $fetch_last_error_content = $contents;
fb850eec 453 curl_close($ch);
a1af1574
AD
454 return false;
455 }
4065b60b 456
153cb6d3
AD
457 if (!$contents) {
458 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
459 curl_close($ch);
460 return false;
461 }
462
fb850eec
AD
463 curl_close($ch);
464
a1af1574 465 return $contents;
4065b60b 466 } else {
3f6f0857
AD
467
468 $fetch_curl_used = false;
469
d3911f80 470 if ($login && $pass){
8d505d78
AD
471 $url_parts = array();
472
473 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
474
d3911f80
AD
475 $pass = urlencode($pass);
476
8d505d78
AD
477 if ($url_parts[1] && $url_parts[2]) {
478 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
479 }
480 }
481
3bba9c39
AD
482 // TODO: should this support POST requests or not? idk
483
153cb6d3 484 if (!$post_query && $last_modified) {
45913edd
AD
485 $context = stream_context_create(array(
486 'http' => array(
487 'method' => 'GET',
24c7e413 488 'ignore_errors' => true,
3bba9c39 489 'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
45913edd 490 'protocol_version'=> 1.1,
153cb6d3
AD
491 'header' => "If-Modified-Since: $last_modified\r\n")
492 ));
01311d86 493 } else {
45913edd
AD
494 $context = stream_context_create(array(
495 'http' => array(
496 'method' => 'GET',
24c7e413 497 'ignore_errors' => true,
3bba9c39 498 'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
45913edd
AD
499 'protocol_version'=> 1.1
500 )));
7475580b 501 }
01311d86 502
6122c449
AD
503 $old_error = error_get_last();
504
01311d86 505 $data = @file_get_contents($url, false, $context);
23d2471c 506
37ddf5b7 507 if (isset($http_response_header) && is_array($http_response_header)) {
9d930af9
AD
508 foreach ($http_response_header as $header) {
509 if (strstr($header, ": ") !== FALSE) {
510 list ($key, $value) = explode(": ", $header);
511
512 $key = strtolower($key);
513
514 if ($key == 'content-type') {
515 $fetch_last_content_type = $value;
516 // don't abort here b/c there might be more than one
517 // e.g. if we were being redirected -- last one is the right one
518 } else if ($key == 'last-modified') {
519 $fetch_last_modified = $value;
520 }
521 }
522
523 if (substr(strtolower($header), 0, 7) == 'http/1.') {
524 $fetch_last_error_code = (int) substr($header, 9, 3);
525 $fetch_last_error = $header;
01311d86 526 }
ef39be2b
MB
527 }
528 }
529
24c7e413 530 if ($fetch_last_error_code != 200) {
23d2471c 531 $error = error_get_last();
6122c449
AD
532
533 if ($error['message'] != $old_error['message']) {
9d930af9 534 $fetch_last_error .= "; " . $error["message"];
6122c449 535 }
24c7e413
AD
536
537 $fetch_last_error_content = $data;
538
539 return false;
23d2471c
AD
540 }
541 return $data;
4065b60b
AD
542 }
543
544 }
78800912 545
9632f884
AD
546 /**
547 * Try to determine the favicon URL for a feed.
548 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
549 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
8d505d78 550 *
9632f884
AD
551 * @param string $url A feed or page URL
552 * @access public
553 * @return mixed The favicon URL, or false if none was found.
554 */
1bd11fdf 555 function get_favicon_url($url) {
99331724 556
1bd11fdf 557 $favicon_url = false;
ed214298 558
4065b60b 559 if ($html = @fetch_file_contents($url)) {
78800912 560
ed214298 561 libxml_use_internal_errors(true);
c798704b 562
ed214298
AD
563 $doc = new DOMDocument();
564 $doc->loadHTML($html);
565 $xpath = new DOMXPath($doc);
717f5e64 566
701c5a7e 567 $base = $xpath->query('/html/head/base[@href]');
a712429e 568 foreach ($base as $b) {
241f69e4 569 $url = rewrite_relative_url($url, $b->getAttribute("href"));
a712429e
AD
570 break;
571 }
572
1bd11fdf 573 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
ed214298
AD
574 if (count($entries) > 0) {
575 foreach ($entries as $entry) {
1bd11fdf
AD
576 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
577 break;
ed214298 578 }
8d505d78 579 }
4065b60b 580 }
c798704b 581
1bd11fdf
AD
582 if (!$favicon_url)
583 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
584
585 return $favicon_url;
586 } // function get_favicon_url
587
a42c55f0 588 function initialize_user_prefs($uid, $profile = false) {
ff485f1d 589
a42c55f0 590 $uid = db_escape_string($uid);
ff485f1d 591
6322ac79 592 if (get_schema_version() < 63) $profile_qpart = "";
f9aa6a89 593
8adb3ec4
AD
594 ////db_query("BEGIN");
595
596 $pdo = DB::pdo();
ff485f1d 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
b78a6f08
AD
604 profile = :profile OR (:profile IS NULL AND profile IS NULL)");
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
a42c55f0
AD
617 $line["def_value"] = db_escape_string($line["def_value"]);
618 $line["pref_name"] = db_escape_string($line["pref_name"]);
d296ba50 619
6322ac79 620 if (get_schema_version() < 63) {
8adb3ec4 621 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
8d505d78 622 (owner_uid,pref_name,value) VALUES
8adb3ec4
AD
623 (?, ?, ?)");
624 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
f9aa6a89
AD
625
626 } else {
8adb3ec4 627 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
8d505d78 628 (owner_uid,pref_name,value, profile) VALUES
8adb3ec4
AD
629 (?, ?, ?, ?)");
630 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
f9aa6a89 631 }
ff485f1d
AD
632
633 }
634 }
635
8adb3ec4 636 ////db_query("COMMIT");
ff485f1d
AD
637
638 }
956c7629 639
8de8bfb8
AD
640 function get_ssl_certificate_id() {
641 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
642 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
643 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
644 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
645 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
646 }
ac617ebc
GG
647 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
648 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
649 $_SERVER["SSL_CLIENT_V_START"] .
650 $_SERVER["SSL_CLIENT_V_END"] .
651 $_SERVER["SSL_CLIENT_S_DN"]);
652 }
8de8bfb8
AD
653 return "";
654 }
655
a42c55f0 656 function authenticate_user($login, $password, $check_only = false) {
c8437f35 657
131b01b3 658 if (!SINGLE_USER_MODE) {
0d421af8 659 $user_id = false;
0f28f81f 660
1ffe3391 661 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
0f28f81f
AD
662
663 $user_id = (int) $plugin->authenticate($login, $password);
664
665 if ($user_id) {
666 $_SESSION["auth_module"] = strtolower(get_class($plugin));
667 break;
668 }
461766f3
AD
669 }
670
0d421af8 671 if ($user_id && !$check_only) {
70400171
AD
672 @session_start();
673
0d421af8 674 $_SESSION["uid"] = $user_id;
3472c4c5 675 $_SESSION["version"] = VERSION_STATIC;
0d421af8 676
8adb3ec4
AD
677 $pdo = DB::pdo();
678 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
679 WHERE id = ?");
680 $sth->execute([$user_id]);
681 $row = $sth->fetch();
8d505d78 682
8adb3ec4
AD
683 $_SESSION["name"] = $row["login"];
684 $_SESSION["access_level"] = $row["access_level"];
3ceb893f 685 $_SESSION["csrf_token"] = uniqid_short();
8d505d78 686
8adb3ec4
AD
687 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
688 $usth->execute([$user_id]);
8d505d78 689
131b01b3 690 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
837ec70e 691 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
8adb3ec4 692 $_SESSION["pwd_hash"] = $row["pwd_hash"];
91c5f229
AD
693
694 $_SESSION["last_version_check"] = time();
8d505d78 695
a42c55f0 696 initialize_user_prefs($_SESSION["uid"]);
8d505d78 697
131b01b3
AD
698 return true;
699 }
8d505d78 700
131b01b3 701 return false;
503eb349 702
131b01b3 703 } else {
503eb349 704
131b01b3
AD
705 $_SESSION["uid"] = 1;
706 $_SESSION["name"] = "admin";
787e5ebc 707 $_SESSION["access_level"] = 10;
21e42e5f 708
0d421af8
AD
709 $_SESSION["hide_hello"] = true;
710 $_SESSION["hide_logout"] = true;
711
d5fd183d
AD
712 $_SESSION["auth_module"] = false;
713
21e42e5f 714 if (!$_SESSION["csrf_token"]) {
3ceb893f 715 $_SESSION["csrf_token"] = uniqid_short();
21e42e5f 716 }
f557cd78 717
0bbba72d 718 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
8d505d78 719
a42c55f0 720 initialize_user_prefs($_SESSION["uid"]);
8d505d78 721
c8437f35
AD
722 return true;
723 }
c8437f35
AD
724 }
725
e6cb77a0
AD
726 function make_password($length = 8) {
727
85db6213
AD
728 $password = "";
729 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
730
731 $i = 0;
732
733 while ($i < $length) {
734 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
735
736 if (!strstr($password, $char)) {
737 $password .= $char;
738 $i++;
739 }
740 }
741 return $password;
e6cb77a0
AD
742 }
743
744 // this is called after user is created to initialize default feeds, labels
745 // or whatever else
8d505d78 746
e6cb77a0
AD
747 // user preferences are checked on every login, not here
748
a42c55f0 749 function initialize_user($uid) {
e6cb77a0 750
8adb3ec4
AD
751 $pdo = DB::pdo();
752
753 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
754 values (?, 'Tiny Tiny RSS: Forum',
f0855b88 755 'http://tt-rss.org/forum/rss.php')");
8adb3ec4 756 $sth->execute([$uid]);
3b0feb9b 757 }
e6cb77a0 758
b8aa49bc 759 function logout_user() {
5ccc1cf5
AD
760 session_destroy();
761 if (isset($_COOKIE[session_name()])) {
762 setcookie(session_name(), '', time()-42000, '/');
763 }
b8aa49bc
AD
764 }
765
8484ce22
AD
766 function validate_csrf($csrf_token) {
767 return $csrf_token == $_SESSION['csrf_token'];
768 }
769
5cbd1fe8
AD
770 function load_user_plugins($owner_uid, $pluginhost = false) {
771
772 if (!$pluginhost) $pluginhost = PluginHost::getInstance();
773
6d45a152 774 if ($owner_uid && SCHEMA_VERSION >= 100) {
a42c55f0 775 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
de612e7a 776
5cbd1fe8 777 $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
e9c04fd4 778
6322ac79 779 if (get_schema_version() > 100) {
5cbd1fe8 780 $pluginhost->load_data();
e9c04fd4 781 }
de612e7a
AD
782 }
783 }
784
6322ac79 785 function login_sequence() {
8adb3ec4
AD
786 $pdo = Db::pdo();
787
97acbaf1 788 if (SINGLE_USER_MODE) {
25db6c51 789 @session_start();
a42c55f0 790 authenticate_user("admin", null);
68349f55 791 startup_gettext();
a42c55f0 792 load_user_plugins($_SESSION["uid"]);
97acbaf1 793 } else {
6322ac79 794 if (!validate_session()) $_SESSION["uid"] = false;
d0eef2a3
AD
795
796 if (!$_SESSION["uid"]) {
97acbaf1 797
a42c55f0
AD
798 if (AUTH_AUTO_LOGIN && authenticate_user(null, null)) {
799 $_SESSION["ref_schema_version"] = get_schema_version(true);
97acbaf1 800 } else {
a42c55f0 801 authenticate_user(null, null, true);
97acbaf1
AD
802 }
803
d0eef2a3 804 if (!$_SESSION["uid"]) {
d0eef2a3
AD
805 @session_destroy();
806 setcookie(session_name(), '', time()-42000, '/');
9ce7a554 807
6322ac79 808 render_login_form();
d0eef2a3
AD
809 exit;
810 }
4ad99f23 811
97acbaf1
AD
812 } else {
813 /* bump login timestamp */
8adb3ec4
AD
814 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
815 $sth->execute([$_SESSION['uid']]);
816
06b0777f 817 $_SESSION["last_login_update"] = time();
01a87dff
AD
818 }
819
de612e7a 820 if ($_SESSION["uid"]) {
7b149552 821 startup_gettext();
a42c55f0 822 load_user_plugins($_SESSION["uid"]);
b1b1d25f
AD
823
824 /* cleanup ccache */
825
8adb3ec4
AD
826 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
827 AND
b1b1d25f
AD
828 (SELECT COUNT(id) FROM ttrss_feeds WHERE
829 ttrss_feeds.id = feed_id) = 0");
830
a21f7495 831 $sth->execute([$_SESSION['uid']]);
8adb3ec4
AD
832
833 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
834 AND
b1b1d25f
AD
835 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
836 ttrss_feed_categories.id = feed_id) = 0");
837
a21f7495 838 $sth->execute([$_SESSION['uid']]);
de612e7a 839 }
b1b1d25f 840
b8aa49bc 841 }
afc3cf55 842 }
3547842a 843
411fe209 844 function truncate_string($str, $max_len, $suffix = '&hellip;') {
878a0083 845 if (mb_strlen($str, "utf-8") > $max_len) {
411fe209 846 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
3547842a
AD
847 } else {
848 return $str;
849 }
850 }
54a60e1a 851
cc43e19b
AD
852 // is not utf8 clean
853 function truncate_middle($str, $max_len, $suffix = '&hellip;') {
854 if (strlen($str) > $max_len) {
855 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
856 } else {
857 return $str;
858 }
859 }
860
ab4b768f
AD
861 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
862
863 try {
864 $source_tz = new DateTimeZone($source_tz);
865 } catch (Exception $e) {
866 $source_tz = new DateTimeZone('UTC');
867 }
868
869 try {
870 $dest_tz = new DateTimeZone($dest_tz);
871 } catch (Exception $e) {
872 $dest_tz = new DateTimeZone('UTC');
873 }
874
875 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
876 return $dt->format('U') + $dest_tz->getOffset($dt);
877 }
878
a42c55f0 879 function make_local_datetime($timestamp, $long, $owner_uid = false,
b6714c77 880 $no_smart_dt = false, $eta_min = false) {
324944f3
AD
881
882 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
883 if (!$timestamp) $timestamp = '1970-01-01 0:00';
884
7d96bfcd 885 global $utc_tz;
5ddef5ba
AD
886 global $user_tz;
887
888 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
324944f3 889
90e5f4f1
AD
890 $timestamp = substr($timestamp, 0, 19);
891
7d96bfcd
AD
892 # We store date in UTC internally
893 $dt = new DateTime($timestamp, $utc_tz);
894
5ddef5ba 895 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
7d96bfcd 896
6bfc97da 897 if ($user_tz_string != 'Automatic') {
324944f3 898
6bfc97da
AD
899 try {
900 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
901 } catch (Exception $e) {
902 $user_tz = $utc_tz;
903 }
904
905 $tz_offset = $user_tz->getOffset($dt);
906 } else {
96f0cbe3 907 $tz_offset = (int) -$_SESSION["clientTzOffset"];
6bfc97da 908 }
5ddef5ba 909
7d96bfcd 910 $user_timestamp = $dt->format('U') + $tz_offset;
324944f3 911
1dc52ae7 912 if (!$no_smart_dt) {
a42c55f0 913 return smart_date_time($user_timestamp,
b6714c77 914 $tz_offset, $owner_uid, $eta_min);
324944f3
AD
915 } else {
916 if ($long)
a42c55f0 917 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
324944f3 918 else
a42c55f0 919 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
324944f3
AD
920
921 return date($format, $user_timestamp);
922 }
923 }
924
b6714c77 925 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
2a5c136e
AD
926 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
927
97aa917c 928 if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
46973af5
AD
929 return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
930 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
be773442 931 return date("G:i", $timestamp);
2a5c136e 932 } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
a42c55f0 933 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
2a5c136e 934 return date($format, $timestamp);
be773442 935 } else {
a42c55f0 936 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
2a5c136e 937 return date($format, $timestamp);
be773442
AD
938 }
939 }
940
e3c99f3b 941 function sql_bool_to_bool($s) {
9955a134 942 if ($s == "t" || $s == "1" || strtolower($s) == "true") {
e3c99f3b
AD
943 return true;
944 } else {
945 return false;
946 }
947 }
8d505d78 948
badac687
AD
949 function bool_to_sql_bool($s) {
950 if ($s) {
951 return "true";
952 } else {
953 return "false";
954 }
955 }
e3c99f3b 956
fcfa9ef1
AD
957 // Session caching removed due to causing wrong redirects to upgrade
958 // script when get_schema_version() is called on an obsolete session
959 // created on a previous schema version.
a42c55f0 960 function get_schema_version($nocache = false) {
7d96bfcd
AD
961 global $schema_version;
962
8adb3ec4
AD
963 $pdo = DB::pdo();
964
e2cf81e2 965 if (!$schema_version && !$nocache) {
8adb3ec4
AD
966 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
967 $version = $row["schema_version"];
7d96bfcd 968 $schema_version = $version;
199db684 969 return $version;
7d96bfcd
AD
970 } else {
971 return $schema_version;
972 }
e4c51a6c
AD
973 }
974
6322ac79 975 function sanity_check() {
31303c6b 976 require_once 'errors.php';
cacc1877 977 global $ERRORS;
ebb948c2 978
6043fb7e 979 $error_code = 0;
a42c55f0 980 $schema_version = get_schema_version(true);
6043fb7e
AD
981
982 if ($schema_version != SCHEMA_VERSION) {
983 $error_code = 5;
984 }
985
a42c55f0 986 if (db_escape_string("testTEST") != "testTEST") {
f29ba148
AD
987 $error_code = 12;
988 }
989
ebb948c2 990 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
6043fb7e
AD
991 }
992
27981ca3 993 function file_is_locked($filename) {
8ff2a86c
AD
994 if (file_exists(LOCK_DIRECTORY . "/$filename")) {
995 if (function_exists('flock')) {
996 $fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
997 if ($fp) {
998 if (flock($fp, LOCK_EX | LOCK_NB)) {
999 flock($fp, LOCK_UN);
1000 fclose($fp);
1001 return false;
1002 }
31a6d42d 1003 fclose($fp);
8ff2a86c
AD
1004 return true;
1005 } else {
31a6d42d
AD
1006 return false;
1007 }
27981ca3 1008 }
8ff2a86c
AD
1009 return true; // consider the file always locked and skip the test
1010 } else {
1011 return false;
27981ca3 1012 }
27981ca3
AD
1013 }
1014
8ff2a86c 1015
fcb4c0c9 1016 function make_lockfile($filename) {
cfa43e02 1017 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
fcb4c0c9 1018
a44bfcfd 1019 if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
58fc7095
AD
1020 $stat_h = fstat($fp);
1021 $stat_f = stat(LOCK_DIRECTORY . "/$filename");
1022
1fcebfb3
AD
1023 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
1024 if ($stat_h["ino"] != $stat_f["ino"] ||
1025 $stat_h["dev"] != $stat_f["dev"]) {
1026
1027 return false;
1028 }
58fc7095
AD
1029 }
1030
4c59adb1
AD
1031 if (function_exists('posix_getpid')) {
1032 fwrite($fp, posix_getpid() . "\n");
1033 }
fcb4c0c9
AD
1034 return $fp;
1035 } else {
1036 return false;
1037 }
1038 }
1039
bf7fcde8 1040 function make_stampfile($filename) {
cfa43e02 1041 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
bf7fcde8 1042
8e00ae9b 1043 if (flock($fp, LOCK_EX | LOCK_NB)) {
bf7fcde8 1044 fwrite($fp, time() . "\n");
8e00ae9b 1045 flock($fp, LOCK_UN);
bf7fcde8
AD
1046 fclose($fp);
1047 return true;
1048 } else {
1049 return false;
1050 }
1051 }
1052
894ebcf5 1053 function sql_random_function() {
8c0496f7 1054 if (DB_TYPE == "mysql") {
894ebcf5
AD
1055 return "RAND()";
1056 } else {
1057 return "RANDOM()";
1058 }
1059 }
1060
ff5cc7d7 1061 function getFeedUnread($feed, $is_cat = false) {
86a8351c 1062 return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
ff5cc7d7
AD
1063 }
1064
ff5cc7d7
AD
1065 function checkbox_to_sql_bool($val) {
1066 return ($val == "on") ? "true" : "false";
1067 }
1068
3ceb893f
AD
1069 function uniqid_short() {
1070 return uniqid(base_convert(rand(), 10, 36));
1071 }
1072
aeb1abed
AD
1073 function make_init_params() {
1074 $params = array();
1075
1076 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1077 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1078 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1079 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1080
1081 $params[strtolower($param)] = (int) get_pref($param);
1082 }
1083
1084 $params["icons_url"] = ICONS_URL;
1085 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
1086 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1087 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1088 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1089 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1090 $params["label_base_index"] = (int) LABEL_BASE_INDEX;
1091
1092 $theme = get_pref( "USER_CSS_THEME", false, false);
1093 $params["theme"] = theme_valid("$theme") ? $theme : "";
1094
1095 $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
1096
1097 $params["php_platform"] = PHP_OS;
1098 $params["php_version"] = PHP_VERSION;
1099
1100 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1101
8adb3ec4
AD
1102 $pdo = Db::pdo();
1103
1104 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1105 ttrss_feeds WHERE owner_uid = ?");
1106 $sth->execute([$_SESSION['uid']]);
1107 $row = $sth->fetch();
aeb1abed 1108
8adb3ec4
AD
1109 $max_feed_id = $row["mid"];
1110 $num_feeds = $row["nf"];
aeb1abed
AD
1111
1112 $params["max_feed_id"] = (int) $max_feed_id;
1113 $params["num_feeds"] = (int) $num_feeds;
1114
1115 $params["hotkeys"] = get_hotkeys_map();
1116
1117 $params["csrf_token"] = $_SESSION["csrf_token"];
1118 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1119
1120 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
1121
1122 $params["icon_alert"] = base64_img("images/alert.png");
1123 $params["icon_information"] = base64_img("images/information.png");
1124 $params["icon_cross"] = base64_img("images/cross.png");
1125 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1126
5e78b0c2
AD
1127 $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1128
aeb1abed
AD
1129 return $params;
1130 }
1131
1132 function get_hotkeys_info() {
1133 $hotkeys = array(
1134 __("Navigation") => array(
1135 "next_feed" => __("Open next feed"),
1136 "prev_feed" => __("Open previous feed"),
1137 "next_article" => __("Open next article"),
1138 "prev_article" => __("Open previous article"),
1139 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1140 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1141 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1142 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1143 "search_dialog" => __("Show search dialog")),
1144 __("Article") => array(
1145 "toggle_mark" => __("Toggle starred"),
1146 "toggle_publ" => __("Toggle published"),
1147 "toggle_unread" => __("Toggle unread"),
1148 "edit_tags" => __("Edit tags"),
1149 "open_in_new_window" => __("Open in new window"),
1150 "catchup_below" => __("Mark below as read"),
1151 "catchup_above" => __("Mark above as read"),
1152 "article_scroll_down" => __("Scroll down"),
1153 "article_scroll_up" => __("Scroll up"),
1154 "select_article_cursor" => __("Select article under cursor"),
1155 "email_article" => __("Email article"),
1156 "close_article" => __("Close/collapse article"),
1157 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1158 "toggle_widescreen" => __("Toggle widescreen mode"),
1159 "toggle_embed_original" => __("Toggle embed original")),
1160 __("Article selection") => array(
1161 "select_all" => __("Select all articles"),
1162 "select_unread" => __("Select unread"),
1163 "select_marked" => __("Select starred"),
1164 "select_published" => __("Select published"),
1165 "select_invert" => __("Invert selection"),
1166 "select_none" => __("Deselect everything")),
1167 __("Feed") => array(
1168 "feed_refresh" => __("Refresh current feed"),
1169 "feed_unhide_read" => __("Un/hide read feeds"),
1170 "feed_subscribe" => __("Subscribe to feed"),
1171 "feed_edit" => __("Edit feed"),
1172 "feed_catchup" => __("Mark as read"),
1173 "feed_reverse" => __("Reverse headlines"),
1174 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1175 "feed_debug_update" => __("Debug feed update"),
1176 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1177 "catchup_all" => __("Mark all feeds as read"),
1178 "cat_toggle_collapse" => __("Un/collapse current category"),
1179 "toggle_combined_mode" => __("Toggle combined mode"),
1180 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1181 __("Go to") => array(
1182 "goto_all" => __("All articles"),
1183 "goto_fresh" => __("Fresh"),
1184 "goto_marked" => __("Starred"),
1185 "goto_published" => __("Published"),
1186 "goto_tagcloud" => __("Tag cloud"),
1187 "goto_prefs" => __("Preferences")),
1188 __("Other") => array(
1189 "create_label" => __("Create label"),
1190 "create_filter" => __("Create filter"),
1191 "collapse_sidebar" => __("Un/collapse sidebar"),
1192 "help_dialog" => __("Show help dialog"))
1193 );
1194
1195 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1196 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1197 }
1198
1199 return $hotkeys;
1200 }
1201
1202 function get_hotkeys_map() {
1203 $hotkeys = array(
1204 // "navigation" => array(
1205 "k" => "next_feed",
1206 "j" => "prev_feed",
1207 "n" => "next_article",
1208 "p" => "prev_article",
1209 "(38)|up" => "prev_article",
1210 "(40)|down" => "next_article",
1211 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1212 // "^(40)|Ctrl-down" => "next_article_noscroll",
1213 "(191)|/" => "search_dialog",
1214 // "article" => array(
1215 "s" => "toggle_mark",
1216 "*s" => "toggle_publ",
1217 "u" => "toggle_unread",
1218 "*t" => "edit_tags",
1219 "o" => "open_in_new_window",
1220 "c p" => "catchup_below",
1221 "c n" => "catchup_above",
1222 "*n" => "article_scroll_down",
1223 "*p" => "article_scroll_up",
1224 "*(38)|Shift+up" => "article_scroll_up",
1225 "*(40)|Shift+down" => "article_scroll_down",
1226 "a *w" => "toggle_widescreen",
1227 "a e" => "toggle_embed_original",
1228 "e" => "email_article",
1229 "a q" => "close_article",
1230 // "article_selection" => array(
1231 "a a" => "select_all",
1232 "a u" => "select_unread",
1233 "a *u" => "select_marked",
1234 "a p" => "select_published",
1235 "a i" => "select_invert",
1236 "a n" => "select_none",
1237 // "feed" => array(
1238 "f r" => "feed_refresh",
1239 "f a" => "feed_unhide_read",
1240 "f s" => "feed_subscribe",
1241 "f e" => "feed_edit",
1242 "f q" => "feed_catchup",
1243 "f x" => "feed_reverse",
1244 "f g" => "feed_toggle_vgroup",
1245 "f *d" => "feed_debug_update",
1246 "f *g" => "feed_debug_viewfeed",
1247 "f *c" => "toggle_combined_mode",
1248 "f c" => "toggle_cdm_expanded",
1249 "*q" => "catchup_all",
1250 "x" => "cat_toggle_collapse",
1251 // "goto" => array(
1252 "g a" => "goto_all",
1253 "g f" => "goto_fresh",
1254 "g s" => "goto_marked",
1255 "g p" => "goto_published",
1256 "g t" => "goto_tagcloud",
1257 "g *p" => "goto_prefs",
1258 // "other" => array(
1259 "(9)|Tab" => "select_article_cursor", // tab
1260 "c l" => "create_label",
1261 "c f" => "create_filter",
1262 "c s" => "collapse_sidebar",
1263 "^(191)|Ctrl+/" => "help_dialog",
1264 );
1265
1266 if (get_pref('COMBINED_DISPLAY_MODE')) {
1267 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1268 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1269 }
1270
1271 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1272 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1273 }
1274
1275 $prefixes = array();
1276
1277 foreach (array_keys($hotkeys) as $hotkey) {
1278 $pair = explode(" ", $hotkey, 2);
1279
1280 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1281 array_push($prefixes, $pair[0]);
1282 }
1283 }
1284
1285 return array($prefixes, $hotkeys);
1286 }
1287
1288 function check_for_update() {
1289 if (defined("GIT_VERSION_TIMESTAMP")) {
1290 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1291
1292 if ($content) {
1293 $content = json_decode($content, true);
1294
1295 if ($content && isset($content["changeset"])) {
1296 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
1297 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
1298
1299 return $content["changeset"]["id"];
1300 }
1301 }
1302 }
1303 }
1304
1305 return "";
1306 }
1307
1308 function make_runtime_info($disable_update_check = false) {
1309 $data = array();
1310
8adb3ec4
AD
1311 $pdo = Db::pdo();
1312
1313 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1314 ttrss_feeds WHERE owner_uid = ?");
1315 $sth->execute([$_SESSION['uid']]);
1316 $row = $sth->fetch();
aeb1abed 1317
8adb3ec4
AD
1318 $max_feed_id = $row['mid'];
1319 $num_feeds = $row['nf'];
aeb1abed
AD
1320
1321 $data["max_feed_id"] = (int) $max_feed_id;
1322 $data["num_feeds"] = (int) $num_feeds;
1323
1324 $data['last_article_id'] = Article::getLastArticleId();
1325 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1326
1327 $data['dep_ts'] = calculate_dep_timestamp();
1328 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1329
5e78b0c2 1330 $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
aeb1abed
AD
1331
1332 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1333 $update_result = @check_for_update();
1334
1335 $data["update_result"] = $update_result;
1336
1337 $_SESSION["last_version_check"] = time();
1338 }
1339
1340 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1341
1342 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1343
1344 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1345
1346 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1347
1348 if ($stamp) {
1349 $stamp_delta = time() - $stamp;
1350
1351 if ($stamp_delta > 1800) {
1352 $stamp_check = 0;
1353 } else {
1354 $stamp_check = 1;
1355 $_SESSION["daemon_stamp_check"] = time();
1356 }
1357
1358 $data['daemon_stamp_ok'] = $stamp_check;
1359
1360 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1361
1362 $data['daemon_stamp'] = $stamp_fmt;
1363 }
1364 }
1365 }
1366
1367 return $data;
1368 }
1369
1370 function search_to_sql($search, $search_language) {
1371
1372 $keywords = str_getcsv(trim($search), " ");
1373 $query_keywords = array();
1374 $search_words = array();
1375 $search_query_leftover = array();
1376
1377 if ($search_language)
1378 $search_language = db_escape_string(mb_strtolower($search_language));
1379 else
1380 $search_language = "english";
1381
1382 foreach ($keywords as $k) {
1383 if (strpos($k, "-") === 0) {
1384 $k = substr($k, 1);
1385 $not = "NOT";
1386 } else {
1387 $not = "";
1388 }
1389
1390 $commandpair = explode(":", mb_strtolower($k), 2);
1391
1392 switch ($commandpair[0]) {
1393 case "title":
1394 if ($commandpair[1]) {
1395 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
1396 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1397 } else {
1398 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1399 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1400 array_push($search_words, $k);
1401 }
1402 break;
1403 case "author":
1404 if ($commandpair[1]) {
1405 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
1406 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1407 } else {
1408 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1409 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1410 array_push($search_words, $k);
1411 }
1412 break;
1413 case "note":
1414 if ($commandpair[1]) {
1415 if ($commandpair[1] == "true")
1416 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1417 else if ($commandpair[1] == "false")
1418 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1419 else
1420 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
1421 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1422 } else {
1423 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1424 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1425 if (!$not) array_push($search_words, $k);
1426 }
1427 break;
1428 case "star":
1429
1430 if ($commandpair[1]) {
1431 if ($commandpair[1] == "true")
1432 array_push($query_keywords, "($not (marked = true))");
1433 else
1434 array_push($query_keywords, "($not (marked = false))");
1435 } else {
1436 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1437 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1438 if (!$not) array_push($search_words, $k);
1439 }
1440 break;
1441 case "pub":
1442 if ($commandpair[1]) {
1443 if ($commandpair[1] == "true")
1444 array_push($query_keywords, "($not (published = true))");
1445 else
1446 array_push($query_keywords, "($not (published = false))");
1447
1448 } else {
1449 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1450 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1451 if (!$not) array_push($search_words, $k);
1452 }
1453 break;
1454 case "unread":
1455 if ($commandpair[1]) {
1456 if ($commandpair[1] == "true")
1457 array_push($query_keywords, "($not (unread = true))");
1458 else
1459 array_push($query_keywords, "($not (unread = false))");
1460
1461 } else {
1462 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1463 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1464 if (!$not) array_push($search_words, $k);
1465 }
1466 break;
1467 default:
1468 if (strpos($k, "@") === 0) {
1469
1470 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1471 $orig_ts = strtotime(substr($k, 1));
1472 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1473
1474 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1475
1476 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
1477 } else {
1478
1479 if (DB_TYPE == "pgsql") {
1480 $k = mb_strtolower($k);
1481 array_push($search_query_leftover, $not ? "!$k" : $k);
1482 } else {
1483 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1484 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1485 }
1486
1487 if (!$not) array_push($search_words, $k);
1488 }
1489 }
1490 }
1491
1492 if (count($search_query_leftover) > 0) {
1493 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
1494
1495 if (DB_TYPE == "pgsql") {
1496 array_push($query_keywords,
1497 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
1498 }
1499
1500 }
1501
1502 $search_query_part = implode("AND", $query_keywords);
1503
1504 return array($search_query_part, $search_words);
1505 }
1506
1507 function iframe_whitelisted($entry) {
1508 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1509
1510 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1511
1512 if ($src) {
1513 foreach ($whitelist as $w) {
1514 if ($src == $w || $src == "www.$w")
1515 return true;
1516 }
1517 }
1518
1519 return false;
1520 }
1521
1522 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1523 if (!$owner) $owner = $_SESSION["uid"];
1524
1525 $res = trim($str); if (!$res) return '';
1526
1527 $charset_hack = '<head>
1528 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1529 </head>';
1530
1531 $res = trim($res); if (!$res) return '';
1532
1533 libxml_use_internal_errors(true);
1534
1535 $doc = new DOMDocument();
1536 $doc->loadHTML($charset_hack . $res);
1537 $xpath = new DOMXPath($doc);
1538
b2d42e96 1539 $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
aeb1abed
AD
1540
1541 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1542
1543 foreach ($entries as $entry) {
1544
1545 if ($entry->hasAttribute('href')) {
1546 $entry->setAttribute('href',
1547 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1548
1549 $entry->setAttribute('rel', 'noopener noreferrer');
1550 }
1551
1552 if ($entry->hasAttribute('src')) {
1553 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1554 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
1555
1556 if (file_exists($cached_filename)) {
1557
1558 // this is strictly cosmetic
1559 if ($entry->tagName == 'img') {
1560 $suffix = ".png";
1561 } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
1562 $suffix = ".mp4";
1563 } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
1564 $suffix = ".ogg";
1565 } else {
1566 $suffix = "";
1567 }
1568
1569 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1570
1571 if ($entry->hasAttribute('srcset')) {
1572 $entry->removeAttribute('srcset');
1573 }
1574
1575 if ($entry->hasAttribute('sizes')) {
1576 $entry->removeAttribute('sizes');
1577 }
1578 }
1579
1580 $entry->setAttribute('src', $src);
1581 }
1582
1583 if ($entry->nodeName == 'img') {
1584
1585 if ($entry->hasAttribute('src')) {
1586 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1587
9f7bd151 1588 if (is_prefix_https() && !$is_https_url) {
aeb1abed
AD
1589
1590 if ($entry->hasAttribute('srcset')) {
1591 $entry->removeAttribute('srcset');
1592 }
1593
1594 if ($entry->hasAttribute('sizes')) {
1595 $entry->removeAttribute('sizes');
1596 }
1597 }
1598 }
1599
1600 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1601 $force_remove_images || $_SESSION["bw_limit"]) {
1602
1603 $p = $doc->createElement('p');
1604
1605 $a = $doc->createElement('a');
1606 $a->setAttribute('href', $entry->getAttribute('src'));
1607
1608 $a->appendChild(new DOMText($entry->getAttribute('src')));
1609 $a->setAttribute('target', '_blank');
1610 $a->setAttribute('rel', 'noopener noreferrer');
1611
1612 $p->appendChild($a);
1613
1614 $entry->parentNode->replaceChild($p, $entry);
1615 }
1616 }
1617
1618 if (strtolower($entry->nodeName) == "a") {
1619 $entry->setAttribute("target", "_blank");
1620 $entry->setAttribute("rel", "noopener noreferrer");
1621 }
1622 }
1623
1624 $entries = $xpath->query('//iframe');
1625 foreach ($entries as $entry) {
1626 if (!iframe_whitelisted($entry)) {
1627 $entry->setAttribute('sandbox', 'allow-scripts');
1628 } else {
9f7bd151 1629 if (is_prefix_https()) {
aeb1abed
AD
1630 $entry->setAttribute("src",
1631 str_replace("http://", "https://",
1632 $entry->getAttribute("src")));
1633 }
1634 }
1635 }
1636
1637 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1638 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1639 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1640 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1641 'dt', 'em', 'footer', 'figure', 'figcaption',
6eeeec48 1642 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
aeb1abed
AD
1643 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1644 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1645 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1646 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1647 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1648
1649 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1650
1651 $disallowed_attributes = array('id', 'style', 'class');
1652
1653 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1654 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1655 if (is_array($retval)) {
1656 $doc = $retval[0];
1657 $allowed_elements = $retval[1];
1658 $disallowed_attributes = $retval[2];
1659 } else {
1660 $doc = $retval;
1661 }
1662 }
1663
1664 $doc->removeChild($doc->firstChild); //remove doctype
1665 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1666
1667 if ($highlight_words) {
1668 foreach ($highlight_words as $word) {
1669
1670 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1671
1672 $elements = $xpath->query("//*/text()");
1673
1674 foreach ($elements as $child) {
1675
1676 $fragment = $doc->createDocumentFragment();
1677 $text = $child->textContent;
1678
1679 while (($pos = mb_stripos($text, $word)) !== false) {
1680 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1681 $word = mb_substr($text, $pos, mb_strlen($word));
1682 $highlight = $doc->createElement('span');
1683 $highlight->appendChild(new DomText($word));
1684 $highlight->setAttribute('class', 'highlight');
1685 $fragment->appendChild($highlight);
1686 $text = mb_substr($text, $pos + mb_strlen($word));
1687 }
1688
1689 if (!empty($text)) $fragment->appendChild(new DomText($text));
1690
1691 $child->parentNode->replaceChild($fragment, $child);
1692 }
1693 }
1694 }
1695
1696 $res = $doc->saveHTML();
1697
1698 /* strip everything outside of <body>...</body> */
1699
1700 $res_frag = array();
1701 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1702 return $res_frag[1];
1703 } else {
1704 return $res;
1705 }
1706 }
1707
1708 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1709 $xpath = new DOMXPath($doc);
1710 $entries = $xpath->query('//*');
1711
1712 foreach ($entries as $entry) {
1713 if (!in_array($entry->nodeName, $allowed_elements)) {
1714 $entry->parentNode->removeChild($entry);
1715 }
1716
1717 if ($entry->hasAttributes()) {
1718 $attrs_to_remove = array();
1719
1720 foreach ($entry->attributes as $attr) {
1721
1722 if (strpos($attr->nodeName, 'on') === 0) {
1723 array_push($attrs_to_remove, $attr);
1724 }
1725
1726 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1727 array_push($attrs_to_remove, $attr);
1728 }
1729
1730 if (in_array($attr->nodeName, $disallowed_attributes)) {
1731 array_push($attrs_to_remove, $attr);
1732 }
1733 }
1734
1735 foreach ($attrs_to_remove as $attr) {
1736 $entry->removeAttributeNode($attr);
1737 }
1738 }
1739 }
1740
1741 return $doc;
1742 }
1743
1744 function trim_array($array) {
1745 $tmp = $array;
1746 array_walk($tmp, 'trim');
1747 return $tmp;
1748 }
1749
1750 function tag_is_valid($tag) {
1751 if ($tag == '') return false;
1752 if (is_numeric($tag)) return false;
1753 if (mb_strlen($tag) > 250) return false;
1754
1755 if (!$tag) return false;
1756
1757 return true;
1758 }
1759
1760 function render_login_form() {
1761 header('Cache-Control: public');
1762
1763 require_once "login_form.php";
1764 exit;
1765 }
1766
1767 function T_sprintf() {
1768 $args = func_get_args();
1769 return vsprintf(__(array_shift($args)), $args);
1770 }
1771
1772 function print_checkpoint($n, $s) {
1773 $ts = microtime(true);
1774 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1775 return $ts;
1776 }
1777
1778 function sanitize_tag($tag) {
1779 $tag = trim($tag);
1780
1781 $tag = mb_strtolower($tag, 'utf-8');
1782
1783 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1784
1785 if (DB_TYPE == "mysql") {
1786 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1787 }
1788
1789 return $tag;
1790 }
1791
9f7bd151 1792 function is_server_https() {
e234ac8d 1793 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
9f7bd151
AD
1794 }
1795
1796 function is_prefix_https() {
1797 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1798 }
1799
b2d42e96 1800 // this returns SELF_URL_PATH sans ending slash
aeb1abed
AD
1801 function get_self_url_prefix() {
1802 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1803 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1804 } else {
1805 return SELF_URL_PATH;
1806 }
1807 }
1808
aeb1abed
AD
1809 function encrypt_password($pass, $salt = '', $mode2 = false) {
1810 if ($salt && $mode2) {
1811 return "MODE2:" . hash('sha256', $salt . $pass);
1812 } else if ($salt) {
1813 return "SHA1X:" . sha1("$salt:$pass");
1814 } else {
1815 return "SHA1:" . sha1($pass);
1816 }
1817 } // function encrypt_password
1818
1819 function load_filters($feed_id, $owner_uid) {
1820 $filters = array();
1821
0086a897 1822 $cat_id = (int)Feeds::getFeedCategory($feed_id);
aeb1abed
AD
1823
1824 if ($cat_id == 0)
1825 $null_cat_qpart = "cat_id IS NULL OR";
1826 else
1827 $null_cat_qpart = "";
1828
8adb3ec4
AD
1829 $pdo = Db::pdo();
1830
1831 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1832 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1833 $sth->execute([$owner_uid]);
aeb1abed 1834
02f3992a 1835 $check_cats = array_merge(
aeb1abed 1836 Feeds::getParentCategories($cat_id, $owner_uid),
02f3992a
AD
1837 [$cat_id]);
1838
1839 $check_cats_str = join(",", $check_cats);
1840 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
aeb1abed 1841
8adb3ec4 1842 while ($line = $sth->fetch()) {
aeb1abed
AD
1843 $filter_id = $line["id"];
1844
02f3992a
AD
1845 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1846
8adb3ec4 1847 $sth2 = $pdo->prepare("SELECT
02f3992a 1848 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
aeb1abed
AD
1849 FROM ttrss_filters2_rules AS r,
1850 ttrss_filter_types AS t
1851 WHERE
b2d42e96 1852 (match_on IS NOT NULL OR
02f3992a 1853 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
8adb3ec4
AD
1854 (feed_id IS NULL OR feed_id = ?))) AND
1855 filter_type = t.id AND filter_id = ?");
1856 $sth2->execute([$feed_id, $filter_id]);
aeb1abed
AD
1857
1858 $rules = array();
1859 $actions = array();
1860
8adb3ec4 1861 while ($rule_line = $sth2->fetch()) {
aeb1abed
AD
1862 # print_r($rule_line);
1863
02f3992a
AD
1864 if ($rule_line["match_on"]) {
1865 $match_on = json_decode($rule_line["match_on"], true);
aeb1abed 1866
02f3992a 1867 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
aeb1abed 1868
02f3992a
AD
1869 $rule = array();
1870 $rule["reg_exp"] = $rule_line["reg_exp"];
1871 $rule["type"] = $rule_line["type_name"];
1872 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1873
1874 array_push($rules, $rule);
1875 } else if (!$match_any_rule) {
1876 // this filter contains a rule that doesn't match to this feed/category combination
1877 // thus filter has to be rejected
aeb1abed 1878
02f3992a
AD
1879 $rules = [];
1880 break;
1881 }
aeb1abed 1882
02f3992a 1883 } else {
0bf7e007 1884
02f3992a
AD
1885 $rule = array();
1886 $rule["reg_exp"] = $rule_line["reg_exp"];
1887 $rule["type"] = $rule_line["type_name"];
1888 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1889
1890 array_push($rules, $rule);
1891 }
aeb1abed
AD
1892 }
1893
02f3992a 1894 if (count($rules) > 0) {
8adb3ec4 1895 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
02f3992a
AD
1896 FROM ttrss_filters2_actions AS a,
1897 ttrss_filter_actions AS t
1898 WHERE
8adb3ec4
AD
1899 action_id = t.id AND filter_id = ?");
1900 $sth2->execute([$filter_id]);
02f3992a 1901
8adb3ec4 1902 while ($action_line = $sth2->fetch()) {
02f3992a
AD
1903 # print_r($action_line);
1904
1905 $action = array();
1906 $action["type"] = $action_line["type_name"];
1907 $action["param"] = $action_line["action_param"];
1908
1909 array_push($actions, $action);
1910 }
1911 }
aeb1abed
AD
1912
1913 $filter = array();
1914 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1915 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1916 $filter["rules"] = $rules;
1917 $filter["actions"] = $actions;
1918
1919 if (count($rules) > 0 && count($actions) > 0) {
1920 array_push($filters, $filter);
1921 }
1922 }
1923
1924 return $filters;
1925 }
1926
1927 function get_score_pic($score) {
1928 if ($score > 100) {
1929 return "score_high.png";
1930 } else if ($score > 0) {
1931 return "score_half_high.png";
1932 } else if ($score < -100) {
1933 return "score_low.png";
1934 } else if ($score < 0) {
1935 return "score_half_low.png";
1936 } else {
1937 return "score_neutral.png";
1938 }
1939 }
1940
1941 function feed_has_icon($id) {
1942 return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0;
1943 }
1944
1945 function init_plugins() {
1946 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1947
1948 return true;
1949 }
1950
1951 function add_feed_category($feed_cat, $parent_cat_id = false) {
1952
1953 if (!$feed_cat) return false;
1954
8adb3ec4 1955 ////db_query("BEGIN");
aeb1abed 1956
aeb1abed
AD
1957 $feed_cat = mb_substr($feed_cat, 0, 250);
1958
8adb3ec4
AD
1959 $pdo = Db::pdo();
1960
1961 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
b78a6f08
AD
1962 WHERE (:parent IS NULL AND parent_cat IS NULL OR parent_cat = :parent)
1963 AND title = :cat AND owner_uid = :uid");
1964 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
aeb1abed 1965
b78a6f08 1966 if ($sth->fetch()) {
aeb1abed 1967
b78a6f08
AD
1968 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1969 VALUES (?, ?, ?)");
1970 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
aeb1abed 1971
b78a6f08 1972 //db_query("COMMIT");
aeb1abed
AD
1973
1974 return true;
1975 }
1976
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
a21f7495 2046 $pdo = Db::pdo();
aeb1abed 2047
a21f7495
AD
2048 $sth = $dbh->prepare("SELECT access_key FROM ttrss_access_keys
2049 WHERE feed_id = ? AND is_cat = ?
2050 AND owner_uid = ?");
2051 $sth->execute([$feed_id, $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
2062 $sth->execute([$key, $feed_id, $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
2214 if (DB_TYPE == "pgsql")
2215 $reg_qpart = "~";
2216 else
2217 $reg_qpart = "REGEXP";
2218
2219 foreach ($filter["rules"] AS $rule) {
2220 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2221 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2222 $rule['reg_exp']) !== FALSE;
2223
2224 if ($regexp_valid) {
2225
2226 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2227
2228 switch ($rule["type"]) {
2229 case "title":
2230 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2231 $rule['reg_exp'] . "')";
2232 break;
2233 case "content":
2234 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2235 $rule['reg_exp'] . "')";
2236 break;
2237 case "both":
2238 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2239 $rule['reg_exp'] . "') OR LOWER(" .
2240 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2241 break;
2242 case "tag":
2243 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2244 $rule['reg_exp'] . "')";
2245 break;
2246 case "link":
2247 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2248 $rule['reg_exp'] . "')";
2249 break;
2250 case "author":
2251 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2252 $rule['reg_exp'] . "')";
2253 break;
2254 }
2255
2256 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2257
2258 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2259 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2260 }
2261
2262 if (isset($rule["cat_id"])) {
2263
2264 if ($rule["cat_id"] > 0) {
2265 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2266 array_push($children, $rule["cat_id"]);
2267
2268 $children = join(",", $children);
2269
2270 $cat_qpart = "cat_id IN ($children)";
2271 } else {
2272 $cat_qpart = "cat_id IS NULL";
2273 }
2274
2275 $qpart .= " AND $cat_qpart";
2276 }
2277
2278 $qpart .= " AND feed_id IS NOT NULL";
2279
2280 array_push($query, "($qpart)");
2281
2282 }
2283 }
2284
2285 if (count($query) > 0) {
2286 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2287 } else {
2288 $fullquery = "(false)";
2289 }
2290
2291 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2292
2293 return $fullquery;
2294 }
2295
2296 if (!function_exists('gzdecode')) {
2297 function gzdecode($string) { // no support for 2nd argument
2298 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2299 base64_encode($string));
2300 }
2301 }
2302
2303 function get_random_bytes($length) {
2304 if (function_exists('openssl_random_pseudo_bytes')) {
2305 return openssl_random_pseudo_bytes($length);
2306 } else {
2307 $output = "";
2308
2309 for ($i = 0; $i < $length; $i++)
2310 $output .= chr(mt_rand(0, 255));
2311
2312 return $output;
2313 }
2314 }
2315
2316 function read_stdin() {
2317 $fp = fopen("php://stdin", "r");
2318
2319 if ($fp) {
2320 $line = trim(fgets($fp));
2321 fclose($fp);
2322 return $line;
2323 }
2324
2325 return null;
2326 }
2327
aeb1abed
AD
2328 function implements_interface($class, $interface) {
2329 return in_array($interface, class_implements($class));
2330 }
2331
2332 function get_minified_js($files) {
2333 require_once 'lib/jshrink/Minifier.php';
2334
2335 $rv = '';
2336
2337 foreach ($files as $js) {
2338 if (!isset($_GET['debug'])) {
2339 $cached_file = CACHE_DIR . "/js/".basename($js).".js";
2340
2341 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2342
2343 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2344
2345 if ($header && $contents) {
2346 list($htag, $hversion) = explode(":", $header);
2347
2348 if ($htag == "tt-rss" && $hversion == VERSION) {
2349 $rv .= $contents;
2350 continue;
2351 }
2352 }
2353 }
2354
2355 $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js"));
2356 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2357 $rv .= $minified;
2358
2359 } else {
2360 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2361 }
2362 }
2363
2364 return $rv;
2365 }
2366
2367 function calculate_dep_timestamp() {
2368 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2369
2370 $max_ts = -1;
2371
2372 foreach ($files as $file) {
2373 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2374 }
2375
2376 return $max_ts;
2377 }
2378
2379 function T_js_decl($s1, $s2) {
2380 if ($s1 && $s2) {
2381 $s1 = preg_replace("/\n/", "", $s1);
2382 $s2 = preg_replace("/\n/", "", $s2);
2383
2384 $s1 = preg_replace("/\"/", "\\\"", $s1);
2385 $s2 = preg_replace("/\"/", "\\\"", $s2);
2386
2387 return "T_messages[\"$s1\"] = \"$s2\";\n";
2388 }
2389 }
2390
2391 function init_js_translations() {
2392
2393 print 'var T_messages = new Object();
b2d42e96 2394
aeb1abed
AD
2395 function __(msg) {
2396 if (T_messages[msg]) {
2397 return T_messages[msg];
2398 } else {
2399 return msg;
2400 }
2401 }
b2d42e96 2402
aeb1abed
AD
2403 function ngettext(msg1, msg2, n) {
2404 return __((parseInt(n) > 1) ? msg2 : msg1);
2405 }';
2406
2407 $l10n = _get_reader();
2408
2409 for ($i = 0; $i < $l10n->total; $i++) {
2410 $orig = $l10n->get_original_string($i);
2411 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2412 $key = explode(chr(0), $orig);
2413 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2414 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2415 } else {
2416 $translation = __($orig);
2417 print T_js_decl($orig, $translation);
2418 }
2419 }
2420 }
2421
aeb1abed
AD
2422 function get_theme_path($theme) {
2423 $check = "themes/$theme";
2424 if (file_exists($check)) return $check;
2425
2426 $check = "themes.local/$theme";
2427 if (file_exists($check)) return $check;
2428 }
2429
2430 function theme_valid($theme) {
2431 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2432
2433 if (in_array($theme, $bundled_themes)) return true;
2434
2435 $file = "themes/" . basename($theme);
2436
2437 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2438
2439 if (file_exists($file) && is_readable($file)) {
2440 $fh = fopen($file, "r");
2441
2442 if ($fh) {
2443 $header = fgets($fh);
2444 fclose($fh);
2445
2446 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2447 }
2448 }
2449
2450 return false;
2451 }
2452
2453 /**
2454 * @SuppressWarnings(unused)
2455 */
2456 function error_json($code) {
2457 require_once "errors.php";
2458
2459 @$message = $ERRORS[$code];
2460
2461 return json_encode(array("error" =>
2462 array("code" => $code, "message" => $message)));
2463
2464 }
2465
904aff76 2466 /*function abs_to_rel_path($dir) {
aeb1abed
AD
2467 $tmp = str_replace(dirname(__DIR__), "", $dir);
2468
2469 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2470
2471 return $tmp;
904aff76 2472 }*/
aeb1abed
AD
2473
2474 function get_upload_error_message($code) {
2475
2476 $errors = array(
2477 0 => __('There is no error, the file uploaded with success'),
2478 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2479 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2480 3 => __('The uploaded file was only partially uploaded'),
2481 4 => __('No file was uploaded'),
2482 6 => __('Missing a temporary folder'),
2483 7 => __('Failed to write file to disk.'),
2484 8 => __('A PHP extension stopped the file upload.'),
2485 );
2486
2487 return $errors[$code];
2488 }
2489
2490 function base64_img($filename) {
2491 if (file_exists($filename)) {
2492 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2493
2494 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2495 } else {
2496 return "";
2497 }
2498 }
2499
8b73bd28
AD
2500 /* this is essentially a wrapper for readfile() which allows plugins to hook
2501 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2502
2503 hook function should return true if request was handled (or at least attempted to)
2504
2505 note that this can be called without user context so the plugin to handle this
2506 should be loaded systemwide in config.php */
2507 function send_local_file($filename) {
2508 if (file_exists($filename)) {
2509 $tmppluginhost = new PluginHost();
2510
2511 $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
2512 $tmppluginhost->load_data();
2513
2514 foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
2515 if ($plugin->hook_send_local_file($filename)) return true;
2516 }
2517
2518 $mimetype = mime_content_type($filename);
2519 header("Content-type: $mimetype");
2520
2521 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2522 header("Last-Modified: $stamp", true);
2523
2524 return readfile($filename);
2525 } else {
2526 return false;
2527 }
2528 }
2529
0b68b162 2530 function check_mysql_tables() {
a21f7495 2531 $pdo = Db::pdo();
0b68b162 2532
a21f7495
AD
2533 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2534 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2535 $sth->execute([DB_NAME]);
0b68b162
AD
2536
2537 $bad_tables = [];
2538
a21f7495 2539 while ($line = $sth->fetch()) {
0b68b162
AD
2540 array_push($bad_tables, $line);
2541 }
2542
2543 return $bad_tables;
2544 }
2545
90dafaa9
AD
2546 function arr_qmarks($arr) {
2547 return str_repeat('?,', count($arr) - 1) . '?';
2548 }