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