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