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