]> git.wh0rd.org - tt-rss.git/blob - include/functions.php
$_SERVER['HTTPS'] can be exists and 'off' for non-https connectios
[tt-rss.git] / include / functions.php
1 <?php
2 define('EXPECTED_CONFIG_VERSION', 26);
3 define('SCHEMA_VERSION', 131);
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 $rows = -1;
227
228 $result = db_query(
229 "SELECT owner_uid FROM ttrss_feeds WHERE id = '$feed_id'");
230
231 $owner_uid = false;
232
233 if (db_num_rows($result) == 1) {
234 $owner_uid = db_fetch_result($result, 0, "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) $query_limit = " unread = false AND ";
255
256 if (DB_TYPE == "pgsql") {
257 $result = db_query("DELETE FROM ttrss_user_entries
258 USING ttrss_entries
259 WHERE ttrss_entries.id = ref_id AND
260 marked = false AND
261 feed_id = '$feed_id' AND
262 $query_limit
263 ttrss_entries.date_updated < NOW() - INTERVAL '$purge_interval days'");
264
265 } else {
266
267 /* $result = db_query("DELETE FROM ttrss_user_entries WHERE
268 marked = false AND feed_id = '$feed_id' AND
269 (SELECT date_updated FROM ttrss_entries WHERE
270 id = ref_id) < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)"); */
271
272 $result = db_query("DELETE FROM ttrss_user_entries
273 USING ttrss_user_entries, ttrss_entries
274 WHERE ttrss_entries.id = ref_id AND
275 marked = false AND
276 feed_id = '$feed_id' AND
277 $query_limit
278 ttrss_entries.date_updated < DATE_SUB(NOW(), INTERVAL $purge_interval DAY)");
279 }
280
281 $rows = db_affected_rows($result);
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 $result = db_query("SELECT purge_interval, owner_uid FROM ttrss_feeds
295 WHERE id = '$feed_id'");
296
297 if (db_num_rows($result) == 1) {
298 $purge_interval = db_fetch_result($result, 0, "purge_interval");
299 $owner_uid = db_fetch_result($result, 0, "owner_uid");
300
301 if ($purge_interval == 0) $purge_interval = get_pref(
302 'PURGE_OLD_DAYS', $owner_uid);
303
304 return $purge_interval;
305
306 } else {
307 return -1;
308 }
309 }
310
311 /*function get_feed_update_interval($feed_id) {
312 $result = db_query("SELECT owner_uid, update_interval FROM
313 ttrss_feeds WHERE id = '$feed_id'");
314
315 if (db_num_rows($result) == 1) {
316 $update_interval = db_fetch_result($result, 0, "update_interval");
317 $owner_uid = db_fetch_result($result, 0, "owner_uid");
318
319 if ($update_interval != 0) {
320 return $update_interval;
321 } else {
322 return get_pref('DEFAULT_UPDATE_INTERVAL', $owner_uid, false);
323 }
324
325 } else {
326 return -1;
327 }
328 }*/
329
330 // TODO: multiple-argument way is deprecated, first parameter is a hash now
331 function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
332 4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
333
334 global $fetch_last_error;
335 global $fetch_last_error_code;
336 global $fetch_last_error_content;
337 global $fetch_last_content_type;
338 global $fetch_curl_used;
339
340 $fetch_last_error = false;
341 $fetch_last_error_code = -1;
342 $fetch_last_error_content = "";
343 $fetch_last_content_type = "";
344 $fetch_curl_used = false;
345
346 if (!is_array($options)) {
347
348 // falling back on compatibility shim
349 $option_names = [ "url", "type", "login", "pass", "post_query", "timeout", "timestamp", "useragent" ];
350 $tmp = [];
351
352 for ($i = 0; $i < func_num_args(); $i++) {
353 $tmp[$option_names[$i]] = func_get_arg($i);
354 }
355
356 $options = $tmp;
357
358 /*$options = array(
359 "url" => func_get_arg(0),
360 "type" => @func_get_arg(1),
361 "login" => @func_get_arg(2),
362 "pass" => @func_get_arg(3),
363 "post_query" => @func_get_arg(4),
364 "timeout" => @func_get_arg(5),
365 "timestamp" => @func_get_arg(6),
366 "useragent" => @func_get_arg(7)
367 ); */
368 }
369
370 $url = $options["url"];
371 $type = isset($options["type"]) ? $options["type"] : false;
372 $login = isset($options["login"]) ? $options["login"] : false;
373 $pass = isset($options["pass"]) ? $options["pass"] : false;
374 $post_query = isset($options["post_query"]) ? $options["post_query"] : false;
375 $timeout = isset($options["timeout"]) ? $options["timeout"] : false;
376 $timestamp = isset($options["timestamp"]) ? $options["timestamp"] : 0;
377 $useragent = isset($options["useragent"]) ? $options["useragent"] : false;
378 $followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
379
380 $url = ltrim($url, ' ');
381 $url = str_replace(' ', '%20', $url);
382
383 if (strpos($url, "//") === 0)
384 $url = 'http:' . $url;
385
386 if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
387
388 $fetch_curl_used = true;
389
390 $ch = curl_init($url);
391
392 if ($timestamp && !$post_query) {
393 curl_setopt($ch, CURLOPT_HTTPHEADER,
394 array("If-Modified-Since: ".gmdate('D, d M Y H:i:s \G\M\T', $timestamp)));
395 }
396
397 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
398 curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : FILE_FETCH_TIMEOUT);
399 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
400 curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
401 curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
402 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
403 curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
404 curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent :
405 SELF_USER_AGENT);
406 curl_setopt($ch, CURLOPT_ENCODING, "");
407 //curl_setopt($ch, CURLOPT_REFERER, $url);
408
409 if (!ini_get("open_basedir")) {
410 curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
411 }
412
413 if (defined('_CURL_HTTP_PROXY')) {
414 curl_setopt($ch, CURLOPT_PROXY, _CURL_HTTP_PROXY);
415 }
416
417 if ($post_query) {
418 curl_setopt($ch, CURLOPT_POST, true);
419 curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
420 }
421
422 if ($login && $pass)
423 curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
424
425 $contents = @curl_exec($ch);
426
427 if (curl_errno($ch) === 23 || curl_errno($ch) === 61) {
428 curl_setopt($ch, CURLOPT_ENCODING, 'none');
429 $contents = @curl_exec($ch);
430 }
431
432 if ($contents === false) {
433 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
434 curl_close($ch);
435 return false;
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 if (curl_errno($ch) != 0) {
445 $fetch_last_error = curl_errno($ch) . " " . curl_error($ch);
446 } else {
447 $fetch_last_error = "HTTP Code: $http_code";
448 }
449 $fetch_last_error_content = $contents;
450 curl_close($ch);
451 return false;
452 }
453
454 curl_close($ch);
455
456 return $contents;
457 } else {
458
459 $fetch_curl_used = false;
460
461 if ($login && $pass){
462 $url_parts = array();
463
464 preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
465
466 $pass = urlencode($pass);
467
468 if ($url_parts[1] && $url_parts[2]) {
469 $url = $url_parts[1] . "://$login:$pass@" . $url_parts[2];
470 }
471 }
472
473 // TODO: should this support POST requests or not? idk
474
475 if (!$post_query && $timestamp) {
476 $context = stream_context_create(array(
477 'http' => array(
478 'method' => 'GET',
479 'ignore_errors' => true,
480 'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
481 'protocol_version'=> 1.1,
482 'header' => "If-Modified-Since: ".gmdate("D, d M Y H:i:s \\G\\M\\T\r\n", $timestamp)
483 )));
484 } else {
485 $context = stream_context_create(array(
486 'http' => array(
487 'method' => 'GET',
488 'ignore_errors' => true,
489 'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
490 'protocol_version'=> 1.1
491 )));
492 }
493
494 $old_error = error_get_last();
495
496 $data = @file_get_contents($url, false, $context);
497
498 if (isset($http_response_header) && is_array($http_response_header)) {
499 foreach ($http_response_header as $h) {
500 if (substr(strtolower($h), 0, 13) == 'content-type:') {
501 $fetch_last_content_type = substr($h, 14);
502 // don't abort here b/c there might be more than one
503 // e.g. if we were being redirected -- last one is the right one
504 }
505
506 if (substr(strtolower($h), 0, 7) == 'http/1.') {
507 $fetch_last_error_code = (int) substr($h, 9, 3);
508 }
509 }
510 }
511
512 if ($fetch_last_error_code != 200) {
513 $error = error_get_last();
514
515 if ($error['message'] != $old_error['message']) {
516 $fetch_last_error = $error["message"];
517 } else {
518 $fetch_last_error = "HTTP Code: $fetch_last_error_code";
519 }
520
521 $fetch_last_error_content = $data;
522
523 return false;
524 }
525 return $data;
526 }
527
528 }
529
530 /**
531 * Try to determine the favicon URL for a feed.
532 * adapted from wordpress favicon plugin by Jeff Minard (http://thecodepro.com/)
533 * http://dev.wp-plugins.org/file/favatars/trunk/favatars.php
534 *
535 * @param string $url A feed or page URL
536 * @access public
537 * @return mixed The favicon URL, or false if none was found.
538 */
539 function get_favicon_url($url) {
540
541 $favicon_url = false;
542
543 if ($html = @fetch_file_contents($url)) {
544
545 libxml_use_internal_errors(true);
546
547 $doc = new DOMDocument();
548 $doc->loadHTML($html);
549 $xpath = new DOMXPath($doc);
550
551 $base = $xpath->query('/html/head/base');
552 foreach ($base as $b) {
553 $url = $b->getAttribute("href");
554 break;
555 }
556
557 $entries = $xpath->query('/html/head/link[@rel="shortcut icon" or @rel="icon"]');
558 if (count($entries) > 0) {
559 foreach ($entries as $entry) {
560 $favicon_url = rewrite_relative_url($url, $entry->getAttribute("href"));
561 break;
562 }
563 }
564 }
565
566 if (!$favicon_url)
567 $favicon_url = rewrite_relative_url($url, "/favicon.ico");
568
569 return $favicon_url;
570 } // function get_favicon_url
571
572 function initialize_user_prefs($uid, $profile = false) {
573
574 $uid = db_escape_string($uid);
575
576 if (!$profile) {
577 $profile = "NULL";
578 $profile_qpart = "AND profile IS NULL";
579 } else {
580 $profile_qpart = "AND profile = '$profile'";
581 }
582
583 if (get_schema_version() < 63) $profile_qpart = "";
584
585 db_query("BEGIN");
586
587 $result = db_query("SELECT pref_name,def_value FROM ttrss_prefs");
588
589 $u_result = db_query("SELECT pref_name
590 FROM ttrss_user_prefs WHERE owner_uid = '$uid' $profile_qpart");
591
592 $active_prefs = array();
593
594 while ($line = db_fetch_assoc($u_result)) {
595 array_push($active_prefs, $line["pref_name"]);
596 }
597
598 while ($line = db_fetch_assoc($result)) {
599 if (array_search($line["pref_name"], $active_prefs) === FALSE) {
600 // print "adding " . $line["pref_name"] . "<br>";
601
602 $line["def_value"] = db_escape_string($line["def_value"]);
603 $line["pref_name"] = db_escape_string($line["pref_name"]);
604
605 if (get_schema_version() < 63) {
606 db_query("INSERT INTO ttrss_user_prefs
607 (owner_uid,pref_name,value) VALUES
608 ('$uid', '".$line["pref_name"]."','".$line["def_value"]."')");
609
610 } else {
611 db_query("INSERT INTO ttrss_user_prefs
612 (owner_uid,pref_name,value, profile) VALUES
613 ('$uid', '".$line["pref_name"]."','".$line["def_value"]."', $profile)");
614 }
615
616 }
617 }
618
619 db_query("COMMIT");
620
621 }
622
623 function get_ssl_certificate_id() {
624 if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
625 return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"] .
626 $_SERVER["REDIRECT_SSL_CLIENT_V_START"] .
627 $_SERVER["REDIRECT_SSL_CLIENT_V_END"] .
628 $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
629 }
630 if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
631 return sha1($_SERVER["SSL_CLIENT_M_SERIAL"] .
632 $_SERVER["SSL_CLIENT_V_START"] .
633 $_SERVER["SSL_CLIENT_V_END"] .
634 $_SERVER["SSL_CLIENT_S_DN"]);
635 }
636 return "";
637 }
638
639 function authenticate_user($login, $password, $check_only = false) {
640
641 if (!SINGLE_USER_MODE) {
642 $user_id = false;
643
644 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
645
646 $user_id = (int) $plugin->authenticate($login, $password);
647
648 if ($user_id) {
649 $_SESSION["auth_module"] = strtolower(get_class($plugin));
650 break;
651 }
652 }
653
654 if ($user_id && !$check_only) {
655 @session_start();
656
657 $_SESSION["uid"] = $user_id;
658 $_SESSION["version"] = VERSION_STATIC;
659
660 $result = db_query("SELECT login,access_level,pwd_hash FROM ttrss_users
661 WHERE id = '$user_id'");
662
663 $_SESSION["name"] = db_fetch_result($result, 0, "login");
664 $_SESSION["access_level"] = db_fetch_result($result, 0, "access_level");
665 $_SESSION["csrf_token"] = uniqid_short();
666
667 db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
668 $_SESSION["uid"]);
669
670 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
671 $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
672 $_SESSION["pwd_hash"] = db_fetch_result($result, 0, "pwd_hash");
673
674 $_SESSION["last_version_check"] = time();
675
676 initialize_user_prefs($_SESSION["uid"]);
677
678 return true;
679 }
680
681 return false;
682
683 } else {
684
685 $_SESSION["uid"] = 1;
686 $_SESSION["name"] = "admin";
687 $_SESSION["access_level"] = 10;
688
689 $_SESSION["hide_hello"] = true;
690 $_SESSION["hide_logout"] = true;
691
692 $_SESSION["auth_module"] = false;
693
694 if (!$_SESSION["csrf_token"]) {
695 $_SESSION["csrf_token"] = uniqid_short();
696 }
697
698 $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
699
700 initialize_user_prefs($_SESSION["uid"]);
701
702 return true;
703 }
704 }
705
706 function make_password($length = 8) {
707
708 $password = "";
709 $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ";
710
711 $i = 0;
712
713 while ($i < $length) {
714 $char = substr($possible, mt_rand(0, strlen($possible)-1), 1);
715
716 if (!strstr($password, $char)) {
717 $password .= $char;
718 $i++;
719 }
720 }
721 return $password;
722 }
723
724 // this is called after user is created to initialize default feeds, labels
725 // or whatever else
726
727 // user preferences are checked on every login, not here
728
729 function initialize_user($uid) {
730
731 db_query("insert into ttrss_feeds (owner_uid,title,feed_url)
732 values ('$uid', 'Tiny Tiny RSS: Forum',
733 'http://tt-rss.org/forum/rss.php')");
734 }
735
736 function logout_user() {
737 session_destroy();
738 if (isset($_COOKIE[session_name()])) {
739 setcookie(session_name(), '', time()-42000, '/');
740 }
741 }
742
743 function validate_csrf($csrf_token) {
744 return $csrf_token == $_SESSION['csrf_token'];
745 }
746
747 function load_user_plugins($owner_uid, $pluginhost = false) {
748
749 if (!$pluginhost) $pluginhost = PluginHost::getInstance();
750
751 if ($owner_uid && SCHEMA_VERSION >= 100) {
752 $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
753
754 $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
755
756 if (get_schema_version() > 100) {
757 $pluginhost->load_data();
758 }
759 }
760 }
761
762 function login_sequence() {
763 if (SINGLE_USER_MODE) {
764 @session_start();
765 authenticate_user("admin", null);
766 startup_gettext();
767 load_user_plugins($_SESSION["uid"]);
768 } else {
769 if (!validate_session()) $_SESSION["uid"] = false;
770
771 if (!$_SESSION["uid"]) {
772
773 if (AUTH_AUTO_LOGIN && authenticate_user(null, null)) {
774 $_SESSION["ref_schema_version"] = get_schema_version(true);
775 } else {
776 authenticate_user(null, null, true);
777 }
778
779 if (!$_SESSION["uid"]) {
780 @session_destroy();
781 setcookie(session_name(), '', time()-42000, '/');
782
783 render_login_form();
784 exit;
785 }
786
787 } else {
788 /* bump login timestamp */
789 db_query("UPDATE ttrss_users SET last_login = NOW() WHERE id = " .
790 $_SESSION["uid"]);
791 $_SESSION["last_login_update"] = time();
792 }
793
794 if ($_SESSION["uid"]) {
795 startup_gettext();
796 load_user_plugins($_SESSION["uid"]);
797
798 /* cleanup ccache */
799
800 db_query("DELETE FROM ttrss_counters_cache WHERE owner_uid = ".
801 $_SESSION["uid"] . " AND
802 (SELECT COUNT(id) FROM ttrss_feeds WHERE
803 ttrss_feeds.id = feed_id) = 0");
804
805 db_query("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ".
806 $_SESSION["uid"] . " AND
807 (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
808 ttrss_feed_categories.id = feed_id) = 0");
809
810 }
811
812 }
813 }
814
815 function truncate_string($str, $max_len, $suffix = '&hellip;') {
816 if (mb_strlen($str, "utf-8") > $max_len) {
817 return mb_substr($str, 0, $max_len, "utf-8") . $suffix;
818 } else {
819 return $str;
820 }
821 }
822
823 // is not utf8 clean
824 function truncate_middle($str, $max_len, $suffix = '&hellip;') {
825 if (strlen($str) > $max_len) {
826 return substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
827 } else {
828 return $str;
829 }
830 }
831
832 function convert_timestamp($timestamp, $source_tz, $dest_tz) {
833
834 try {
835 $source_tz = new DateTimeZone($source_tz);
836 } catch (Exception $e) {
837 $source_tz = new DateTimeZone('UTC');
838 }
839
840 try {
841 $dest_tz = new DateTimeZone($dest_tz);
842 } catch (Exception $e) {
843 $dest_tz = new DateTimeZone('UTC');
844 }
845
846 $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
847 return $dt->format('U') + $dest_tz->getOffset($dt);
848 }
849
850 function make_local_datetime($timestamp, $long, $owner_uid = false,
851 $no_smart_dt = false, $eta_min = false) {
852
853 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
854 if (!$timestamp) $timestamp = '1970-01-01 0:00';
855
856 global $utc_tz;
857 global $user_tz;
858
859 if (!$utc_tz) $utc_tz = new DateTimeZone('UTC');
860
861 $timestamp = substr($timestamp, 0, 19);
862
863 # We store date in UTC internally
864 $dt = new DateTime($timestamp, $utc_tz);
865
866 $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
867
868 if ($user_tz_string != 'Automatic') {
869
870 try {
871 if (!$user_tz) $user_tz = new DateTimeZone($user_tz_string);
872 } catch (Exception $e) {
873 $user_tz = $utc_tz;
874 }
875
876 $tz_offset = $user_tz->getOffset($dt);
877 } else {
878 $tz_offset = (int) -$_SESSION["clientTzOffset"];
879 }
880
881 $user_timestamp = $dt->format('U') + $tz_offset;
882
883 if (!$no_smart_dt) {
884 return smart_date_time($user_timestamp,
885 $tz_offset, $owner_uid, $eta_min);
886 } else {
887 if ($long)
888 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
889 else
890 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
891
892 return date($format, $user_timestamp);
893 }
894 }
895
896 function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
897 if (!$owner_uid) $owner_uid = $_SESSION['uid'];
898
899 if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
900 return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
901 } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
902 return date("G:i", $timestamp);
903 } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
904 $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
905 return date($format, $timestamp);
906 } else {
907 $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
908 return date($format, $timestamp);
909 }
910 }
911
912 function sql_bool_to_bool($s) {
913 if ($s == "t" || $s == "1" || strtolower($s) == "true") {
914 return true;
915 } else {
916 return false;
917 }
918 }
919
920 function bool_to_sql_bool($s) {
921 if ($s) {
922 return "true";
923 } else {
924 return "false";
925 }
926 }
927
928 // Session caching removed due to causing wrong redirects to upgrade
929 // script when get_schema_version() is called on an obsolete session
930 // created on a previous schema version.
931 function get_schema_version($nocache = false) {
932 global $schema_version;
933
934 if (!$schema_version && !$nocache) {
935 $result = db_query("SELECT schema_version FROM ttrss_version");
936 $version = db_fetch_result($result, 0, "schema_version");
937 $schema_version = $version;
938 return $version;
939 } else {
940 return $schema_version;
941 }
942 }
943
944 function sanity_check() {
945 require_once 'errors.php';
946 global $ERRORS;
947
948 $error_code = 0;
949 $schema_version = get_schema_version(true);
950
951 if ($schema_version != SCHEMA_VERSION) {
952 $error_code = 5;
953 }
954
955 if (DB_TYPE == "mysql") {
956 $result = db_query("SELECT true", false);
957 if (db_num_rows($result) != 1) {
958 $error_code = 10;
959 }
960 }
961
962 if (db_escape_string("testTEST") != "testTEST") {
963 $error_code = 12;
964 }
965
966 return array("code" => $error_code, "message" => $ERRORS[$error_code]);
967 }
968
969 function file_is_locked($filename) {
970 if (file_exists(LOCK_DIRECTORY . "/$filename")) {
971 if (function_exists('flock')) {
972 $fp = @fopen(LOCK_DIRECTORY . "/$filename", "r");
973 if ($fp) {
974 if (flock($fp, LOCK_EX | LOCK_NB)) {
975 flock($fp, LOCK_UN);
976 fclose($fp);
977 return false;
978 }
979 fclose($fp);
980 return true;
981 } else {
982 return false;
983 }
984 }
985 return true; // consider the file always locked and skip the test
986 } else {
987 return false;
988 }
989 }
990
991
992 function make_lockfile($filename) {
993 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
994
995 if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
996 $stat_h = fstat($fp);
997 $stat_f = stat(LOCK_DIRECTORY . "/$filename");
998
999 if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
1000 if ($stat_h["ino"] != $stat_f["ino"] ||
1001 $stat_h["dev"] != $stat_f["dev"]) {
1002
1003 return false;
1004 }
1005 }
1006
1007 if (function_exists('posix_getpid')) {
1008 fwrite($fp, posix_getpid() . "\n");
1009 }
1010 return $fp;
1011 } else {
1012 return false;
1013 }
1014 }
1015
1016 function make_stampfile($filename) {
1017 $fp = fopen(LOCK_DIRECTORY . "/$filename", "w");
1018
1019 if (flock($fp, LOCK_EX | LOCK_NB)) {
1020 fwrite($fp, time() . "\n");
1021 flock($fp, LOCK_UN);
1022 fclose($fp);
1023 return true;
1024 } else {
1025 return false;
1026 }
1027 }
1028
1029 function sql_random_function() {
1030 if (DB_TYPE == "mysql") {
1031 return "RAND()";
1032 } else {
1033 return "RANDOM()";
1034 }
1035 }
1036
1037 function getFeedUnread($feed, $is_cat = false) {
1038 return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
1039 }
1040
1041
1042 /*function get_pgsql_version() {
1043 $result = db_query("SELECT version() AS version");
1044 $version = explode(" ", db_fetch_result($result, 0, "version"));
1045 return $version[1];
1046 }*/
1047
1048 function checkbox_to_sql_bool($val) {
1049 return ($val == "on") ? "true" : "false";
1050 }
1051
1052 /*function getFeedCatTitle($id) {
1053 if ($id == -1) {
1054 return __("Special");
1055 } else if ($id < LABEL_BASE_INDEX) {
1056 return __("Labels");
1057 } else if ($id > 0) {
1058 $result = db_query("SELECT ttrss_feed_categories.title
1059 FROM ttrss_feeds, ttrss_feed_categories WHERE ttrss_feeds.id = '$id' AND
1060 cat_id = ttrss_feed_categories.id");
1061 if (db_num_rows($result) == 1) {
1062 return db_fetch_result($result, 0, "title");
1063 } else {
1064 return __("Uncategorized");
1065 }
1066 } else {
1067 return "getFeedCatTitle($id) failed";
1068 }
1069
1070 }*/
1071
1072 function uniqid_short() {
1073 return uniqid(base_convert(rand(), 10, 36));
1074 }
1075
1076 function make_init_params() {
1077 $params = array();
1078
1079 foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
1080 "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
1081 "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
1082 "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
1083
1084 $params[strtolower($param)] = (int) get_pref($param);
1085 }
1086
1087 $params["icons_url"] = ICONS_URL;
1088 $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
1089 $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1090 $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1091 $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1092 $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1093 $params["label_base_index"] = (int) LABEL_BASE_INDEX;
1094
1095 $theme = get_pref( "USER_CSS_THEME", false, false);
1096 $params["theme"] = theme_valid("$theme") ? $theme : "";
1097
1098 $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
1099
1100 $params["php_platform"] = PHP_OS;
1101 $params["php_version"] = PHP_VERSION;
1102
1103 $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1104
1105 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1106 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
1107
1108 $max_feed_id = db_fetch_result($result, 0, "mid");
1109 $num_feeds = db_fetch_result($result, 0, "nf");
1110
1111 $params["max_feed_id"] = (int) $max_feed_id;
1112 $params["num_feeds"] = (int) $num_feeds;
1113
1114 $params["hotkeys"] = get_hotkeys_map();
1115
1116 $params["csrf_token"] = $_SESSION["csrf_token"];
1117 $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1118
1119 $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
1120
1121 $params["icon_alert"] = base64_img("images/alert.png");
1122 $params["icon_information"] = base64_img("images/information.png");
1123 $params["icon_cross"] = base64_img("images/cross.png");
1124 $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
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 $result = db_query("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1309 ttrss_feeds WHERE owner_uid = " . $_SESSION["uid"]);
1310
1311 $max_feed_id = db_fetch_result($result, 0, "mid");
1312 $num_feeds = db_fetch_result($result, 0, "nf");
1313
1314 $data["max_feed_id"] = (int) $max_feed_id;
1315 $data["num_feeds"] = (int) $num_feeds;
1316
1317 $data['last_article_id'] = Article::getLastArticleId();
1318 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1319
1320 $data['dep_ts'] = calculate_dep_timestamp();
1321 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1322
1323
1324 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1325 $update_result = @check_for_update();
1326
1327 $data["update_result"] = $update_result;
1328
1329 $_SESSION["last_version_check"] = time();
1330 }
1331
1332 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1333
1334 $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1335
1336 if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1337
1338 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1339
1340 if ($stamp) {
1341 $stamp_delta = time() - $stamp;
1342
1343 if ($stamp_delta > 1800) {
1344 $stamp_check = 0;
1345 } else {
1346 $stamp_check = 1;
1347 $_SESSION["daemon_stamp_check"] = time();
1348 }
1349
1350 $data['daemon_stamp_ok'] = $stamp_check;
1351
1352 $stamp_fmt = date("Y.m.d, G:i", $stamp);
1353
1354 $data['daemon_stamp'] = $stamp_fmt;
1355 }
1356 }
1357 }
1358
1359 return $data;
1360 }
1361
1362 function search_to_sql($search, $search_language) {
1363
1364 $keywords = str_getcsv(trim($search), " ");
1365 $query_keywords = array();
1366 $search_words = array();
1367 $search_query_leftover = array();
1368
1369 if ($search_language)
1370 $search_language = db_escape_string(mb_strtolower($search_language));
1371 else
1372 $search_language = "english";
1373
1374 foreach ($keywords as $k) {
1375 if (strpos($k, "-") === 0) {
1376 $k = substr($k, 1);
1377 $not = "NOT";
1378 } else {
1379 $not = "";
1380 }
1381
1382 $commandpair = explode(":", mb_strtolower($k), 2);
1383
1384 switch ($commandpair[0]) {
1385 case "title":
1386 if ($commandpair[1]) {
1387 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE '%".
1388 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1389 } else {
1390 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1391 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1392 array_push($search_words, $k);
1393 }
1394 break;
1395 case "author":
1396 if ($commandpair[1]) {
1397 array_push($query_keywords, "($not (LOWER(author) LIKE '%".
1398 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1399 } else {
1400 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1401 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1402 array_push($search_words, $k);
1403 }
1404 break;
1405 case "note":
1406 if ($commandpair[1]) {
1407 if ($commandpair[1] == "true")
1408 array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1409 else if ($commandpair[1] == "false")
1410 array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1411 else
1412 array_push($query_keywords, "($not (LOWER(note) LIKE '%".
1413 db_escape_string(mb_strtolower($commandpair[1]))."%'))");
1414 } else {
1415 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1416 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1417 if (!$not) array_push($search_words, $k);
1418 }
1419 break;
1420 case "star":
1421
1422 if ($commandpair[1]) {
1423 if ($commandpair[1] == "true")
1424 array_push($query_keywords, "($not (marked = true))");
1425 else
1426 array_push($query_keywords, "($not (marked = false))");
1427 } else {
1428 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1429 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1430 if (!$not) array_push($search_words, $k);
1431 }
1432 break;
1433 case "pub":
1434 if ($commandpair[1]) {
1435 if ($commandpair[1] == "true")
1436 array_push($query_keywords, "($not (published = true))");
1437 else
1438 array_push($query_keywords, "($not (published = false))");
1439
1440 } else {
1441 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1442 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1443 if (!$not) array_push($search_words, $k);
1444 }
1445 break;
1446 case "unread":
1447 if ($commandpair[1]) {
1448 if ($commandpair[1] == "true")
1449 array_push($query_keywords, "($not (unread = true))");
1450 else
1451 array_push($query_keywords, "($not (unread = false))");
1452
1453 } else {
1454 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1455 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1456 if (!$not) array_push($search_words, $k);
1457 }
1458 break;
1459 default:
1460 if (strpos($k, "@") === 0) {
1461
1462 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1463 $orig_ts = strtotime(substr($k, 1));
1464 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1465
1466 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1467
1468 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
1469 } else {
1470
1471 if (DB_TYPE == "pgsql") {
1472 $k = mb_strtolower($k);
1473 array_push($search_query_leftover, $not ? "!$k" : $k);
1474 } else {
1475 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1476 OR UPPER(ttrss_entries.content) $not LIKE UPPER('%$k%'))");
1477 }
1478
1479 if (!$not) array_push($search_words, $k);
1480 }
1481 }
1482 }
1483
1484 if (count($search_query_leftover) > 0) {
1485 $search_query_leftover = db_escape_string(implode(" & ", $search_query_leftover));
1486
1487 if (DB_TYPE == "pgsql") {
1488 array_push($query_keywords,
1489 "(tsvector_combined @@ to_tsquery('$search_language', '$search_query_leftover'))");
1490 }
1491
1492 }
1493
1494 $search_query_part = implode("AND", $query_keywords);
1495
1496 return array($search_query_part, $search_words);
1497 }
1498
1499 function iframe_whitelisted($entry) {
1500 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1501
1502 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1503
1504 if ($src) {
1505 foreach ($whitelist as $w) {
1506 if ($src == $w || $src == "www.$w")
1507 return true;
1508 }
1509 }
1510
1511 return false;
1512 }
1513
1514 function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1515 if (!$owner) $owner = $_SESSION["uid"];
1516
1517 $res = trim($str); if (!$res) return '';
1518
1519 $charset_hack = '<head>
1520 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1521 </head>';
1522
1523 $res = trim($res); if (!$res) return '';
1524
1525 libxml_use_internal_errors(true);
1526
1527 $doc = new DOMDocument();
1528 $doc->loadHTML($charset_hack . $res);
1529 $xpath = new DOMXPath($doc);
1530
1531 $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
1532
1533 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1534
1535 foreach ($entries as $entry) {
1536
1537 if ($entry->hasAttribute('href')) {
1538 $entry->setAttribute('href',
1539 rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1540
1541 $entry->setAttribute('rel', 'noopener noreferrer');
1542 }
1543
1544 if ($entry->hasAttribute('src')) {
1545 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1546 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
1547
1548 if (file_exists($cached_filename)) {
1549
1550 // this is strictly cosmetic
1551 if ($entry->tagName == 'img') {
1552 $suffix = ".png";
1553 } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
1554 $suffix = ".mp4";
1555 } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
1556 $suffix = ".ogg";
1557 } else {
1558 $suffix = "";
1559 }
1560
1561 $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1562
1563 if ($entry->hasAttribute('srcset')) {
1564 $entry->removeAttribute('srcset');
1565 }
1566
1567 if ($entry->hasAttribute('sizes')) {
1568 $entry->removeAttribute('sizes');
1569 }
1570 }
1571
1572 $entry->setAttribute('src', $src);
1573 }
1574
1575 if ($entry->nodeName == 'img') {
1576
1577 if ($entry->hasAttribute('src')) {
1578 $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1579
1580 if (is_prefix_https() && !$is_https_url) {
1581
1582 if ($entry->hasAttribute('srcset')) {
1583 $entry->removeAttribute('srcset');
1584 }
1585
1586 if ($entry->hasAttribute('sizes')) {
1587 $entry->removeAttribute('sizes');
1588 }
1589 }
1590 }
1591
1592 if (($owner && get_pref("STRIP_IMAGES", $owner)) ||
1593 $force_remove_images || $_SESSION["bw_limit"]) {
1594
1595 $p = $doc->createElement('p');
1596
1597 $a = $doc->createElement('a');
1598 $a->setAttribute('href', $entry->getAttribute('src'));
1599
1600 $a->appendChild(new DOMText($entry->getAttribute('src')));
1601 $a->setAttribute('target', '_blank');
1602 $a->setAttribute('rel', 'noopener noreferrer');
1603
1604 $p->appendChild($a);
1605
1606 $entry->parentNode->replaceChild($p, $entry);
1607 }
1608 }
1609
1610 if (strtolower($entry->nodeName) == "a") {
1611 $entry->setAttribute("target", "_blank");
1612 $entry->setAttribute("rel", "noopener noreferrer");
1613 }
1614 }
1615
1616 $entries = $xpath->query('//iframe');
1617 foreach ($entries as $entry) {
1618 if (!iframe_whitelisted($entry)) {
1619 $entry->setAttribute('sandbox', 'allow-scripts');
1620 } else {
1621 if (is_prefix_https()) {
1622 $entry->setAttribute("src",
1623 str_replace("http://", "https://",
1624 $entry->getAttribute("src")));
1625 }
1626 }
1627 }
1628
1629 $allowed_elements = array('a', 'address', 'acronym', 'audio', 'article', 'aside',
1630 'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1631 'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1632 'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1633 'dt', 'em', 'footer', 'figure', 'figcaption',
1634 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'html', 'i',
1635 'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1636 'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1637 'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1638 'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1639 'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1640
1641 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1642
1643 $disallowed_attributes = array('id', 'style', 'class');
1644
1645 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1646 $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1647 if (is_array($retval)) {
1648 $doc = $retval[0];
1649 $allowed_elements = $retval[1];
1650 $disallowed_attributes = $retval[2];
1651 } else {
1652 $doc = $retval;
1653 }
1654 }
1655
1656 $doc->removeChild($doc->firstChild); //remove doctype
1657 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1658
1659 if ($highlight_words) {
1660 foreach ($highlight_words as $word) {
1661
1662 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1663
1664 $elements = $xpath->query("//*/text()");
1665
1666 foreach ($elements as $child) {
1667
1668 $fragment = $doc->createDocumentFragment();
1669 $text = $child->textContent;
1670
1671 while (($pos = mb_stripos($text, $word)) !== false) {
1672 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1673 $word = mb_substr($text, $pos, mb_strlen($word));
1674 $highlight = $doc->createElement('span');
1675 $highlight->appendChild(new DomText($word));
1676 $highlight->setAttribute('class', 'highlight');
1677 $fragment->appendChild($highlight);
1678 $text = mb_substr($text, $pos + mb_strlen($word));
1679 }
1680
1681 if (!empty($text)) $fragment->appendChild(new DomText($text));
1682
1683 $child->parentNode->replaceChild($fragment, $child);
1684 }
1685 }
1686 }
1687
1688 $res = $doc->saveHTML();
1689
1690 /* strip everything outside of <body>...</body> */
1691
1692 $res_frag = array();
1693 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1694 return $res_frag[1];
1695 } else {
1696 return $res;
1697 }
1698 }
1699
1700 function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1701 $xpath = new DOMXPath($doc);
1702 $entries = $xpath->query('//*');
1703
1704 foreach ($entries as $entry) {
1705 if (!in_array($entry->nodeName, $allowed_elements)) {
1706 $entry->parentNode->removeChild($entry);
1707 }
1708
1709 if ($entry->hasAttributes()) {
1710 $attrs_to_remove = array();
1711
1712 foreach ($entry->attributes as $attr) {
1713
1714 if (strpos($attr->nodeName, 'on') === 0) {
1715 array_push($attrs_to_remove, $attr);
1716 }
1717
1718 if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1719 array_push($attrs_to_remove, $attr);
1720 }
1721
1722 if (in_array($attr->nodeName, $disallowed_attributes)) {
1723 array_push($attrs_to_remove, $attr);
1724 }
1725 }
1726
1727 foreach ($attrs_to_remove as $attr) {
1728 $entry->removeAttributeNode($attr);
1729 }
1730 }
1731 }
1732
1733 return $doc;
1734 }
1735
1736 function trim_array($array) {
1737 $tmp = $array;
1738 array_walk($tmp, 'trim');
1739 return $tmp;
1740 }
1741
1742 function tag_is_valid($tag) {
1743 if ($tag == '') return false;
1744 if (is_numeric($tag)) return false;
1745 if (mb_strlen($tag) > 250) return false;
1746
1747 if (!$tag) return false;
1748
1749 return true;
1750 }
1751
1752 function render_login_form() {
1753 header('Cache-Control: public');
1754
1755 require_once "login_form.php";
1756 exit;
1757 }
1758
1759 function T_sprintf() {
1760 $args = func_get_args();
1761 return vsprintf(__(array_shift($args)), $args);
1762 }
1763
1764 function print_checkpoint($n, $s) {
1765 $ts = microtime(true);
1766 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1767 return $ts;
1768 }
1769
1770 function sanitize_tag($tag) {
1771 $tag = trim($tag);
1772
1773 $tag = mb_strtolower($tag, 'utf-8');
1774
1775 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1776
1777 if (DB_TYPE == "mysql") {
1778 $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1779 }
1780
1781 return $tag;
1782 }
1783
1784 function is_server_https() {
1785 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1786 }
1787
1788 function is_prefix_https() {
1789 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1790 }
1791
1792 // this returns SELF_URL_PATH sans ending slash
1793 function get_self_url_prefix() {
1794 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1795 return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1796 } else {
1797 return SELF_URL_PATH;
1798 }
1799 }
1800
1801 function encrypt_password($pass, $salt = '', $mode2 = false) {
1802 if ($salt && $mode2) {
1803 return "MODE2:" . hash('sha256', $salt . $pass);
1804 } else if ($salt) {
1805 return "SHA1X:" . sha1("$salt:$pass");
1806 } else {
1807 return "SHA1:" . sha1($pass);
1808 }
1809 } // function encrypt_password
1810
1811 function load_filters($feed_id, $owner_uid) {
1812 $filters = array();
1813
1814 $cat_id = (int)Feeds::getFeedCategory($feed_id);
1815
1816 if ($cat_id == 0)
1817 $null_cat_qpart = "cat_id IS NULL OR";
1818 else
1819 $null_cat_qpart = "";
1820
1821 $result = db_query("SELECT * FROM ttrss_filters2 WHERE
1822 owner_uid = $owner_uid AND enabled = true ORDER BY order_id, title");
1823
1824 $check_cats = array_merge(
1825 Feeds::getParentCategories($cat_id, $owner_uid),
1826 [$cat_id]);
1827
1828 $check_cats_str = join(",", $check_cats);
1829 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1830
1831 while ($line = db_fetch_assoc($result)) {
1832 $filter_id = $line["id"];
1833
1834 $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1835
1836 $result2 = db_query("SELECT
1837 r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1838 FROM ttrss_filters2_rules AS r,
1839 ttrss_filter_types AS t
1840 WHERE
1841 (match_on IS NOT NULL OR
1842 (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1843 (feed_id IS NULL OR feed_id = '$feed_id'))) AND
1844 filter_type = t.id AND filter_id = '$filter_id'");
1845
1846 $rules = array();
1847 $actions = array();
1848
1849 while ($rule_line = db_fetch_assoc($result2)) {
1850 # print_r($rule_line);
1851
1852 if ($rule_line["match_on"]) {
1853 $match_on = json_decode($rule_line["match_on"], true);
1854
1855 if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1856
1857 $rule = array();
1858 $rule["reg_exp"] = $rule_line["reg_exp"];
1859 $rule["type"] = $rule_line["type_name"];
1860 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1861
1862 array_push($rules, $rule);
1863 } else if (!$match_any_rule) {
1864 // this filter contains a rule that doesn't match to this feed/category combination
1865 // thus filter has to be rejected
1866
1867 $rules = [];
1868 break;
1869 }
1870
1871 } else {
1872
1873 $rule = array();
1874 $rule["reg_exp"] = $rule_line["reg_exp"];
1875 $rule["type"] = $rule_line["type_name"];
1876 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1877
1878 array_push($rules, $rule);
1879 }
1880 }
1881
1882 if (count($rules) > 0) {
1883 $result2 = db_query("SELECT a.action_param,t.name AS type_name
1884 FROM ttrss_filters2_actions AS a,
1885 ttrss_filter_actions AS t
1886 WHERE
1887 action_id = t.id AND filter_id = '$filter_id'");
1888
1889 while ($action_line = db_fetch_assoc($result2)) {
1890 # print_r($action_line);
1891
1892 $action = array();
1893 $action["type"] = $action_line["type_name"];
1894 $action["param"] = $action_line["action_param"];
1895
1896 array_push($actions, $action);
1897 }
1898 }
1899
1900 $filter = array();
1901 $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1902 $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1903 $filter["rules"] = $rules;
1904 $filter["actions"] = $actions;
1905
1906 if (count($rules) > 0 && count($actions) > 0) {
1907 array_push($filters, $filter);
1908 }
1909 }
1910
1911 return $filters;
1912 }
1913
1914 function get_score_pic($score) {
1915 if ($score > 100) {
1916 return "score_high.png";
1917 } else if ($score > 0) {
1918 return "score_half_high.png";
1919 } else if ($score < -100) {
1920 return "score_low.png";
1921 } else if ($score < 0) {
1922 return "score_half_low.png";
1923 } else {
1924 return "score_neutral.png";
1925 }
1926 }
1927
1928 function feed_has_icon($id) {
1929 return is_file(ICONS_DIR . "/$id.ico") && filesize(ICONS_DIR . "/$id.ico") > 0;
1930 }
1931
1932 function init_plugins() {
1933 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1934
1935 return true;
1936 }
1937
1938 function add_feed_category($feed_cat, $parent_cat_id = false) {
1939
1940 if (!$feed_cat) return false;
1941
1942 db_query("BEGIN");
1943
1944 if ($parent_cat_id) {
1945 $parent_qpart = "parent_cat = '$parent_cat_id'";
1946 $parent_insert = "'$parent_cat_id'";
1947 } else {
1948 $parent_qpart = "parent_cat IS NULL";
1949 $parent_insert = "NULL";
1950 }
1951
1952 $feed_cat = mb_substr($feed_cat, 0, 250);
1953
1954 $result = db_query(
1955 "SELECT id FROM ttrss_feed_categories
1956 WHERE $parent_qpart AND title = '$feed_cat' AND owner_uid = ".$_SESSION["uid"]);
1957
1958 if (db_num_rows($result) == 0) {
1959
1960 $result = db_query(
1961 "INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
1962 VALUES ('".$_SESSION["uid"]."', '$feed_cat', $parent_insert)");
1963
1964 db_query("COMMIT");
1965
1966 return true;
1967 }
1968
1969 return false;
1970 }
1971
1972 /**
1973 * Fixes incomplete URLs by prepending "http://".
1974 * Also replaces feed:// with http://, and
1975 * prepends a trailing slash if the url is a domain name only.
1976 *
1977 * @param string $url Possibly incomplete URL
1978 *
1979 * @return string Fixed URL.
1980 */
1981 function fix_url($url) {
1982
1983 // support schema-less urls
1984 if (strpos($url, '//') === 0) {
1985 $url = 'https:' . $url;
1986 }
1987
1988 if (strpos($url, '://') === false) {
1989 $url = 'http://' . $url;
1990 } else if (substr($url, 0, 5) == 'feed:') {
1991 $url = 'http:' . substr($url, 5);
1992 }
1993
1994 //prepend slash if the URL has no slash in it
1995 // "http://www.example" -> "http://www.example/"
1996 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
1997 $url .= '/';
1998 }
1999
2000 //convert IDNA hostname to punycode if possible
2001 if (function_exists("idn_to_ascii")) {
2002 $parts = parse_url($url);
2003 if (mb_detect_encoding($parts['host']) != 'ASCII')
2004 {
2005 $parts['host'] = idn_to_ascii($parts['host']);
2006 $url = build_url($parts);
2007 }
2008 }
2009
2010 if ($url != "http:///")
2011 return $url;
2012 else
2013 return '';
2014 }
2015
2016 function validate_feed_url($url) {
2017 $parts = parse_url($url);
2018
2019 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2020
2021 }
2022
2023 /* function save_email_address($email) {
2024 // FIXME: implement persistent storage of emails
2025
2026 if (!$_SESSION['stored_emails'])
2027 $_SESSION['stored_emails'] = array();
2028
2029 if (!in_array($email, $_SESSION['stored_emails']))
2030 array_push($_SESSION['stored_emails'], $email);
2031 } */
2032
2033
2034 function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2035
2036 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2037
2038 $sql_is_cat = bool_to_sql_bool($is_cat);
2039
2040 $result = db_query("SELECT access_key FROM ttrss_access_keys
2041 WHERE feed_id = '$feed_id' AND is_cat = $sql_is_cat
2042 AND owner_uid = " . $owner_uid);
2043
2044 if (db_num_rows($result) == 1) {
2045 return db_fetch_result($result, 0, "access_key");
2046 } else {
2047 $key = db_escape_string(uniqid_short());
2048
2049 $result = db_query("INSERT INTO ttrss_access_keys
2050 (access_key, feed_id, is_cat, owner_uid)
2051 VALUES ('$key', '$feed_id', $sql_is_cat, '$owner_uid')");
2052
2053 return $key;
2054 }
2055 return false;
2056 }
2057
2058 function get_feeds_from_html($url, $content)
2059 {
2060 $url = fix_url($url);
2061 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2062
2063 libxml_use_internal_errors(true);
2064
2065 $doc = new DOMDocument();
2066 $doc->loadHTML($content);
2067 $xpath = new DOMXPath($doc);
2068 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2069 '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2070 $feedUrls = array();
2071 foreach ($entries as $entry) {
2072 if ($entry->hasAttribute('href')) {
2073 $title = $entry->getAttribute('title');
2074 if ($title == '') {
2075 $title = $entry->getAttribute('type');
2076 }
2077 $feedUrl = rewrite_relative_url(
2078 $baseUrl, $entry->getAttribute('href')
2079 );
2080 $feedUrls[$feedUrl] = $title;
2081 }
2082 }
2083 return $feedUrls;
2084 }
2085
2086 function is_html($content) {
2087 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2088 }
2089
2090 function url_is_html($url, $login = false, $pass = false) {
2091 return is_html(fetch_file_contents($url, false, $login, $pass));
2092 }
2093
2094 function build_url($parts) {
2095 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2096 }
2097
2098 function cleanup_url_path($path) {
2099 $path = str_replace("/./", "/", $path);
2100 $path = str_replace("//", "/", $path);
2101
2102 return $path;
2103 }
2104
2105 /**
2106 * Converts a (possibly) relative URL to a absolute one.
2107 *
2108 * @param string $url Base URL (i.e. from where the document is)
2109 * @param string $rel_url Possibly relative URL in the document
2110 *
2111 * @return string Absolute URL
2112 */
2113 function rewrite_relative_url($url, $rel_url) {
2114 if (strpos($rel_url, "://") !== false) {
2115 return $rel_url;
2116 } else if (strpos($rel_url, "//") === 0) {
2117 # protocol-relative URL (rare but they exist)
2118 return $rel_url;
2119 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2120 # magnet:, feed:, etc
2121 return $rel_url;
2122 } else if (strpos($rel_url, "/") === 0) {
2123 $parts = parse_url($url);
2124 $parts['path'] = $rel_url;
2125 $parts['path'] = cleanup_url_path($parts['path']);
2126
2127 return build_url($parts);
2128
2129 } else {
2130 $parts = parse_url($url);
2131 if (!isset($parts['path'])) {
2132 $parts['path'] = '/';
2133 }
2134 $dir = $parts['path'];
2135 if (substr($dir, -1) !== '/') {
2136 $dir = dirname($parts['path']);
2137 $dir !== '/' && $dir .= '/';
2138 }
2139 $parts['path'] = $dir . $rel_url;
2140 $parts['path'] = cleanup_url_path($parts['path']);
2141
2142 return build_url($parts);
2143 }
2144 }
2145
2146 function cleanup_tags($days = 14, $limit = 1000) {
2147
2148 if (DB_TYPE == "pgsql") {
2149 $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2150 } else if (DB_TYPE == "mysql") {
2151 $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2152 }
2153
2154 $tags_deleted = 0;
2155
2156 while ($limit > 0) {
2157 $limit_part = 500;
2158
2159 $query = "SELECT ttrss_tags.id AS id
2160 FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2161 WHERE post_int_id = int_id AND $interval_query AND
2162 ref_id = ttrss_entries.id AND tag_cache != '' LIMIT $limit_part";
2163
2164 $result = db_query($query);
2165
2166 $ids = array();
2167
2168 while ($line = db_fetch_assoc($result)) {
2169 array_push($ids, $line['id']);
2170 }
2171
2172 if (count($ids) > 0) {
2173 $ids = join(",", $ids);
2174
2175 $tmp_result = db_query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2176 $tags_deleted += db_affected_rows($tmp_result);
2177 } else {
2178 break;
2179 }
2180
2181 $limit -= $limit_part;
2182 }
2183
2184 return $tags_deleted;
2185 }
2186
2187 function print_user_stylesheet() {
2188 $value = get_pref('USER_STYLESHEET');
2189
2190 if ($value) {
2191 print "<style type=\"text/css\">";
2192 print str_replace("<br/>", "\n", $value);
2193 print "</style>";
2194 }
2195
2196 }
2197
2198 function filter_to_sql($filter, $owner_uid) {
2199 $query = array();
2200
2201 if (DB_TYPE == "pgsql")
2202 $reg_qpart = "~";
2203 else
2204 $reg_qpart = "REGEXP";
2205
2206 foreach ($filter["rules"] AS $rule) {
2207 $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2208 $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2209 $rule['reg_exp']) !== FALSE;
2210
2211 if ($regexp_valid) {
2212
2213 $rule['reg_exp'] = db_escape_string($rule['reg_exp']);
2214
2215 switch ($rule["type"]) {
2216 case "title":
2217 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2218 $rule['reg_exp'] . "')";
2219 break;
2220 case "content":
2221 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2222 $rule['reg_exp'] . "')";
2223 break;
2224 case "both":
2225 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2226 $rule['reg_exp'] . "') OR LOWER(" .
2227 "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2228 break;
2229 case "tag":
2230 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2231 $rule['reg_exp'] . "')";
2232 break;
2233 case "link":
2234 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2235 $rule['reg_exp'] . "')";
2236 break;
2237 case "author":
2238 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2239 $rule['reg_exp'] . "')";
2240 break;
2241 }
2242
2243 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2244
2245 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2246 $qpart .= " AND feed_id = " . db_escape_string($rule["feed_id"]);
2247 }
2248
2249 if (isset($rule["cat_id"])) {
2250
2251 if ($rule["cat_id"] > 0) {
2252 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2253 array_push($children, $rule["cat_id"]);
2254
2255 $children = join(",", $children);
2256
2257 $cat_qpart = "cat_id IN ($children)";
2258 } else {
2259 $cat_qpart = "cat_id IS NULL";
2260 }
2261
2262 $qpart .= " AND $cat_qpart";
2263 }
2264
2265 $qpart .= " AND feed_id IS NOT NULL";
2266
2267 array_push($query, "($qpart)");
2268
2269 }
2270 }
2271
2272 if (count($query) > 0) {
2273 $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2274 } else {
2275 $fullquery = "(false)";
2276 }
2277
2278 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2279
2280 return $fullquery;
2281 }
2282
2283 if (!function_exists('gzdecode')) {
2284 function gzdecode($string) { // no support for 2nd argument
2285 return file_get_contents('compress.zlib://data:who/cares;base64,'.
2286 base64_encode($string));
2287 }
2288 }
2289
2290 function get_random_bytes($length) {
2291 if (function_exists('openssl_random_pseudo_bytes')) {
2292 return openssl_random_pseudo_bytes($length);
2293 } else {
2294 $output = "";
2295
2296 for ($i = 0; $i < $length; $i++)
2297 $output .= chr(mt_rand(0, 255));
2298
2299 return $output;
2300 }
2301 }
2302
2303 function read_stdin() {
2304 $fp = fopen("php://stdin", "r");
2305
2306 if ($fp) {
2307 $line = trim(fgets($fp));
2308 fclose($fp);
2309 return $line;
2310 }
2311
2312 return null;
2313 }
2314
2315 function implements_interface($class, $interface) {
2316 return in_array($interface, class_implements($class));
2317 }
2318
2319 function get_minified_js($files) {
2320 require_once 'lib/jshrink/Minifier.php';
2321
2322 $rv = '';
2323
2324 foreach ($files as $js) {
2325 if (!isset($_GET['debug'])) {
2326 $cached_file = CACHE_DIR . "/js/".basename($js).".js";
2327
2328 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js.js")) {
2329
2330 list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2331
2332 if ($header && $contents) {
2333 list($htag, $hversion) = explode(":", $header);
2334
2335 if ($htag == "tt-rss" && $hversion == VERSION) {
2336 $rv .= $contents;
2337 continue;
2338 }
2339 }
2340 }
2341
2342 $minified = JShrink\Minifier::minify(file_get_contents("js/$js.js"));
2343 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2344 $rv .= $minified;
2345
2346 } else {
2347 $rv .= file_get_contents("js/$js.js"); // no cache in debug mode
2348 }
2349 }
2350
2351 return $rv;
2352 }
2353
2354 function calculate_dep_timestamp() {
2355 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2356
2357 $max_ts = -1;
2358
2359 foreach ($files as $file) {
2360 if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2361 }
2362
2363 return $max_ts;
2364 }
2365
2366 function T_js_decl($s1, $s2) {
2367 if ($s1 && $s2) {
2368 $s1 = preg_replace("/\n/", "", $s1);
2369 $s2 = preg_replace("/\n/", "", $s2);
2370
2371 $s1 = preg_replace("/\"/", "\\\"", $s1);
2372 $s2 = preg_replace("/\"/", "\\\"", $s2);
2373
2374 return "T_messages[\"$s1\"] = \"$s2\";\n";
2375 }
2376 }
2377
2378 function init_js_translations() {
2379
2380 print 'var T_messages = new Object();
2381
2382 function __(msg) {
2383 if (T_messages[msg]) {
2384 return T_messages[msg];
2385 } else {
2386 return msg;
2387 }
2388 }
2389
2390 function ngettext(msg1, msg2, n) {
2391 return __((parseInt(n) > 1) ? msg2 : msg1);
2392 }';
2393
2394 $l10n = _get_reader();
2395
2396 for ($i = 0; $i < $l10n->total; $i++) {
2397 $orig = $l10n->get_original_string($i);
2398 if(strpos($orig, "\000") !== FALSE) { // Plural forms
2399 $key = explode(chr(0), $orig);
2400 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2401 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2402 } else {
2403 $translation = __($orig);
2404 print T_js_decl($orig, $translation);
2405 }
2406 }
2407 }
2408
2409 function get_theme_path($theme) {
2410 $check = "themes/$theme";
2411 if (file_exists($check)) return $check;
2412
2413 $check = "themes.local/$theme";
2414 if (file_exists($check)) return $check;
2415 }
2416
2417 function theme_valid($theme) {
2418 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2419
2420 if (in_array($theme, $bundled_themes)) return true;
2421
2422 $file = "themes/" . basename($theme);
2423
2424 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2425
2426 if (file_exists($file) && is_readable($file)) {
2427 $fh = fopen($file, "r");
2428
2429 if ($fh) {
2430 $header = fgets($fh);
2431 fclose($fh);
2432
2433 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2434 }
2435 }
2436
2437 return false;
2438 }
2439
2440 /**
2441 * @SuppressWarnings(unused)
2442 */
2443 function error_json($code) {
2444 require_once "errors.php";
2445
2446 @$message = $ERRORS[$code];
2447
2448 return json_encode(array("error" =>
2449 array("code" => $code, "message" => $message)));
2450
2451 }
2452
2453 /*function abs_to_rel_path($dir) {
2454 $tmp = str_replace(dirname(__DIR__), "", $dir);
2455
2456 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2457
2458 return $tmp;
2459 }*/
2460
2461 function get_upload_error_message($code) {
2462
2463 $errors = array(
2464 0 => __('There is no error, the file uploaded with success'),
2465 1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2466 2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2467 3 => __('The uploaded file was only partially uploaded'),
2468 4 => __('No file was uploaded'),
2469 6 => __('Missing a temporary folder'),
2470 7 => __('Failed to write file to disk.'),
2471 8 => __('A PHP extension stopped the file upload.'),
2472 );
2473
2474 return $errors[$code];
2475 }
2476
2477 function base64_img($filename) {
2478 if (file_exists($filename)) {
2479 $ext = pathinfo($filename, PATHINFO_EXTENSION);
2480
2481 return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2482 } else {
2483 return "";
2484 }
2485 }
2486