]> git.wh0rd.org - tt-rss.git/blob - include/functions.php
Merge branch 'master' of git.fakecake.org:tt-rss
[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 $entry->removeAttribute('width');
1600 $entry->removeAttribute('height');
1601
1602 if ($entry->hasAttribute('src')) {
1603 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1604
1605 if (is_prefix_https() && !$is_https_url) {
1606
1607 if ($entry->hasAttribute('srcset')) {
1608 $entry->removeAttribute('srcset');
1609 }
1610
1611 if ($entry->hasAttribute('sizes')) {
1612 $entry->removeAttribute('sizes');
1613 }
1614 }
1615 }
1616
1617 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1618 $force_remove_images || $_SESSION["bw_limit"]) {
1619
1620 $p = $doc->createElement('p');
1621
1622 $a = $doc->createElement('a');
1623 $a->setAttribute('href', $entry->getAttribute('src'));
1624
1625 $a->appendChild(new DOMText($entry->getAttribute('src')));
1626 $a->setAttribute('target', '_blank');
1627 $a->setAttribute('rel', 'noopener noreferrer');
1628
1629 $p->appendChild($a);
1630
1631 $entry->parentNode->replaceChild($p, $entry);
1632 }
1633 }
1634
1635 if (strtolower($entry->nodeName) == "a") {
1636 $entry->setAttribute("target", "_blank");
1637 $entry->setAttribute("rel", "noopener noreferrer");
1638 }
1639 }
1640
1641 $entries = $xpath->query('//iframe');
1642 foreach ($entries as $entry) {
1643 if (!iframe_whitelisted($entry)) {
1644 $entry->setAttribute('sandbox', 'allow-scripts');
1645 } else {
1646 if (is_prefix_https()) {
1647 $entry->setAttribute("src",
1648 str_replace("http://", "https://",
1649 $entry->getAttribute("src")));
1650 }
1651 }
1652 }
1653
1654 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1655 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1656 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1657 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1658 'dt', 'em', 'footer', 'figure', 'figcaption',
1659 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1660 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1661 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1662 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1663 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1664 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1665
1666 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1667
1668 $disallowed_attributes = array('id', 'style', 'class');
1669
1670 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1671 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1672 if (is_array($retval)) {
1673 $doc = $retval[0];
1674 $allowed_elements = $retval[1];
1675 $disallowed_attributes = $retval[2];
1676 } else {
1677 $doc = $retval;
1678 }
1679 }
1680
1681 $doc->removeChild($doc->firstChild); //remove doctype
1682 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1683
1684 if ($highlight_words) {
1685 foreach ($highlight_words as $word) {
1686
1687 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1688
1689 $elements = $xpath->query("//*/text()");
1690
1691 foreach ($elements as $child) {
1692
1693 $fragment = $doc->createDocumentFragment();
1694 $text = $child->textContent;
1695
1696 while (($pos = mb_stripos($text, $word)) !== false) {
1697 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1698 $word = mb_substr($text, $pos, mb_strlen($word));
1699 $highlight = $doc->createElement('span');
1700 $highlight->appendChild(new DomText($word));
1701 $highlight->setAttribute('class', 'highlight');
1702 $fragment->appendChild($highlight);
1703 $text = mb_substr($text, $pos + mb_strlen($word));
1704 }
1705
1706 if (!empty($text)) $fragment->appendChild(new DomText($text));
1707
1708 $child->parentNode->replaceChild($fragment, $child);
1709 }
1710 }
1711 }
1712
1713 $res = $doc->saveHTML();
1714
1715 /* strip everything outside of <body>...</body> */
1716
1717 $res_frag = array();
1718 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1719 return $res_frag[1];
1720 } else {
1721 return $res;
1722 }
1723 }
1724
1725 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1726 $xpath = new DOMXPath($doc);
1727 $entries = $xpath->query('//*');
1728
1729 foreach ($entries as $entry) {
1730 if (!in_array($entry->nodeName, $allowed_elements)) {
1731 $entry->parentNode->removeChild($entry);
1732 }
1733
1734 if ($entry->hasAttributes()) {
1735 $attrs_to_remove = array();
1736
1737 foreach ($entry->attributes as $attr) {
1738
1739 if (strpos($attr->nodeName, 'on') === 0) {
1740 array_push($attrs_to_remove, $attr);
1741 }
1742
1743 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1744 array_push($attrs_to_remove, $attr);
1745 }
1746
1747 if (in_array($attr->nodeName, $disallowed_attributes)) {
1748 array_push($attrs_to_remove, $attr);
1749 }
1750 }
1751
1752 foreach ($attrs_to_remove as $attr) {
1753 $entry->removeAttributeNode($attr);
1754 }
1755 }
1756 }
1757
1758 return $doc;
1759 }
1760
1761 function trim_array($array) {
1762 $tmp = $array;
1763 array_walk($tmp, 'trim');
1764 return $tmp;
1765 }
1766
1767 function tag_is_valid($tag) {
1768 if (!$tag || is_numeric($tag) || mb_strlen($tag) > 250)
1769 return false;
1770
1771 return true;
1772 }
1773
1774 function render_login_form() {
1775 header('Cache-Control: public');
1776
1777 require_once "login_form.php";
1778 exit;
1779 }
1780
1781 function T_sprintf() {
1782 $args = func_get_args();
1783 return vsprintf(__(array_shift($args)), $args);
1784 }
1785
1786 function print_checkpoint($n, $s) {
1787 $ts = microtime(true);
1788 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1789 return $ts;
1790 }
1791
1792 function sanitize_tag($tag) {
1793 $tag = trim($tag);
1794
1795 $tag = mb_strtolower($tag, 'utf-8');
1796
1797 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1798
1799 if (DB_TYPE == "mysql") {
1800 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1801 }
1802
1803 return $tag;
1804 }
1805
1806 function is_server_https() {
1807 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1808 }
1809
1810 function is_prefix_https() {
1811 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1812 }
1813
1814 // this returns SELF_URL_PATH sans ending slash
1815 function get_self_url_prefix() {
1816 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1817 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1818 } else {
1819 return SELF_URL_PATH;
1820 }
1821 }
1822
1823 function encrypt_password($pass, $salt = '', $mode2 = false) {
1824 if ($salt && $mode2) {
1825 return "MODE2:" . hash('sha256', $salt . $pass);
1826 } else if ($salt) {
1827 return "SHA1X:" . sha1("$salt:$pass");
1828 } else {
1829 return "SHA1:" . sha1($pass);
1830 }
1831 } // function encrypt_password
1832
1833 function load_filters($feed_id, $owner_uid) {
1834 $filters = array();
1835
1836 $feed_id = (int) $feed_id;
1837 $cat_id = (int)Feeds::getFeedCategory($feed_id);
1838
1839 if ($cat_id == 0)
1840 $null_cat_qpart = "cat_id IS NULL OR";
1841 else
1842 $null_cat_qpart = "";
1843
1844 $pdo = Db::pdo();
1845
1846 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1847 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1848 $sth->execute([$owner_uid]);
1849
1850 $check_cats = array_merge(
1851 Feeds::getParentCategories($cat_id, $owner_uid),
1852 [$cat_id]);
1853
1854 $check_cats_str = join(",", $check_cats);
1855 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1856
1857 while ($line = $sth->fetch()) {
1858 $filter_id = $line["id"];
1859
1860 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1861
1862 $sth2 = $pdo->prepare("SELECT
1863 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1864 FROM ttrss_filters2_rules AS r,
1865 ttrss_filter_types AS t
1866 WHERE
1867 (match_on IS NOT NULL OR
1868 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1869 (feed_id IS NULL OR feed_id = ?))) AND
1870 filter_type = t.id AND filter_id = ?");
1871 $sth2->execute([$feed_id, $filter_id]);
1872
1873 $rules = array();
1874 $actions = array();
1875
1876 while ($rule_line = $sth2->fetch()) {
1877 # print_r($rule_line);
1878
1879 if ($rule_line["match_on"]) {
1880 $match_on = json_decode($rule_line["match_on"], true);
1881
1882 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1883
1884 $rule = array();
1885 $rule["reg_exp"] = $rule_line["reg_exp"];
1886 $rule["type"] = $rule_line["type_name"];
1887 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1888
1889 array_push($rules, $rule);
1890 } else if (!$match_any_rule) {
1891 // this filter contains a rule that doesn't match to this feed/category combination
1892 // thus filter has to be rejected
1893
1894 $rules = [];
1895 break;
1896 }
1897
1898 } else {
1899
1900 $rule = array();
1901 $rule["reg_exp"] = $rule_line["reg_exp"];
1902 $rule["type"] = $rule_line["type_name"];
1903 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1904
1905 array_push($rules, $rule);
1906 }
1907 }
1908
1909 if (count($rules) > 0) {
1910 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1911 FROM ttrss_filters2_actions AS a,
1912 ttrss_filter_actions AS t
1913 WHERE
1914 action_id = t.id AND filter_id = ?");
1915 $sth2->execute([$filter_id]);
1916
1917 while ($action_line = $sth2->fetch()) {
1918 # print_r($action_line);
1919
1920 $action = array();
1921 $action["type"] = $action_line["type_name"];
1922 $action["param"] = $action_line["action_param"];
1923
1924 array_push($actions, $action);
1925 }
1926 }
1927
1928 $filter = array();
1929 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1930 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1931 $filter["rules"] = $rules;
1932 $filter["actions"] = $actions;
1933
1934 if (count($rules) > 0 && count($actions) > 0) {
1935 array_push($filters, $filter);
1936 }
1937 }
1938
1939 return $filters;
1940 }
1941
1942 function get_score_pic($score) {
1943 if ($score > 100) {
1944 return "score_high.png";
1945 } else if ($score > 0) {
1946 return "score_half_high.png";
1947 } else if ($score < -100) {
1948 return "score_low.png";
1949 } else if ($score < 0) {
1950 return "score_half_low.png";
1951 } else {
1952 return "score_neutral.png";
1953 }
1954 }
1955
1956 function init_plugins() {
1957 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1958
1959 return true;
1960 }
1961
1962 function add_feed_category($feed_cat, $parent_cat_id = false) {
1963
1964 if (!$feed_cat) return false;
1965
1966 $feed_cat = mb_substr($feed_cat, 0, 250);
1967 if (!$parent_cat_id) $parent_cat_id = null;
1968
1969 $pdo = Db::pdo();
1970 $tr_in_progress = false;
1971
1972 try {
1973 $pdo->beginTransaction();
1974 } catch (Exception $e) {
1975 $tr_in_progress = true;
1976 }
1977
1978 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
1979 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
1980 AND title = :title AND owner_uid = :uid");
1981 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
1982
1983 if (!$sth->fetch()) {
1984
1985 $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1986 VALUES (?, ?, ?)");
1987 $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
1988
1989 if (!$tr_in_progress) $pdo->commit();
1990
1991 return true;
1992 }
1993
1994 $pdo->commit();
1995
1996 return false;
1997 }
1998
1999 /**
2000 * Fixes incomplete URLs by prepending "http://".
2001 * Also replaces feed:// with http://, and
2002 * prepends a trailing slash if the url is a domain name only.
2003 *
2004 * @param string $url Possibly incomplete URL
2005 *
2006 * @return string Fixed URL.
2007 */
2008 function fix_url($url) {
2009
2010 // support schema-less urls
2011 if (strpos($url, '//') === 0) {
2012 $url = 'https:' . $url;
2013 }
2014
2015 if (strpos($url, '://') === false) {
2016 $url = 'http://' . $url;
2017 } else if (substr($url, 0, 5) == 'feed:') {
2018 $url = 'http:' . substr($url, 5);
2019 }
2020
2021 //prepend slash if the URL has no slash in it
2022 // "http://www.example" -> "http://www.example/"
2023 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2024 $url .= '/';
2025 }
2026
2027 //convert IDNA hostname to punycode if possible
2028 if (function_exists("idn_to_ascii")) {
2029 $parts = parse_url($url);
2030 if (mb_detect_encoding($parts['host']) != 'ASCII')
2031 {
2032 $parts['host'] = idn_to_ascii($parts['host']);
2033 $url = build_url($parts);
2034 }
2035 }
2036
2037 if ($url != "http:///")
2038 return $url;
2039 else
2040 return '';
2041 }
2042
2043 function validate_feed_url($url) {
2044 $parts = parse_url($url);
2045
2046 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2047
2048 }
2049
2050 /* function save_email_address($email) {
2051 // FIXME: implement persistent storage of emails
2052
2053 if (!$_SESSION['stored_emails'])
2054 $_SESSION['stored_emails'] = array();
2055
2056 if (!in_array($email, $_SESSION['stored_emails']))
2057 array_push($_SESSION['stored_emails'], $email);
2058 } */
2059
2060
2061 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2062
2063 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2064
2065 $is_cat = bool_to_sql_bool($is_cat);
2066
2067 $pdo = Db::pdo();
2068
2069 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2070 WHERE feed_id = ? AND is_cat = ?
2071 AND owner_uid = ?");
2072 $sth->execute([$feed_id, (int)$is_cat, $owner_uid]);
2073
2074 if ($row = $sth->fetch()) {
2075 return $row["access_key"];
2076 } else {
2077 $key = uniqid_short();
2078
2079 $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2080 (access_key, feed_id, is_cat, owner_uid)
2081 VALUES (?, ?, ?, ?)");
2082
2083 $sth->execute([$key, $feed_id, (int)$is_cat, $owner_uid]);
2084
2085 return $key;
2086 }
2087 }
2088
2089 function get_feeds_from_html($url, $content)
2090 {
2091 $url = fix_url($url);
2092 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2093
2094 libxml_use_internal_errors(true);
2095
2096 $doc = new DOMDocument();
2097 $doc->loadHTML($content);
2098 $xpath = new DOMXPath($doc);
2099 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2100 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2101 $feedUrls = array();
2102 foreach ($entries as $entry) {
2103 if ($entry->hasAttribute('href')) {
2104 $title = $entry->getAttribute('title');
2105 if ($title == '') {
2106 $title = $entry->getAttribute('type');
2107 }
2108 $feedUrl = rewrite_relative_url(
2109 $baseUrl, $entry->getAttribute('href')
2110 );
2111 $feedUrls[$feedUrl] = $title;
2112 }
2113 }
2114 return $feedUrls;
2115 }
2116
2117 function is_html($content) {
2118 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2119 }
2120
2121 function url_is_html($url, $login = false, $pass = false) {
2122 return is_html(fetch_file_contents($url, false, $login, $pass));
2123 }
2124
2125 function build_url($parts) {
2126 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2127 }
2128
2129 function cleanup_url_path($path) {
2130 $path = str_replace("/./", "/", $path);
2131 $path = str_replace("//", "/", $path);
2132
2133 return $path;
2134 }
2135
2136 /**
2137 * Converts a (possibly) relative URL to a absolute one.
2138 *
2139 * @param string $url Base URL (i.e. from where the document is)
2140 * @param string $rel_url Possibly relative URL in the document
2141 *
2142 * @return string Absolute URL
2143 */
2144 function rewrite_relative_url($url, $rel_url) {
2145 if (strpos($rel_url, "://") !== false) {
2146 return $rel_url;
2147 } else if (strpos($rel_url, "//") === 0) {
2148 # protocol-relative URL (rare but they exist)
2149 return $rel_url;
2150 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2151 # magnet:, feed:, etc
2152 return $rel_url;
2153 } else if (strpos($rel_url, "/") === 0) {
2154 $parts = parse_url($url);
2155 $parts['path'] = $rel_url;
2156 $parts['path'] = cleanup_url_path($parts['path']);
2157
2158 return build_url($parts);
2159
2160 } else {
2161 $parts = parse_url($url);
2162 if (!isset($parts['path'])) {
2163 $parts['path'] = '/';
2164 }
2165 $dir = $parts['path'];
2166 if (substr($dir, -1) !== '/') {
2167 $dir = dirname($parts['path']);
2168 $dir !== '/' && $dir .= '/';
2169 }
2170 $parts['path'] = $dir . $rel_url;
2171 $parts['path'] = cleanup_url_path($parts['path']);
2172
2173 return build_url($parts);
2174 }
2175 }
2176
2177 function cleanup_tags($days = 14, $limit = 1000) {
2178
2179 $days = (int) $days;
2180
2181 if (DB_TYPE == "pgsql") {
2182 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2183 } else if (DB_TYPE == "mysql") {
2184 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2185 }
2186
2187 $tags_deleted = 0;
2188
2189 $pdo = Db::pdo();
2190
2191 while ($limit > 0) {
2192 $limit_part = 500;
2193
2194 $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2195 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2196 WHERE post_int_id = int_id AND $interval_query AND
2197 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2198 $sth->execute([$limit]);
2199
2200 $ids = array();
2201
2202 while ($line = $sth->fetch()) {
2203 array_push($ids, $line['id']);
2204 }
2205
2206 if (count($ids) > 0) {
2207 $ids = join(",", $ids);
2208
2209 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2210 $tags_deleted = $usth->rowCount();
2211 } else {
2212 break;
2213 }
2214
2215 $limit -= $limit_part;
2216 }
2217
2218 return $tags_deleted;
2219 }
2220
2221 function print_user_stylesheet() {
2222 $value = get_pref('USER_STYLESHEET');
2223
2224 if ($value) {
2225 print "<style type=\"text/css\">";
2226 print str_replace("<br/>", "\n", $value);
2227 print "</style>";
2228 }
2229
2230 }
2231
2232 function filter_to_sql($filter, $owner_uid) {
2233 $query = array();
2234
2235 $pdo = Db::pdo();
2236
2237 if (DB_TYPE == "pgsql")
2238 $reg_qpart = "~";
2239 else
2240 $reg_qpart = "REGEXP";
2241
2242 foreach ($filter["rules"] AS $rule) {
2243 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2244 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2245 $rule['reg_exp']) !== FALSE;
2246
2247 if ($regexp_valid) {
2248
2249 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2250
2251 switch ($rule["type"]) {
2252 case "title":
2253 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2254 $rule['reg_exp'] . "')";
2255 break;
2256 case "content":
2257 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2258 $rule['reg_exp'] . "')";
2259 break;
2260 case "both":
2261 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2262 $rule['reg_exp'] . "') OR LOWER(" .
2263 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2264 break;
2265 case "tag":
2266 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2267 $rule['reg_exp'] . "')";
2268 break;
2269 case "link":
2270 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2271 $rule['reg_exp'] . "')";
2272 break;
2273 case "author":
2274 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2275 $rule['reg_exp'] . "')";
2276 break;
2277 }
2278
2279 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2280
2281 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2282 $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2283 }
2284
2285 if (isset($rule["cat_id"])) {
2286
2287 if ($rule["cat_id"] > 0) {
2288 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2289 array_push($children, $rule["cat_id"]);
2290 $children = array_map("intval", $children);
2291
2292 $children = join(",", $children);
2293
2294 $cat_qpart = "cat_id IN ($children)";
2295 } else {
2296 $cat_qpart = "cat_id IS NULL";
2297 }
2298
2299 $qpart .= " AND $cat_qpart";
2300 }
2301
2302 $qpart .= " AND feed_id IS NOT NULL";
2303
2304 array_push($query, "($qpart)");
2305
2306 }
2307 }
2308
2309 if (count($query) > 0) {
2310 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2311 } else {
2312 $fullquery = "(false)";
2313 }
2314
2315 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2316
2317 return $fullquery;
2318 }
2319
2320 if (!function_exists('gzdecode')) {
2321 function gzdecode($string) { // no support for 2nd argument
2322 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2323 base64_encode($string));
2324 }
2325 }
2326
2327 function get_random_bytes($length) {
2328 if (function_exists('openssl_random_pseudo_bytes')) {
2329 return openssl_random_pseudo_bytes($length);
2330 } else {
2331 $output = "";
2332
2333 for ($i = 0; $i < $length; $i++)
2334 $output .= chr(mt_rand(0, 255));
2335
2336 return $output;
2337 }
2338 }
2339
2340 function read_stdin() {
2341 $fp = fopen("php://stdin", "r");
2342
2343 if ($fp) {
2344 $line = trim(fgets($fp));
2345 fclose($fp);
2346 return $line;
2347 }
2348
2349 return null;
2350 }
2351
2352 function implements_interface($class, $interface) {
2353 return in_array($interface, class_implements($class));
2354 }
2355
2356 function get_minified_js($files) {
2357 require_once 'lib/jshrink/Minifier.php';
2358
2359 $rv = '';
2360
2361 foreach ($files as $js) {
2362 if (!isset($_GET['debug'])) {
2363 $cached_file = CACHE_DIR . "/js/".basename($js);
2364
2365 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2366
2367 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2368
2369 if ($header && $contents) {
2370 list($htag, $hversion) = explode(":", $header);
2371
2372 if ($htag == "tt-rss" && $hversion == VERSION) {
2373 $rv .= $contents;
2374 continue;
2375 }
2376 }
2377 }
2378
2379 $minified = JShrink\Minifier::minify(file_get_contents("js/$js"));
2380 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2381 $rv .= $minified;
2382
2383 } else {
2384 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2385 }
2386 }
2387
2388 return $rv;
2389 }
2390
2391 function calculate_dep_timestamp() {
2392 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2393
2394 $max_ts = -1;
2395
2396 foreach ($files as $file) {
2397 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2398 }
2399
2400 return $max_ts;
2401 }
2402
2403 function T_js_decl($s1, $s2) {
2404 if ($s1 && $s2) {
2405 $s1 = preg_replace("/\n/", "", $s1);
2406 $s2 = preg_replace("/\n/", "", $s2);
2407
2408 $s1 = preg_replace("/\"/", "\\\"", $s1);
2409 $s2 = preg_replace("/\"/", "\\\"", $s2);
2410
2411 return "T_messages[\"$s1\"] = \"$s2\";\n";
2412 }
2413 }
2414
2415 function init_js_translations() {
2416
2417 print 'var T_messages = new Object();
2418
2419 function __(msg) {
2420 if (T_messages[msg]) {
2421 return T_messages[msg];
2422 } else {
2423 return msg;
2424 }
2425 }
2426
2427 function ngettext(msg1, msg2, n) {
2428 return __((parseInt(n) > 1) ? msg2 : msg1);
2429 }';
2430
2431 $l10n = _get_reader();
2432
2433 for ($i = 0; $i < $l10n->total; $i++) {
2434 $orig = $l10n->get_original_string($i);
2435 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2436 $key = explode(chr(0), $orig);
2437 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2438 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2439 } else {
2440 $translation = __($orig);
2441 print T_js_decl($orig, $translation);
2442 }
2443 }
2444 }
2445
2446 function get_theme_path($theme) {
2447 if ($theme == "default.php")
2448 return "css/default.css";
2449
2450 $check = "themes/$theme";
2451 if (file_exists($check)) return $check;
2452
2453 $check = "themes.local/$theme";
2454 if (file_exists($check)) return $check;
2455 }
2456
2457 function theme_valid($theme) {
2458 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2459
2460 if (in_array($theme, $bundled_themes)) return true;
2461
2462 $file = "themes/" . basename($theme);
2463
2464 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2465
2466 if (file_exists($file) && is_readable($file)) {
2467 $fh = fopen($file, "r");
2468
2469 if ($fh) {
2470 $header = fgets($fh);
2471 fclose($fh);
2472
2473 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2474 }
2475 }
2476
2477 return false;
2478 }
2479
2480 /**
2481 * @SuppressWarnings(unused)
2482 */
2483 function error_json($code) {
2484 require_once "errors.php";
2485
2486 @$message = $ERRORS[$code];
2487
2488 return json_encode(array("error" =>
2489 array("code" => $code, "message" => $message)));
2490
2491 }
2492
2493 /*function abs_to_rel_path($dir) {
2494 $tmp = str_replace(dirname(__DIR__), "", $dir);
2495
2496 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2497
2498 return $tmp;
2499 }*/
2500
2501 function get_upload_error_message($code) {
2502
2503 $errors = array(
2504 0 => __('There is no error, the file uploaded with success'),
2505 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2506 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2507 3 => __('The uploaded file was only partially uploaded'),
2508 4 => __('No file was uploaded'),
2509 6 => __('Missing a temporary folder'),
2510 7 => __('Failed to write file to disk.'),
2511 8 => __('A PHP extension stopped the file upload.'),
2512 );
2513
2514 return $errors[$code];
2515 }
2516
2517 function base64_img($filename) {
2518 if (file_exists($filename)) {
2519 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2520
2521 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2522 } else {
2523 return "";
2524 }
2525 }
2526
2527 /* this is essentially a wrapper for readfile() which allows plugins to hook
2528 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2529
2530 hook function should return true if request was handled (or at least attempted to)
2531
2532 note that this can be called without user context so the plugin to handle this
2533 should be loaded systemwide in config.php */
2534 function send_local_file($filename) {
2535 if (file_exists($filename)) {
2536 $tmppluginhost = new PluginHost();
2537
2538 $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
2539 $tmppluginhost->load_data();
2540
2541 foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
2542 if ($plugin->hook_send_local_file($filename)) return true;
2543 }
2544
2545 $mimetype = mime_content_type($filename);
2546 header("Content-type: $mimetype");
2547
2548 $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2549 header("Last-Modified: $stamp", true);
2550
2551 return readfile($filename);
2552 } else {
2553 return false;
2554 }
2555 }
2556
2557 function check_mysql_tables() {
2558 $pdo = Db::pdo();
2559
2560 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2561 table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2562 $sth->execute([DB_NAME]);
2563
2564 $bad_tables = [];
2565
2566 while ($line = $sth->fetch()) {
2567 array_push($bad_tables, $line);
2568 }
2569
2570 return $bad_tables;
2571 }
2572
2573 function validate_field($string, $allowed, $default = "") {
2574 if (in_array($string, $allowed))
2575 return $string;
2576 else
2577 return $default;
2578 }
2579
2580 function arr_qmarks($arr) {
2581 return str_repeat('?,', count($arr) - 1) . '?';
2582 }