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