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