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