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