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