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