]> git.wh0rd.org - tt-rss.git/blob - include/functions.php
rpc: switch to PDO
[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*6);
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 if ($s == "t" || $s == "1" || strtolower($s) == "true") {
945 return true;
946 } else {
947 return false;
948 }
949 }
950
951 function bool_to_sql_bool($s) {
952 if ($s) {
953 return "true";
954 } else {
955 return "false";
956 }
957 }
958
959 // Session caching removed due to causing wrong redirects to upgrade
960 // script when get_schema_version() is called on an obsolete session
961 // created on a previous schema version.
962 function get_schema_version($nocache = false) {
963 global $schema_version;
964
965 $pdo = DB::pdo();
966
967 if (!$schema_version && !$nocache) {
968 $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
969 $version = $row["schema_version"];
970 $schema_version = $version;
971 return $version;
972 } else {
973 return $schema_version;
974 }
975 }
976
977 function sanity_check() {
978 require_once 'errors.php';
979 global $ERRORS;
980
981 $error_code = 0;
982 $schema_version = get_schema_version(true);
983
984 if ($schema_version != SCHEMA_VERSION) {
985 $error_code = 5;
986 }
987
988 if (db_escape_string("testTEST") != "testTEST") {
989 $error_code = 12;
990 }
991
992 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
993 }
994
995 function file_is_locked($filename) {
996 if (file_exists(LOCK_DIRECTORY . "/$filename")) {
997 if (function_exists('flock')) {
998 $fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
999 if ($fp) {
1000 if (flock($fp, LOCK_EX | LOCK_NB)) {
1001 flock($fp, LOCK_UN);
1002 fclose($fp);
1003 return false;
1004 }
1005 fclose($fp);
1006 return true;
1007 } else {
1008 return false;
1009 }
1010 }
1011 return true; // consider the file always locked and skip the test
1012 } else {
1013 return false;
1014 }
1015 }
1016
1017
1018 function make_lockfile($filename) {
1019 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
1020
1021 if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
1022 $stat_h = fstat($fp);
1023 $stat_f = stat(LOCK_DIRECTORY . "/$filename");
1024
1025 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
1026 if ($stat_h["ino"] != $stat_f["ino"] ||
1027 $stat_h["dev"] != $stat_f["dev"]) {
1028
1029 return false;
1030 }
1031 }
1032
1033 if (function_exists('posix_getpid')) {
1034 fwrite($fp, posix_getpid() . "\n");
1035 }
1036 return $fp;
1037 } else {
1038 return false;
1039 }
1040 }
1041
1042 function make_stampfile($filename) {
1043 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
1044
1045 if (flock($fp, LOCK_EX | LOCK_NB)) {
1046 fwrite($fp, time() . "\n");
1047 flock($fp, LOCK_UN);
1048 fclose($fp);
1049 return true;
1050 } else {
1051 return false;
1052 }
1053 }
1054
1055 function sql_random_function() {
1056 if (DB_TYPE == "mysql") {
1057 return "RAND()";
1058 } else {
1059 return "RANDOM()";
1060 }
1061 }
1062
1063 function getFeedUnread($feed, $is_cat = false) {
1064 return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1065 }
1066
1067 function checkbox_to_sql_bool($val) {
1068 return ($val == "on") ? "true" : "false";
1069 }
1070
1071 function uniqid_short() {
1072 return uniqid(base_convert(rand(), 10, 36));
1073 }
1074
1075 function make_init_params() {
1076 $params = array();
1077
1078 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1079 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1080 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1081 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1082
1083 $params[strtolower($param)] = (int) get_pref($param);
1084 }
1085
1086 $params["icons_url"] = ICONS_URL;
1087 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
1088 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1089 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1090 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1091 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
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 = "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('%$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('%$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('%$k%')
1428 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$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('%$k%')
1441 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$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('%$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('%$k%')
1467 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$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('%$k%')
1488 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$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
1589 if ($entry->hasAttribute('src')) {
1590 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1591
1592 if (is_prefix_https() && !$is_https_url) {
1593
1594 if ($entry->hasAttribute('srcset')) {
1595 $entry->removeAttribute('srcset');
1596 }
1597
1598 if ($entry->hasAttribute('sizes')) {
1599 $entry->removeAttribute('sizes');
1600 }
1601 }
1602 }
1603
1604 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1605 $force_remove_images || $_SESSION["bw_limit"]) {
1606
1607 $p = $doc->createElement('p');
1608
1609 $a = $doc->createElement('a');
1610 $a->setAttribute('href', $entry->getAttribute('src'));
1611
1612 $a->appendChild(new DOMText($entry->getAttribute('src')));
1613 $a->setAttribute('target', '_blank');
1614 $a->setAttribute('rel', 'noopener noreferrer');
1615
1616 $p->appendChild($a);
1617
1618 $entry->parentNode->replaceChild($p, $entry);
1619 }
1620 }
1621
1622 if (strtolower($entry->nodeName) == "a") {
1623 $entry->setAttribute("target", "_blank");
1624 $entry->setAttribute("rel", "noopener noreferrer");
1625 }
1626 }
1627
1628 $entries = $xpath->query('//iframe');
1629 foreach ($entries as $entry) {
1630 if (!iframe_whitelisted($entry)) {
1631 $entry->setAttribute('sandbox', 'allow-scripts');
1632 } else {
1633 if (is_prefix_https()) {
1634 $entry->setAttribute("src",
1635 str_replace("http://", "https://",
1636 $entry->getAttribute("src")));
1637 }
1638 }
1639 }
1640
1641 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1642 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1643 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1644 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1645 'dt', 'em', 'footer', 'figure', 'figcaption',
1646 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1647 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1648 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1649 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1650 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1651 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1652
1653 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1654
1655 $disallowed_attributes = array('id', 'style', 'class');
1656
1657 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1658 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1659 if (is_array($retval)) {
1660 $doc = $retval[0];
1661 $allowed_elements = $retval[1];
1662 $disallowed_attributes = $retval[2];
1663 } else {
1664 $doc = $retval;
1665 }
1666 }
1667
1668 $doc->removeChild($doc->firstChild); //remove doctype
1669 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1670
1671 if ($highlight_words) {
1672 foreach ($highlight_words as $word) {
1673
1674 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1675
1676 $elements = $xpath->query("//*/text()");
1677
1678 foreach ($elements as $child) {
1679
1680 $fragment = $doc->createDocumentFragment();
1681 $text = $child->textContent;
1682
1683 while (($pos = mb_stripos($text, $word)) !== false) {
1684 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1685 $word = mb_substr($text, $pos, mb_strlen($word));
1686 $highlight = $doc->createElement('span');
1687 $highlight->appendChild(new DomText($word));
1688 $highlight->setAttribute('class', 'highlight');
1689 $fragment->appendChild($highlight);
1690 $text = mb_substr($text, $pos + mb_strlen($word));
1691 }
1692
1693 if (!empty($text)) $fragment->appendChild(new DomText($text));
1694
1695 $child->parentNode->replaceChild($fragment, $child);
1696 }
1697 }
1698 }
1699
1700 $res = $doc->saveHTML();
1701
1702 /* strip everything outside of <body>...</body> */
1703
1704 $res_frag = array();
1705 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1706 return $res_frag[1];
1707 } else {
1708 return $res;
1709 }
1710 }
1711
1712 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1713 $xpath = new DOMXPath($doc);
1714 $entries = $xpath->query('//*');
1715
1716 foreach ($entries as $entry) {
1717 if (!in_array($entry->nodeName, $allowed_elements)) {
1718 $entry->parentNode->removeChild($entry);
1719 }
1720
1721 if ($entry->hasAttributes()) {
1722 $attrs_to_remove = array();
1723
1724 foreach ($entry->attributes as $attr) {
1725
1726 if (strpos($attr->nodeName, 'on') === 0) {
1727 array_push($attrs_to_remove, $attr);
1728 }
1729
1730 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1731 array_push($attrs_to_remove, $attr);
1732 }
1733
1734 if (in_array($attr->nodeName, $disallowed_attributes)) {
1735 array_push($attrs_to_remove, $attr);
1736 }
1737 }
1738
1739 foreach ($attrs_to_remove as $attr) {
1740 $entry->removeAttributeNode($attr);
1741 }
1742 }
1743 }
1744
1745 return $doc;
1746 }
1747
1748 function trim_array($array) {
1749 $tmp = $array;
1750 array_walk($tmp, 'trim');
1751 return $tmp;
1752 }
1753
1754 function tag_is_valid($tag) {
1755 if ($tag == '') return false;
1756 if (is_numeric($tag)) return false;
1757 if (mb_strlen($tag) > 250) return false;
1758
1759 if (!$tag) return false;
1760
1761 return true;
1762 }
1763
1764 function render_login_form() {
1765 header('Cache-Control: public');
1766
1767 require_once "login_form.php";
1768 exit;
1769 }
1770
1771 function T_sprintf() {
1772 $args = func_get_args();
1773 return vsprintf(__(array_shift($args)), $args);
1774 }
1775
1776 function print_checkpoint($n, $s) {
1777 $ts = microtime(true);
1778 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1779 return $ts;
1780 }
1781
1782 function sanitize_tag($tag) {
1783 $tag = trim($tag);
1784
1785 $tag = mb_strtolower($tag, 'utf-8');
1786
1787 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1788
1789 if (DB_TYPE == "mysql") {
1790 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1791 }
1792
1793 return $tag;
1794 }
1795
1796 function is_server_https() {
1797 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1798 }
1799
1800 function is_prefix_https() {
1801 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1802 }
1803
1804 // this returns SELF_URL_PATH sans ending slash
1805 function get_self_url_prefix() {
1806 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1807 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1808 } else {
1809 return SELF_URL_PATH;
1810 }
1811 }
1812
1813 function encrypt_password($pass, $salt = '', $mode2 = false) {
1814 if ($salt && $mode2) {
1815 return "MODE2:" . hash('sha256', $salt . $pass);
1816 } else if ($salt) {
1817 return "SHA1X:" . sha1("$salt:$pass");
1818 } else {
1819 return "SHA1:" . sha1($pass);
1820 }
1821 } // function encrypt_password
1822
1823 function load_filters($feed_id, $owner_uid) {
1824 $filters = array();
1825
1826 $cat_id = (int)Feeds::getFeedCategory($feed_id);
1827
1828 if ($cat_id == 0)
1829 $null_cat_qpart = "cat_id IS NULL OR";
1830 else
1831 $null_cat_qpart = "";
1832
1833 $pdo = Db::pdo();
1834
1835 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1836 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1837 $sth->execute([$owner_uid]);
1838
1839 $check_cats = array_merge(
1840 Feeds::getParentCategories($cat_id, $owner_uid),
1841 [$cat_id]);
1842
1843 $check_cats_str = join(",", $check_cats);
1844 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1845
1846 while ($line = $sth->fetch()) {
1847 $filter_id = $line["id"];
1848
1849 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1850
1851 $sth2 = $pdo->prepare("SELECT
1852 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1853 FROM ttrss_filters2_rules AS r,
1854 ttrss_filter_types AS t
1855 WHERE
1856 (match_on IS NOT NULL OR
1857 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1858 (feed_id IS NULL OR feed_id = ?))) AND
1859 filter_type = t.id AND filter_id = ?");
1860 $sth2->execute([$feed_id, $filter_id]);
1861
1862 $rules = array();
1863 $actions = array();
1864
1865 while ($rule_line = $sth2->fetch()) {
1866 # print_r($rule_line);
1867
1868 if ($rule_line["match_on"]) {
1869 $match_on = json_decode($rule_line["match_on"], true);
1870
1871 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1872
1873 $rule = array();
1874 $rule["reg_exp"] = $rule_line["reg_exp"];
1875 $rule["type"] = $rule_line["type_name"];
1876 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1877
1878 array_push($rules, $rule);
1879 } else if (!$match_any_rule) {
1880 // this filter contains a rule that doesn't match to this feed/category combination
1881 // thus filter has to be rejected
1882
1883 $rules = [];
1884 break;
1885 }
1886
1887 } else {
1888
1889 $rule = array();
1890 $rule["reg_exp"] = $rule_line["reg_exp"];
1891 $rule["type"] = $rule_line["type_name"];
1892 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1893
1894 array_push($rules, $rule);
1895 }
1896 }
1897
1898 if (count($rules) > 0) {
1899 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1900 FROM ttrss_filters2_actions AS a,
1901 ttrss_filter_actions AS t
1902 WHERE
1903 action_id = t.id AND filter_id = ?");
1904 $sth2->execute([$filter_id]);
1905
1906 while ($action_line = $sth2->fetch()) {
1907 # print_r($action_line);
1908
1909 $action = array();
1910 $action["type"] = $action_line["type_name"];
1911 $action["param"] = $action_line["action_param"];
1912
1913 array_push($actions, $action);
1914 }
1915 }
1916
1917 $filter = array();
1918 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1919 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1920 $filter["rules"] = $rules;
1921 $filter["actions"] = $actions;
1922
1923 if (count($rules) > 0 && count($actions) > 0) {
1924 array_push($filters, $filter);
1925 }
1926 }
1927
1928 return $filters;
1929 }
1930
1931 function get_score_pic($score) {
1932 if ($score > 100) {
1933 return "score_high.png";
1934 } else if ($score > 0) {
1935 return "score_half_high.png";
1936 } else if ($score < -100) {
1937 return "score_low.png";
1938 } else if ($score < 0) {
1939 return "score_half_low.png";
1940 } else {
1941 return "score_neutral.png";
1942 }
1943 }
1944
1945 function feed_has_icon($id) {
1946 return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0;
1947 }
1948
1949 function init_plugins() {
1950 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1951
1952 return true;
1953 }
1954
1955 function add_feed_category($feed_cat, $parent_cat_id = false) {
1956
1957 if (!$feed_cat) return false;
1958
1959 $feed_cat = mb_substr($feed_cat, 0, 250);
1960 if (!$parent_cat_id) $parent_cat_id = null;
1961
1962 $pdo = Db::pdo();
1963 $pdo->beginTransaction();
1964
1965 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1966 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
1967 AND title = :title AND owner_uid = :uid");
1968 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
1969
1970 if (!$sth->fetch()) {
1971
1972 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1973 VALUES (?, ?, ?)");
1974 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
1975
1976 $pdo->commit();
1977
1978 return true;
1979 }
1980
1981 $pdo->commit();
1982
1983 return false;
1984 }
1985
1986 /**
1987 * Fixes incomplete URLs by prepending "http://".
1988 * Also replaces feed:// with http://, and
1989 * prepends a trailing slash if the url is a domain name only.
1990 *
1991 * @param string $url Possibly incomplete URL
1992 *
1993 * @return string Fixed URL.
1994 */
1995 function fix_url($url) {
1996
1997 // support schema-less urls
1998 if (strpos($url, '//') === 0) {
1999 $url = 'https:' . $url;
2000 }
2001
2002 if (strpos($url, '://') === false) {
2003 $url = 'http://' . $url;
2004 } else if (substr($url, 0, 5) == 'feed:') {
2005 $url = 'http:' . substr($url, 5);
2006 }
2007
2008 //prepend slash if the URL has no slash in it
2009 // "http://www.example" -> "http://www.example/"
2010 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2011 $url .= '/';
2012 }
2013
2014 //convert IDNA hostname to punycode if possible
2015 if (function_exists("idn_to_ascii")) {
2016 $parts = parse_url($url);
2017 if (mb_detect_encoding($parts['host']) != 'ASCII')
2018 {
2019 $parts['host'] = idn_to_ascii($parts['host']);
2020 $url = build_url($parts);
2021 }
2022 }
2023
2024 if ($url != "http:///")
2025 return $url;
2026 else
2027 return '';
2028 }
2029
2030 function validate_feed_url($url) {
2031 $parts = parse_url($url);
2032
2033 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2034
2035 }
2036
2037 /* function save_email_address($email) {
2038 // FIXME: implement persistent storage of emails
2039
2040 if (!$_SESSION['stored_emails'])
2041 $_SESSION['stored_emails'] = array();
2042
2043 if (!in_array($email, $_SESSION['stored_emails']))
2044 array_push($_SESSION['stored_emails'], $email);
2045 } */
2046
2047
2048 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2049
2050 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2051
2052 $is_cat = bool_to_sql_bool($is_cat);
2053
2054 $pdo = Db::pdo();
2055
2056 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2057 WHERE feed_id = ? AND is_cat = ?
2058 AND owner_uid = ?");
2059 $sth->execute([$feed_id, $is_cat, $owner_uid]);
2060
2061 if ($row = $sth->fetch()) {
2062 return $row["access_key"];
2063 } else {
2064 $key = uniqid_short();
2065
2066 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2067 (access_key, feed_id, is_cat, owner_uid)
2068 VALUES (?, ?, ?, ?)");
2069
2070 $sth->execute([$key, $feed_id, $is_cat, $owner_uid]);
2071
2072 return $key;
2073 }
2074 }
2075
2076 function get_feeds_from_html($url, $content)
2077 {
2078 $url = fix_url($url);
2079 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2080
2081 libxml_use_internal_errors(true);
2082
2083 $doc = new DOMDocument();
2084 $doc->loadHTML($content);
2085 $xpath = new DOMXPath($doc);
2086 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2087 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2088 $feedUrls = array();
2089 foreach ($entries as $entry) {
2090 if ($entry->hasAttribute('href')) {
2091 $title = $entry->getAttribute('title');
2092 if ($title == '') {
2093 $title = $entry->getAttribute('type');
2094 }
2095 $feedUrl = rewrite_relative_url(
2096 $baseUrl, $entry->getAttribute('href')
2097 );
2098 $feedUrls[$feedUrl] = $title;
2099 }
2100 }
2101 return $feedUrls;
2102 }
2103
2104 function is_html($content) {
2105 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2106 }
2107
2108 function url_is_html($url, $login = false, $pass = false) {
2109 return is_html(fetch_file_contents($url, false, $login, $pass));
2110 }
2111
2112 function build_url($parts) {
2113 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2114 }
2115
2116 function cleanup_url_path($path) {
2117 $path = str_replace("/./", "/", $path);
2118 $path = str_replace("//", "/", $path);
2119
2120 return $path;
2121 }
2122
2123 /**
2124 * Converts a (possibly) relative URL to a absolute one.
2125 *
2126 * @param string $url Base URL (i.e. from where the document is)
2127 * @param string $rel_url Possibly relative URL in the document
2128 *
2129 * @return string Absolute URL
2130 */
2131 function rewrite_relative_url($url, $rel_url) {
2132 if (strpos($rel_url, "://") !== false) {
2133 return $rel_url;
2134 } else if (strpos($rel_url, "//") === 0) {
2135 # protocol-relative URL (rare but they exist)
2136 return $rel_url;
2137 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2138 # magnet:, feed:, etc
2139 return $rel_url;
2140 } else if (strpos($rel_url, "/") === 0) {
2141 $parts = parse_url($url);
2142 $parts['path'] = $rel_url;
2143 $parts['path'] = cleanup_url_path($parts['path']);
2144
2145 return build_url($parts);
2146
2147 } else {
2148 $parts = parse_url($url);
2149 if (!isset($parts['path'])) {
2150 $parts['path'] = '/';
2151 }
2152 $dir = $parts['path'];
2153 if (substr($dir, -1) !== '/') {
2154 $dir = dirname($parts['path']);
2155 $dir !== '/' && $dir .= '/';
2156 }
2157 $parts['path'] = $dir . $rel_url;
2158 $parts['path'] = cleanup_url_path($parts['path']);
2159
2160 return build_url($parts);
2161 }
2162 }
2163
2164 function cleanup_tags($days = 14, $limit = 1000) {
2165
2166 $days = (int) $days;
2167
2168 if (DB_TYPE == "pgsql") {
2169 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2170 } else if (DB_TYPE == "mysql") {
2171 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2172 }
2173
2174 $tags_deleted = 0;
2175
2176 $pdo = Db::pdo();
2177
2178 while ($limit > 0) {
2179 $limit_part = 500;
2180
2181 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2182 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2183 WHERE post_int_id = int_id AND $interval_query AND
2184 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2185 $sth->execute([$limit]);
2186
2187 $ids = array();
2188
2189 while ($line = $sth->fetch()) {
2190 array_push($ids, $line['id']);
2191 }
2192
2193 if (count($ids) > 0) {
2194 $ids = join(",", $ids);
2195
2196 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2197 $tags_deleted = $usth->rowCount();
2198 } else {
2199 break;
2200 }
2201
2202 $limit -= $limit_part;
2203 }
2204
2205 return $tags_deleted;
2206 }
2207
2208 function print_user_stylesheet() {
2209 $value = get_pref('USER_STYLESHEET');
2210
2211 if ($value) {
2212 print "<style type=\"text/css\">";
2213 print str_replace("<br/>", "\n", $value);
2214 print "</style>";
2215 }
2216
2217 }
2218
2219 function filter_to_sql($filter, $owner_uid) {
2220 $query = array();
2221
2222 if (DB_TYPE == "pgsql")
2223 $reg_qpart = "~";
2224 else
2225 $reg_qpart = "REGEXP";
2226
2227 foreach ($filter["rules"] AS $rule) {
2228 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2229 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2230 $rule['reg_exp']) !== FALSE;
2231
2232 if ($regexp_valid) {
2233
2234 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2235
2236 switch ($rule["type"]) {
2237 case "title":
2238 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2239 $rule['reg_exp'] . "')";
2240 break;
2241 case "content":
2242 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2243 $rule['reg_exp'] . "')";
2244 break;
2245 case "both":
2246 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2247 $rule['reg_exp'] . "') OR LOWER(" .
2248 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2249 break;
2250 case "tag":
2251 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2252 $rule['reg_exp'] . "')";
2253 break;
2254 case "link":
2255 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2256 $rule['reg_exp'] . "')";
2257 break;
2258 case "author":
2259 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2260 $rule['reg_exp'] . "')";
2261 break;
2262 }
2263
2264 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2265
2266 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2267 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2268 }
2269
2270 if (isset($rule["cat_id"])) {
2271
2272 if ($rule["cat_id"] > 0) {
2273 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2274 array_push($children, $rule["cat_id"]);
2275
2276 $children = join(",", $children);
2277
2278 $cat_qpart = "cat_id IN ($children)";
2279 } else {
2280 $cat_qpart = "cat_id IS NULL";
2281 }
2282
2283 $qpart .= " AND $cat_qpart";
2284 }
2285
2286 $qpart .= " AND feed_id IS NOT NULL";
2287
2288 array_push($query, "($qpart)");
2289
2290 }
2291 }
2292
2293 if (count($query) > 0) {
2294 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2295 } else {
2296 $fullquery = "(false)";
2297 }
2298
2299 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2300
2301 return $fullquery;
2302 }
2303
2304 if (!function_exists('gzdecode')) {
2305 function gzdecode($string) { // no support for 2nd argument
2306 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2307 base64_encode($string));
2308 }
2309 }
2310
2311 function get_random_bytes($length) {
2312 if (function_exists('openssl_random_pseudo_bytes')) {
2313 return openssl_random_pseudo_bytes($length);
2314 } else {
2315 $output = "";
2316
2317 for ($i = 0; $i < $length; $i++)
2318 $output .= chr(mt_rand(0, 255));
2319
2320 return $output;
2321 }
2322 }
2323
2324 function read_stdin() {
2325 $fp = fopen("php://stdin", "r");
2326
2327 if ($fp) {
2328 $line = trim(fgets($fp));
2329 fclose($fp);
2330 return $line;
2331 }
2332
2333 return null;
2334 }
2335
2336 function implements_interface($class, $interface) {
2337 return in_array($interface, class_implements($class));
2338 }
2339
2340 function get_minified_js($files) {
2341 require_once 'lib/jshrink/Minifier.php';
2342
2343 $rv = '';
2344
2345 foreach ($files as $js) {
2346 if (!isset($_GET['debug'])) {
2347 $cached_file = CACHE_DIR . "/js/".basename($js).".js";
2348
2349 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2350
2351 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2352
2353 if ($header && $contents) {
2354 list($htag, $hversion) = explode(":", $header);
2355
2356 if ($htag == "tt-rss" && $hversion == VERSION) {
2357 $rv .= $contents;
2358 continue;
2359 }
2360 }
2361 }
2362
2363 $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js"));
2364 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2365 $rv .= $minified;
2366
2367 } else {
2368 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2369 }
2370 }
2371
2372 return $rv;
2373 }
2374
2375 function calculate_dep_timestamp() {
2376 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2377
2378 $max_ts = -1;
2379
2380 foreach ($files as $file) {
2381 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2382 }
2383
2384 return $max_ts;
2385 }
2386
2387 function T_js_decl($s1, $s2) {
2388 if ($s1 && $s2) {
2389 $s1 = preg_replace("/\n/", "", $s1);
2390 $s2 = preg_replace("/\n/", "", $s2);
2391
2392 $s1 = preg_replace("/\"/", "\\\"", $s1);
2393 $s2 = preg_replace("/\"/", "\\\"", $s2);
2394
2395 return "T_messages[\"$s1\"] = \"$s2\";\n";
2396 }
2397 }
2398
2399 function init_js_translations() {
2400
2401 print 'var T_messages = new Object();
2402
2403 function __(msg) {
2404 if (T_messages[msg]) {
2405 return T_messages[msg];
2406 } else {
2407 return msg;
2408 }
2409 }
2410
2411 function ngettext(msg1, msg2, n) {
2412 return __((parseInt(n) > 1) ? msg2 : msg1);
2413 }';
2414
2415 $l10n = _get_reader();
2416
2417 for ($i = 0; $i < $l10n->total; $i++) {
2418 $orig = $l10n->get_original_string($i);
2419 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2420 $key = explode(chr(0), $orig);
2421 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2422 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2423 } else {
2424 $translation = __($orig);
2425 print T_js_decl($orig, $translation);
2426 }
2427 }
2428 }
2429
2430 function get_theme_path($theme) {
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 arr_qmarks($arr) {
2555 return str_repeat('?,', count($arr) - 1) . '?';
2556 }