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