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