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