]> git.wh0rd.org - tt-rss.git/blame - include/functions.php
Merge branch 'pdo-experimental' of git.fakecake.org:tt-rss into pdo-experimental
[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
603 FROM ttrss_user_prefs WHERE owner_uid = ? AND profile = ?");
604 $u_sth->execute([$uid, $profile]);
ff485f1d
AD
605
606 $active_prefs = array();
607
8adb3ec4 608 while ($line = $u_sth->fetch()) {
8d505d78 609 array_push($active_prefs, $line["pref_name"]);
ff485f1d
AD
610 }
611
8adb3ec4 612 while ($line = $sth->fetch()) {
ff485f1d
AD
613 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
614// print "adding " . $line["pref_name"] . "<br>";
615
a42c55f0
AD
616 $line["def_value"] = db_escape_string($line["def_value"]);
617 $line["pref_name"] = db_escape_string($line["pref_name"]);
d296ba50 618
6322ac79 619 if (get_schema_version() < 63) {
8adb3ec4 620 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
8d505d78 621 (owner_uid,pref_name,value) VALUES
8adb3ec4
AD
622 (?, ?, ?)");
623 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
f9aa6a89
AD
624
625 } else {
8adb3ec4 626 $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
8d505d78 627 (owner_uid,pref_name,value, profile) VALUES
8adb3ec4
AD
628 (?, ?, ?, ?)");
629 $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
f9aa6a89 630 }
ff485f1d
AD
631
632 }
633 }
634
8adb3ec4 635 ////db_query("COMMIT");
ff485f1d
AD
636
637 }
956c7629 638
8de8bfb8
AD
639 function get_ssl_certificate_id() {
640 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
641 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
642 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
643 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
644 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
645 }
ac617ebc
GG
646 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
647 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
648 $_SERVER["SSL_CLIENT_V_START"] .
649 $_SERVER["SSL_CLIENT_V_END"] .
650 $_SERVER["SSL_CLIENT_S_DN"]);
651 }
8de8bfb8
AD
652 return "";
653 }
654
a42c55f0 655 function authenticate_user($login, $password, $check_only = false) {
c8437f35 656
131b01b3 657 if (!SINGLE_USER_MODE) {
0d421af8 658 $user_id = false;
0f28f81f 659
1ffe3391 660 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
0f28f81f
AD
661
662 $user_id = (int) $plugin->authenticate($login, $password);
663
664 if ($user_id) {
665 $_SESSION["auth_module"] = strtolower(get_class($plugin));
666 break;
667 }
461766f3
AD
668 }
669
0d421af8 670 if ($user_id && !$check_only) {
70400171
AD
671 @session_start();
672
0d421af8 673 $_SESSION["uid"] = $user_id;
3472c4c5 674 $_SESSION["version"] = VERSION_STATIC;
0d421af8 675
8adb3ec4
AD
676 $pdo = DB::pdo();
677 $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
678 WHERE id = ?");
679 $sth->execute([$user_id]);
680 $row = $sth->fetch();
8d505d78 681
8adb3ec4
AD
682 $_SESSION["name"] = $row["login"];
683 $_SESSION["access_level"] = $row["access_level"];
3ceb893f 684 $_SESSION["csrf_token"] = uniqid_short();
8d505d78 685
8adb3ec4
AD
686 $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
687 $usth->execute([$user_id]);
8d505d78 688
131b01b3 689 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
837ec70e 690 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
8adb3ec4 691 $_SESSION["pwd_hash"] = $row["pwd_hash"];
91c5f229
AD
692
693 $_SESSION["last_version_check"] = time();
8d505d78 694
a42c55f0 695 initialize_user_prefs($_SESSION["uid"]);
8d505d78 696
131b01b3
AD
697 return true;
698 }
8d505d78 699
131b01b3 700 return false;
503eb349 701
131b01b3 702 } else {
503eb349 703
131b01b3
AD
704 $_SESSION["uid"] = 1;
705 $_SESSION["name"] = "admin";
787e5ebc 706 $_SESSION["access_level"] = 10;
21e42e5f 707
0d421af8
AD
708 $_SESSION["hide_hello"] = true;
709 $_SESSION["hide_logout"] = true;
710
d5fd183d
AD
711 $_SESSION["auth_module"] = false;
712
21e42e5f 713 if (!$_SESSION["csrf_token"]) {
3ceb893f 714 $_SESSION["csrf_token"] = uniqid_short();
21e42e5f 715 }
f557cd78 716
0bbba72d 717 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
8d505d78 718
a42c55f0 719 initialize_user_prefs($_SESSION["uid"]);
8d505d78 720
c8437f35
AD
721 return true;
722 }
c8437f35
AD
723 }
724
e6cb77a0
AD
725 function make_password($length = 8) {
726
85db6213
AD
727 $password = "";
728 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
729
730 $i = 0;
731
732 while ($i < $length) {
733 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
734
735 if (!strstr($password, $char)) {
736 $password .= $char;
737 $i++;
738 }
739 }
740 return $password;
e6cb77a0
AD
741 }
742
743 // this is called after user is created to initialize default feeds, labels
744 // or whatever else
8d505d78 745
e6cb77a0
AD
746 // user preferences are checked on every login, not here
747
a42c55f0 748 function initialize_user($uid) {
e6cb77a0 749
8adb3ec4
AD
750 $pdo = DB::pdo();
751
752 $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
753 values (?, 'Tiny Tiny RSS: Forum',
f0855b88 754 'http://tt-rss.org/forum/rss.php')");
8adb3ec4 755 $sth->execute([$uid]);
3b0feb9b 756 }
e6cb77a0 757
b8aa49bc 758 function logout_user() {
5ccc1cf5
AD
759 session_destroy();
760 if (isset($_COOKIE[session_name()])) {
761 setcookie(session_name(), '', time()-42000, '/');
762 }
b8aa49bc
AD
763 }
764
8484ce22
AD
765 function validate_csrf($csrf_token) {
766 return $csrf_token == $_SESSION['csrf_token'];
767 }
768
5cbd1fe8
AD
769 function load_user_plugins($owner_uid, $pluginhost = false) {
770
771 if (!$pluginhost) $pluginhost = PluginHost::getInstance();
772
6d45a152 773 if ($owner_uid && SCHEMA_VERSION >= 100) {
a42c55f0 774 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
de612e7a 775
5cbd1fe8 776 $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
e9c04fd4 777
6322ac79 778 if (get_schema_version() > 100) {
5cbd1fe8 779 $pluginhost->load_data();
e9c04fd4 780 }
de612e7a
AD
781 }
782 }
783
6322ac79 784 function login_sequence() {
8adb3ec4
AD
785 $pdo = Db::pdo();
786
97acbaf1 787 if (SINGLE_USER_MODE) {
25db6c51 788 @session_start();
a42c55f0 789 authenticate_user("admin", null);
68349f55 790 startup_gettext();
a42c55f0 791 load_user_plugins($_SESSION["uid"]);
97acbaf1 792 } else {
6322ac79 793 if (!validate_session()) $_SESSION["uid"] = false;
d0eef2a3
AD
794
795 if (!$_SESSION["uid"]) {
97acbaf1 796
a42c55f0
AD
797 if (AUTH_AUTO_LOGIN && authenticate_user(null, null)) {
798 $_SESSION["ref_schema_version"] = get_schema_version(true);
97acbaf1 799 } else {
a42c55f0 800 authenticate_user(null, null, true);
97acbaf1
AD
801 }
802
d0eef2a3 803 if (!$_SESSION["uid"]) {
d0eef2a3
AD
804 @session_destroy();
805 setcookie(session_name(), '', time()-42000, '/');
9ce7a554 806
6322ac79 807 render_login_form();
d0eef2a3
AD
808 exit;
809 }
4ad99f23 810
97acbaf1
AD
811 } else {
812 /* bump login timestamp */
8adb3ec4
AD
813 $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
814 $sth->execute([$_SESSION['uid']]);
815
06b0777f 816 $_SESSION["last_login_update"] = time();
01a87dff
AD
817 }
818
de612e7a 819 if ($_SESSION["uid"]) {
7b149552 820 startup_gettext();
a42c55f0 821 load_user_plugins($_SESSION["uid"]);
b1b1d25f
AD
822
823 /* cleanup ccache */
824
8adb3ec4
AD
825 $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
826 AND
b1b1d25f
AD
827 (SELECT COUNT(id) FROM ttrss_feeds WHERE
828 ttrss_feeds.id = feed_id) = 0");
829
8adb3ec4
AD
830 $sth->execute($_SESSION['uid']);
831
832 $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
833 AND
b1b1d25f
AD
834 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
835 ttrss_feed_categories.id = feed_id) = 0");
836
8adb3ec4 837 $sth->execute($_SESSION['uid']);
de612e7a 838 }
b1b1d25f 839
b8aa49bc 840 }
afc3cf55 841 }
3547842a 842
411fe209 843 function truncate_string($str, $max_len, $suffix = '&hellip;') {
878a0083 844 if (mb_strlen($str, "utf-8") > $max_len) {
411fe209 845 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
3547842a
AD
846 } else {
847 return $str;
848 }
849 }
54a60e1a 850
cc43e19b
AD
851 // is not utf8 clean
852 function truncate_middle($str, $max_len, $suffix = '&hellip;') {
853 if (strlen($str) > $max_len) {
854 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
855 } else {
856 return $str;
857 }
858 }
859
ab4b768f
AD
860 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
861
862 try {
863 $source_tz = new DateTimeZone($source_tz);
864 } catch (Exception $e) {
865 $source_tz = new DateTimeZone('UTC');
866 }
867
868 try {
869 $dest_tz = new DateTimeZone($dest_tz);
870 } catch (Exception $e) {
871 $dest_tz = new DateTimeZone('UTC');
872 }
873
874 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
875 return $dt->format('U') + $dest_tz->getOffset($dt);
876 }
877
a42c55f0 878 function make_local_datetime($timestamp, $long, $owner_uid = false,
b6714c77 879 $no_smart_dt = false, $eta_min = false) {
324944f3
AD
880
881 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
882 if (!$timestamp) $timestamp = '1970-01-01 0:00';
883
7d96bfcd 884 global $utc_tz;
5ddef5ba
AD
885 global $user_tz;
886
887 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
324944f3 888
90e5f4f1
AD
889 $timestamp = substr($timestamp, 0, 19);
890
7d96bfcd
AD
891 # We store date in UTC internally
892 $dt = new DateTime($timestamp, $utc_tz);
893
5ddef5ba 894 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
7d96bfcd 895
6bfc97da 896 if ($user_tz_string != 'Automatic') {
324944f3 897
6bfc97da
AD
898 try {
899 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
900 } catch (Exception $e) {
901 $user_tz = $utc_tz;
902 }
903
904 $tz_offset = $user_tz->getOffset($dt);
905 } else {
96f0cbe3 906 $tz_offset = (int) -$_SESSION["clientTzOffset"];
6bfc97da 907 }
5ddef5ba 908
7d96bfcd 909 $user_timestamp = $dt->format('U') + $tz_offset;
324944f3 910
1dc52ae7 911 if (!$no_smart_dt) {
a42c55f0 912 return smart_date_time($user_timestamp,
b6714c77 913 $tz_offset, $owner_uid, $eta_min);
324944f3
AD
914 } else {
915 if ($long)
a42c55f0 916 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
324944f3 917 else
a42c55f0 918 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
324944f3
AD
919
920 return date($format, $user_timestamp);
921 }
922 }
923
b6714c77 924 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
2a5c136e
AD
925 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
926
97aa917c 927 if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
46973af5
AD
928 return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
929 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
be773442 930 return date("G:i", $timestamp);
2a5c136e 931 } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
a42c55f0 932 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
2a5c136e 933 return date($format, $timestamp);
be773442 934 } else {
a42c55f0 935 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
2a5c136e 936 return date($format, $timestamp);
be773442
AD
937 }
938 }
939
e3c99f3b 940 function sql_bool_to_bool($s) {
9955a134 941 if ($s == "t" || $s == "1" || strtolower($s) == "true") {
e3c99f3b
AD
942 return true;
943 } else {
944 return false;
945 }
946 }
8d505d78 947
badac687
AD
948 function bool_to_sql_bool($s) {
949 if ($s) {
950 return "true";
951 } else {
952 return "false";
953 }
954 }
e3c99f3b 955
fcfa9ef1
AD
956 // Session caching removed due to causing wrong redirects to upgrade
957 // script when get_schema_version() is called on an obsolete session
958 // created on a previous schema version.
a42c55f0 959 function get_schema_version($nocache = false) {
7d96bfcd
AD
960 global $schema_version;
961
8adb3ec4
AD
962 $pdo = DB::pdo();
963
e2cf81e2 964 if (!$schema_version && !$nocache) {
8adb3ec4
AD
965 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
966 $version = $row["schema_version"];
7d96bfcd 967 $schema_version = $version;
199db684 968 return $version;
7d96bfcd
AD
969 } else {
970 return $schema_version;
971 }
e4c51a6c
AD
972 }
973
6322ac79 974 function sanity_check() {
31303c6b 975 require_once 'errors.php';
cacc1877 976 global $ERRORS;
ebb948c2 977
6043fb7e 978 $error_code = 0;
a42c55f0 979 $schema_version = get_schema_version(true);
6043fb7e
AD
980
981 if ($schema_version != SCHEMA_VERSION) {
982 $error_code = 5;
983 }
984
a42c55f0 985 if (db_escape_string("testTEST") != "testTEST") {
f29ba148
AD
986 $error_code = 12;
987 }
988
ebb948c2 989 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
6043fb7e
AD
990 }
991
27981ca3 992 function file_is_locked($filename) {
8ff2a86c
AD
993 if (file_exists(LOCK_DIRECTORY . "/$filename")) {
994 if (function_exists('flock')) {
995 $fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
996 if ($fp) {
997 if (flock($fp, LOCK_EX | LOCK_NB)) {
998 flock($fp, LOCK_UN);
999 fclose($fp);
1000 return false;
1001 }
31a6d42d 1002 fclose($fp);
8ff2a86c
AD
1003 return true;
1004 } else {
31a6d42d
AD
1005 return false;
1006 }
27981ca3 1007 }
8ff2a86c
AD
1008 return true; // consider the file always locked and skip the test
1009 } else {
1010 return false;
27981ca3 1011 }
27981ca3
AD
1012 }
1013
8ff2a86c 1014
fcb4c0c9 1015 function make_lockfile($filename) {
cfa43e02 1016 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
fcb4c0c9 1017
a44bfcfd 1018 if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
58fc7095
AD
1019 $stat_h = fstat($fp);
1020 $stat_f = stat(LOCK_DIRECTORY . "/$filename");
1021
1fcebfb3
AD
1022 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
1023 if ($stat_h["ino"] != $stat_f["ino"] ||
1024 $stat_h["dev"] != $stat_f["dev"]) {
1025
1026 return false;
1027 }
58fc7095
AD
1028 }
1029
4c59adb1
AD
1030 if (function_exists('posix_getpid')) {
1031 fwrite($fp, posix_getpid() . "\n");
1032 }
fcb4c0c9
AD
1033 return $fp;
1034 } else {
1035 return false;
1036 }
1037 }
1038
bf7fcde8 1039 function make_stampfile($filename) {
cfa43e02 1040 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
bf7fcde8 1041
8e00ae9b 1042 if (flock($fp, LOCK_EX | LOCK_NB)) {
bf7fcde8 1043 fwrite($fp, time() . "\n");
8e00ae9b 1044 flock($fp, LOCK_UN);
bf7fcde8
AD
1045 fclose($fp);
1046 return true;
1047 } else {
1048 return false;
1049 }
1050 }
1051
894ebcf5 1052 function sql_random_function() {
8c0496f7 1053 if (DB_TYPE == "mysql") {
894ebcf5
AD
1054 return "RAND()";
1055 } else {
1056 return "RANDOM()";
1057 }
1058 }
1059
ff5cc7d7 1060 function getFeedUnread($feed, $is_cat = false) {
86a8351c 1061 return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
ff5cc7d7
AD
1062 }
1063
ff5cc7d7
AD
1064 function checkbox_to_sql_bool($val) {
1065 return ($val == "on") ? "true" : "false";
1066 }
1067
3ceb893f
AD
1068 function uniqid_short() {
1069 return uniqid(base_convert(rand(), 10, 36));
1070 }
1071
aeb1abed
AD
1072 function make_init_params() {
1073 $params = array();
1074
1075 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1076 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1077 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1078 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1079
1080 $params[strtolower($param)] = (int) get_pref($param);
1081 }
1082
1083 $params["icons_url"] = ICONS_URL;
1084 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
1085 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1086 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1087 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1088 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1089 $params["label_base_index"] = (int) LABEL_BASE_INDEX;
1090
1091 $theme = get_pref( "USER_CSS_THEME", false, false);
1092 $params["theme"] = theme_valid("$theme") ? $theme : "";
1093
1094 $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
1095
1096 $params["php_platform"] = PHP_OS;
1097 $params["php_version"] = PHP_VERSION;
1098
1099 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1100
8adb3ec4
AD
1101 $pdo = Db::pdo();
1102
1103 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1104 ttrss_feeds WHERE owner_uid = ?");
1105 $sth->execute([$_SESSION['uid']]);
1106 $row = $sth->fetch();
aeb1abed 1107
8adb3ec4
AD
1108 $max_feed_id = $row["mid"];
1109 $num_feeds = $row["nf"];
aeb1abed
AD
1110
1111 $params["max_feed_id"] = (int) $max_feed_id;
1112 $params["num_feeds"] = (int) $num_feeds;
1113
1114 $params["hotkeys"] = get_hotkeys_map();
1115
1116 $params["csrf_token"] = $_SESSION["csrf_token"];
1117 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1118
1119 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
1120
1121 $params["icon_alert"] = base64_img("images/alert.png");
1122 $params["icon_information"] = base64_img("images/information.png");
1123 $params["icon_cross"] = base64_img("images/cross.png");
1124 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1125
5e78b0c2
AD
1126 $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1127
aeb1abed
AD
1128 return $params;
1129 }
1130
1131 function get_hotkeys_info() {
1132 $hotkeys = array(
1133 __("Navigation") => array(
1134 "next_feed" => __("Open next feed"),
1135 "prev_feed" => __("Open previous feed"),
1136 "next_article" => __("Open next article"),
1137 "prev_article" => __("Open previous article"),
1138 "next_article_noscroll" => __("Open next article (don't scroll long articles)"),
1139 "prev_article_noscroll" => __("Open previous article (don't scroll long articles)"),
1140 "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1141 "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1142 "search_dialog" => __("Show search dialog")),
1143 __("Article") => array(
1144 "toggle_mark" => __("Toggle starred"),
1145 "toggle_publ" => __("Toggle published"),
1146 "toggle_unread" => __("Toggle unread"),
1147 "edit_tags" => __("Edit tags"),
1148 "open_in_new_window" => __("Open in new window"),
1149 "catchup_below" => __("Mark below as read"),
1150 "catchup_above" => __("Mark above as read"),
1151 "article_scroll_down" => __("Scroll down"),
1152 "article_scroll_up" => __("Scroll up"),
1153 "select_article_cursor" => __("Select article under cursor"),
1154 "email_article" => __("Email article"),
1155 "close_article" => __("Close/collapse article"),
1156 "toggle_expand" => __("Toggle article expansion (combined mode)"),
1157 "toggle_widescreen" => __("Toggle widescreen mode"),
1158 "toggle_embed_original" => __("Toggle embed original")),
1159 __("Article selection") => array(
1160 "select_all" => __("Select all articles"),
1161 "select_unread" => __("Select unread"),
1162 "select_marked" => __("Select starred"),
1163 "select_published" => __("Select published"),
1164 "select_invert" => __("Invert selection"),
1165 "select_none" => __("Deselect everything")),
1166 __("Feed") => array(
1167 "feed_refresh" => __("Refresh current feed"),
1168 "feed_unhide_read" => __("Un/hide read feeds"),
1169 "feed_subscribe" => __("Subscribe to feed"),
1170 "feed_edit" => __("Edit feed"),
1171 "feed_catchup" => __("Mark as read"),
1172 "feed_reverse" => __("Reverse headlines"),
1173 "feed_toggle_vgroup" => __("Toggle headline grouping"),
1174 "feed_debug_update" => __("Debug feed update"),
1175 "feed_debug_viewfeed" => __("Debug viewfeed()"),
1176 "catchup_all" => __("Mark all feeds as read"),
1177 "cat_toggle_collapse" => __("Un/collapse current category"),
1178 "toggle_combined_mode" => __("Toggle combined mode"),
1179 "toggle_cdm_expanded" => __("Toggle auto expand in combined mode")),
1180 __("Go to") => array(
1181 "goto_all" => __("All articles"),
1182 "goto_fresh" => __("Fresh"),
1183 "goto_marked" => __("Starred"),
1184 "goto_published" => __("Published"),
1185 "goto_tagcloud" => __("Tag cloud"),
1186 "goto_prefs" => __("Preferences")),
1187 __("Other") => array(
1188 "create_label" => __("Create label"),
1189 "create_filter" => __("Create filter"),
1190 "collapse_sidebar" => __("Un/collapse sidebar"),
1191 "help_dialog" => __("Show help dialog"))
1192 );
1193
1194 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1195 $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1196 }
1197
1198 return $hotkeys;
1199 }
1200
1201 function get_hotkeys_map() {
1202 $hotkeys = array(
1203 // "navigation" => array(
1204 "k" => "next_feed",
1205 "j" => "prev_feed",
1206 "n" => "next_article",
1207 "p" => "prev_article",
1208 "(38)|up" => "prev_article",
1209 "(40)|down" => "next_article",
1210 // "^(38)|Ctrl-up" => "prev_article_noscroll",
1211 // "^(40)|Ctrl-down" => "next_article_noscroll",
1212 "(191)|/" => "search_dialog",
1213 // "article" => array(
1214 "s" => "toggle_mark",
1215 "*s" => "toggle_publ",
1216 "u" => "toggle_unread",
1217 "*t" => "edit_tags",
1218 "o" => "open_in_new_window",
1219 "c p" => "catchup_below",
1220 "c n" => "catchup_above",
1221 "*n" => "article_scroll_down",
1222 "*p" => "article_scroll_up",
1223 "*(38)|Shift+up" => "article_scroll_up",
1224 "*(40)|Shift+down" => "article_scroll_down",
1225 "a *w" => "toggle_widescreen",
1226 "a e" => "toggle_embed_original",
1227 "e" => "email_article",
1228 "a q" => "close_article",
1229 // "article_selection" => array(
1230 "a a" => "select_all",
1231 "a u" => "select_unread",
1232 "a *u" => "select_marked",
1233 "a p" => "select_published",
1234 "a i" => "select_invert",
1235 "a n" => "select_none",
1236 // "feed" => array(
1237 "f r" => "feed_refresh",
1238 "f a" => "feed_unhide_read",
1239 "f s" => "feed_subscribe",
1240 "f e" => "feed_edit",
1241 "f q" => "feed_catchup",
1242 "f x" => "feed_reverse",
1243 "f g" => "feed_toggle_vgroup",
1244 "f *d" => "feed_debug_update",
1245 "f *g" => "feed_debug_viewfeed",
1246 "f *c" => "toggle_combined_mode",
1247 "f c" => "toggle_cdm_expanded",
1248 "*q" => "catchup_all",
1249 "x" => "cat_toggle_collapse",
1250 // "goto" => array(
1251 "g a" => "goto_all",
1252 "g f" => "goto_fresh",
1253 "g s" => "goto_marked",
1254 "g p" => "goto_published",
1255 "g t" => "goto_tagcloud",
1256 "g *p" => "goto_prefs",
1257 // "other" => array(
1258 "(9)|Tab" => "select_article_cursor", // tab
1259 "c l" => "create_label",
1260 "c f" => "create_filter",
1261 "c s" => "collapse_sidebar",
1262 "^(191)|Ctrl+/" => "help_dialog",
1263 );
1264
1265 if (get_pref('COMBINED_DISPLAY_MODE')) {
1266 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1267 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
1268 }
1269
1270 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1271 $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1272 }
1273
1274 $prefixes = array();
1275
1276 foreach (array_keys($hotkeys) as $hotkey) {
1277 $pair = explode(" ", $hotkey, 2);
1278
1279 if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1280 array_push($prefixes, $pair[0]);
1281 }
1282 }
1283
1284 return array($prefixes, $hotkeys);
1285 }
1286
1287 function check_for_update() {
1288 if (defined("GIT_VERSION_TIMESTAMP")) {
1289 $content = @fetch_file_contents(array("url" => "http://tt-rss.org/version.json", "timeout" => 5));
1290
1291 if ($content) {
1292 $content = json_decode($content, true);
1293
1294 if ($content && isset($content["changeset"])) {
1295 if ((int)GIT_VERSION_TIMESTAMP < (int)$content["changeset"]["timestamp"] &&
1296 GIT_VERSION_HEAD != $content["changeset"]["id"]) {
1297
1298 return $content["changeset"]["id"];
1299 }
1300 }
1301 }
1302 }
1303
1304 return "";
1305 }
1306
1307 function make_runtime_info($disable_update_check = false) {
1308 $data = array();
1309
8adb3ec4
AD
1310 $pdo = Db::pdo();
1311
1312 $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1313 ttrss_feeds WHERE owner_uid = ?");
1314 $sth->execute([$_SESSION['uid']]);
1315 $row = $sth->fetch();
aeb1abed 1316
8adb3ec4
AD
1317 $max_feed_id = $row['mid'];
1318 $num_feeds = $row['nf'];
aeb1abed
AD
1319
1320 $data["max_feed_id"] = (int) $max_feed_id;
1321 $data["num_feeds"] = (int) $num_feeds;
1322
1323 $data['last_article_id'] = Article::getLastArticleId();
1324 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1325
1326 $data['dep_ts'] = calculate_dep_timestamp();
1327 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1328
5e78b0c2 1329 $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
aeb1abed
AD
1330
1331 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1332 $update_result = @check_for_update();
1333
1334 $data["update_result"] = $update_result;
1335
1336 $_SESSION["last_version_check"] = time();
1337 }
1338
1339 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1340
1341 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1342
1343 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1344
1345 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1346
1347 if ($stamp) {
1348 $stamp_delta = time() - $stamp;
1349
1350 if ($stamp_delta > 1800) {
1351 $stamp_check = 0;
1352 } else {
1353 $stamp_check = 1;
1354 $_SESSION["daemon_stamp_check"] = time();
1355 }
1356
1357 $data['daemon_stamp_ok'] = $stamp_check;
1358
1359 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1360
1361 $data['daemon_stamp'] = $stamp_fmt;
1362 }
1363 }
1364 }
1365
1366 return $data;
1367 }
1368
1369 function search_to_sql($search, $search_language) {
1370
1371 $keywords = str_getcsv(trim($search), " ");
1372 $query_keywords = array();
1373 $search_words = array();
1374 $search_query_leftover = array();
1375
1376 if ($search_language)
1377 $search_language = db_escape_string(mb_strtolower($search_language));
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 '%".
1395 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
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 '%".
1405 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
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 '%".
1420 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
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) {
1492 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
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
8adb3ec4 1954 ////db_query("BEGIN");
aeb1abed
AD
1955
1956 if ($parent_cat_id) {
1957 $parent_qpart = "parent_cat = '$parent_cat_id'";
1958 $parent_insert = "'$parent_cat_id'";
1959 } else {
1960 $parent_qpart = "parent_cat IS NULL";
1961 $parent_insert = "NULL";
1962 }
1963
1964 $feed_cat = mb_substr($feed_cat, 0, 250);
1965
8adb3ec4
AD
1966 $pdo = Db::pdo();
1967
1968 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1969 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ?");
aeb1abed
AD
1970
1971 if (db_num_rows($result) == 0) {
1972
1973 $result = db_query(
1974 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1975 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
1976
1977 db_query("COMMIT");
1978
1979 return true;
1980 }
1981
1982 return false;
1983 }
1984
1985 /**
1986 * Fixes incomplete URLs by prepending "http://".
1987 * Also replaces feed:// with http://, and
1988 * prepends a trailing slash if the url is a domain name only.
1989 *
1990 * @param string $url Possibly incomplete URL
1991 *
1992 * @return string Fixed URL.
1993 */
1994 function fix_url($url) {
1995
1996 // support schema-less urls
1997 if (strpos($url, '//') === 0) {
1998 $url = 'https:' . $url;
1999 }
2000
2001 if (strpos($url, '://') === false) {
2002 $url = 'http://' . $url;
2003 } else if (substr($url, 0, 5) == 'feed:') {
2004 $url = 'http:' . substr($url, 5);
2005 }
2006
2007 //prepend slash if the URL has no slash in it
2008 // "http://www.example" -> "http://www.example/"
2009 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2010 $url .= '/';
2011 }
2012
2013 //convert IDNA hostname to punycode if possible
2014 if (function_exists("idn_to_ascii")) {
2015 $parts = parse_url($url);
2016 if (mb_detect_encoding($parts['host']) != 'ASCII')
2017 {
2018 $parts['host'] = idn_to_ascii($parts['host']);
2019 $url = build_url($parts);
2020 }
2021 }
2022
2023 if ($url != "http:///")
2024 return $url;
2025 else
2026 return '';
2027 }
2028
2029 function validate_feed_url($url) {
2030 $parts = parse_url($url);
2031
2032 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2033
2034 }
2035
2036 /* function save_email_address($email) {
2037 // FIXME: implement persistent storage of emails
2038
2039 if (!$_SESSION['stored_emails'])
2040 $_SESSION['stored_emails'] = array();
2041
2042 if (!in_array($email, $_SESSION['stored_emails']))
2043 array_push($_SESSION['stored_emails'], $email);
2044 } */
2045
2046
2047 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2048
2049 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2050
2051 $sql_is_cat = bool_to_sql_bool($is_cat);
2052
2053 $result = db_query("SELECT access_key FROM ttrss_access_keys
2054 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
2055 AND owner_uid = " . $owner_uid);
2056
2057 if (db_num_rows($result) == 1) {
2058 return db_fetch_result($result, 0, "access_key");
2059 } else {
2060 $key = db_escape_string(uniqid_short());
2061
2062 $result = db_query("INSERT INTO ttrss_access_keys
2063 (access_key, feed_id, is_cat, owner_uid)
2064 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
2065
2066 return $key;
2067 }
2068 return false;
2069 }
2070
2071 function get_feeds_from_html($url, $content)
2072 {
2073 $url = fix_url($url);
2074 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2075
2076 libxml_use_internal_errors(true);
2077
2078 $doc = new DOMDocument();
2079 $doc->loadHTML($content);
2080 $xpath = new DOMXPath($doc);
2081 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2082 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2083 $feedUrls = array();
2084 foreach ($entries as $entry) {
2085 if ($entry->hasAttribute('href')) {
2086 $title = $entry->getAttribute('title');
2087 if ($title == '') {
2088 $title = $entry->getAttribute('type');
2089 }
2090 $feedUrl = rewrite_relative_url(
2091 $baseUrl, $entry->getAttribute('href')
2092 );
2093 $feedUrls[$feedUrl] = $title;
2094 }
2095 }
2096 return $feedUrls;
2097 }
2098
2099 function is_html($content) {
2100 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2101 }
2102
2103 function url_is_html($url, $login = false, $pass = false) {
2104 return is_html(fetch_file_contents($url, false, $login, $pass));
2105 }
2106
2107 function build_url($parts) {
2108 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2109 }
2110
2111 function cleanup_url_path($path) {
2112 $path = str_replace("/./", "/", $path);
2113 $path = str_replace("//", "/", $path);
2114
2115 return $path;
2116 }
2117
2118 /**
2119 * Converts a (possibly) relative URL to a absolute one.
2120 *
2121 * @param string $url Base URL (i.e. from where the document is)
2122 * @param string $rel_url Possibly relative URL in the document
2123 *
2124 * @return string Absolute URL
2125 */
2126 function rewrite_relative_url($url, $rel_url) {
2127 if (strpos($rel_url, "://") !== false) {
2128 return $rel_url;
2129 } else if (strpos($rel_url, "//") === 0) {
2130 # protocol-relative URL (rare but they exist)
2131 return $rel_url;
2132 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2133 # magnet:, feed:, etc
2134 return $rel_url;
2135 } else if (strpos($rel_url, "/") === 0) {
2136 $parts = parse_url($url);
2137 $parts['path'] = $rel_url;
2138 $parts['path'] = cleanup_url_path($parts['path']);
2139
2140 return build_url($parts);
2141
2142 } else {
2143 $parts = parse_url($url);
2144 if (!isset($parts['path'])) {
2145 $parts['path'] = '/';
2146 }
2147 $dir = $parts['path'];
2148 if (substr($dir, -1) !== '/') {
2149 $dir = dirname($parts['path']);
2150 $dir !== '/' && $dir .= '/';
2151 }
2152 $parts['path'] = $dir . $rel_url;
2153 $parts['path'] = cleanup_url_path($parts['path']);
2154
2155 return build_url($parts);
2156 }
2157 }
2158
2159 function cleanup_tags($days = 14, $limit = 1000) {
2160
2161 if (DB_TYPE == "pgsql") {
2162 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2163 } else if (DB_TYPE == "mysql") {
2164 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2165 }
2166
2167 $tags_deleted = 0;
2168
2169 while ($limit > 0) {
2170 $limit_part = 500;
2171
2172 $query = "SELECT ttrss_tags.id AS id
2173 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2174 WHERE post_int_id = int_id AND $interval_query AND
2175 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2176
2177 $result = db_query($query);
2178
2179 $ids = array();
2180
2181 while ($line = db_fetch_assoc($result)) {
2182 array_push($ids, $line['id']);
2183 }
2184
2185 if (count($ids) > 0) {
2186 $ids = join(",", $ids);
2187
2188 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2189 $tags_deleted += db_affected_rows($tmp_result);
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
AD
2530 function check_mysql_tables() {
2531 $schema = db_escape_string(DB_NAME);
2532
2533 $result = db_query("SELECT engine, table_name FROM information_schema.tables WHERE
2534 table_schema = '$schema' AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2535
2536 $bad_tables = [];
2537
2538 while ($line = db_fetch_assoc($result)) {
2539 array_push($bad_tables, $line);
2540 }
2541
2542 return $bad_tables;
2543 }
2544