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