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