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