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