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