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