]> git.wh0rd.org Git - tt-rss.git/blob - include/functions.php
bring back (debloated) version of unexpanded combined mode
[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_cdm_expanded" => __("Toggle auto expand in combined mode"),
1168                                 "toggle_combined_mode" => __("Toggle combined mode")),
1169                         __("Go to") => array(
1170                                 "goto_all" => __("All articles"),
1171                                 "goto_fresh" => __("Fresh"),
1172                                 "goto_marked" => __("Starred"),
1173                                 "goto_published" => __("Published"),
1174                                 "goto_tagcloud" => __("Tag cloud"),
1175                                 "goto_prefs" => __("Preferences")),
1176                         __("Other") => array(
1177                                 "create_label" => __("Create label"),
1178                                 "create_filter" => __("Create filter"),
1179                                 "collapse_sidebar" => __("Un/collapse sidebar"),
1180                                 "help_dialog" => __("Show help dialog"))
1181                 );
1182
1183                 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1184                         $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1185                 }
1186
1187                 return $hotkeys;
1188         }
1189
1190         function get_hotkeys_map() {
1191                 $hotkeys = array(
1192         //                      "navigation" => array(
1193                         "k" => "next_feed",
1194                         "j" => "prev_feed",
1195                         "n" => "next_article",
1196                         "p" => "prev_article",
1197                         "(38)|up" => "prev_article",
1198                         "(40)|down" => "next_article",
1199         //                              "^(38)|Ctrl-up" => "prev_article_noscroll",
1200         //                              "^(40)|Ctrl-down" => "next_article_noscroll",
1201                         "(191)|/" => "search_dialog",
1202         //                      "article" => array(
1203                         "s" => "toggle_mark",
1204                         "*s" => "toggle_publ",
1205                         "u" => "toggle_unread",
1206                         "*t" => "edit_tags",
1207                         "o" => "open_in_new_window",
1208                         "c p" => "catchup_below",
1209                         "c n" => "catchup_above",
1210                         "*n" => "article_scroll_down",
1211                         "*p" => "article_scroll_up",
1212                         "*(38)|Shift+up" => "article_scroll_up",
1213                         "*(40)|Shift+down" => "article_scroll_down",
1214                         "a *w" => "toggle_widescreen",
1215                         "a e" => "toggle_embed_original",
1216                         "e" => "email_article",
1217                         "a q" => "close_article",
1218         //                      "article_selection" => array(
1219                         "a a" => "select_all",
1220                         "a u" => "select_unread",
1221                         "a *u" => "select_marked",
1222                         "a p" => "select_published",
1223                         "a i" => "select_invert",
1224                         "a n" => "select_none",
1225         //                      "feed" => array(
1226                         "f r" => "feed_refresh",
1227                         "f a" => "feed_unhide_read",
1228                         "f s" => "feed_subscribe",
1229                         "f e" => "feed_edit",
1230                         "f q" => "feed_catchup",
1231                         "f x" => "feed_reverse",
1232                         "f g" => "feed_toggle_vgroup",
1233                         "f *d" => "feed_debug_update",
1234                         "f *g" => "feed_debug_viewfeed",
1235                         "f *c" => "toggle_combined_mode",
1236                         "f c" => "toggle_cdm_expanded",
1237                         "*q" => "catchup_all",
1238                         "x" => "cat_toggle_collapse",
1239         //                      "goto" => array(
1240                         "g a" => "goto_all",
1241                         "g f" => "goto_fresh",
1242                         "g s" => "goto_marked",
1243                         "g p" => "goto_published",
1244                         "g t" => "goto_tagcloud",
1245                         "g *p" => "goto_prefs",
1246         //                      "other" => array(
1247                         "(9)|Tab" => "select_article_cursor", // tab
1248                         "c l" => "create_label",
1249                         "c f" => "create_filter",
1250                         "c s" => "collapse_sidebar",
1251                         "^(191)|Ctrl+/" => "help_dialog",
1252                 );
1253
1254                 $hotkeys["^(38)|Ctrl-up"] = "prev_article_noscroll";
1255                 $hotkeys["^(40)|Ctrl-down"] = "next_article_noscroll";
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                 $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1312
1313                 $data['dep_ts'] = calculate_dep_timestamp();
1314                 $data['reload_on_ts_change'] = !defined('_NO_RELOAD_ON_TS_CHANGE');
1315
1316                 $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1317
1318                 if (CHECK_FOR_UPDATES && !$disable_update_check && $_SESSION["last_version_check"] + 86400 + rand(-1000, 1000) < time()) {
1319                         $update_result = @check_for_update();
1320
1321                         $data["update_result"] = $update_result;
1322
1323                         $_SESSION["last_version_check"] = time();
1324                 }
1325
1326                 if (file_exists(LOCK_DIRECTORY . "/update_daemon.lock")) {
1327
1328                         $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1329
1330                         if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1331
1332                                 $stamp = (int) @file_get_contents(LOCK_DIRECTORY . "/update_daemon.stamp");
1333
1334                                 if ($stamp) {
1335                                         $stamp_delta = time() - $stamp;
1336
1337                                         if ($stamp_delta > 1800) {
1338                                                 $stamp_check = 0;
1339                                         } else {
1340                                                 $stamp_check = 1;
1341                                                 $_SESSION["daemon_stamp_check"] = time();
1342                                         }
1343
1344                                         $data['daemon_stamp_ok'] = $stamp_check;
1345
1346                                         $stamp_fmt = date("Y.m.d, G:i", $stamp);
1347
1348                                         $data['daemon_stamp'] = $stamp_fmt;
1349                                 }
1350                         }
1351                 }
1352
1353                 return $data;
1354         }
1355
1356         function search_to_sql($search, $search_language) {
1357
1358                 $keywords = str_getcsv(trim($search), " ");
1359                 $query_keywords = array();
1360                 $search_words = array();
1361                 $search_query_leftover = array();
1362
1363                 $pdo = Db::pdo();
1364
1365                 if ($search_language)
1366                         $search_language = $pdo->quote(mb_strtolower($search_language));
1367                 else
1368                         $search_language = $pdo->quote("english");
1369
1370                 foreach ($keywords as $k) {
1371                         if (strpos($k, "-") === 0) {
1372                                 $k = substr($k, 1);
1373                                 $not = "NOT";
1374                         } else {
1375                                 $not = "";
1376                         }
1377
1378                         $commandpair = explode(":", mb_strtolower($k), 2);
1379
1380                         switch ($commandpair[0]) {
1381                                 case "title":
1382                                         if ($commandpair[1]) {
1383                                                 array_push($query_keywords, "($not (LOWER(ttrss_entries.title) LIKE ".
1384                                                         $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%') ."))");
1385                                         } else {
1386                                                 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1387                                                                 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1388                                                 array_push($search_words, $k);
1389                                         }
1390                                         break;
1391                                 case "author":
1392                                         if ($commandpair[1]) {
1393                                                 array_push($query_keywords, "($not (LOWER(author) LIKE ".
1394                                                         $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1395                                         } else {
1396                                                 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1397                                                                 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1398                                                 array_push($search_words, $k);
1399                                         }
1400                                         break;
1401                                 case "note":
1402                                         if ($commandpair[1]) {
1403                                                 if ($commandpair[1] == "true")
1404                                                         array_push($query_keywords, "($not (note IS NOT NULL AND note != ''))");
1405                                                 else if ($commandpair[1] == "false")
1406                                                         array_push($query_keywords, "($not (note IS NULL OR note = ''))");
1407                                                 else
1408                                                         array_push($query_keywords, "($not (LOWER(note) LIKE ".
1409                                                                 $pdo->quote('%' . mb_strtolower($commandpair[1]) . '%')."))");
1410                                         } else {
1411                                                 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1412                                                                 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1413                                                 if (!$not) array_push($search_words, $k);
1414                                         }
1415                                         break;
1416                                 case "star":
1417
1418                                         if ($commandpair[1]) {
1419                                                 if ($commandpair[1] == "true")
1420                                                         array_push($query_keywords, "($not (marked = true))");
1421                                                 else
1422                                                         array_push($query_keywords, "($not (marked = false))");
1423                                         } else {
1424                                                 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1425                                                                 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1426                                                 if (!$not) array_push($search_words, $k);
1427                                         }
1428                                         break;
1429                                 case "pub":
1430                                         if ($commandpair[1]) {
1431                                                 if ($commandpair[1] == "true")
1432                                                         array_push($query_keywords, "($not (published = true))");
1433                                                 else
1434                                                         array_push($query_keywords, "($not (published = false))");
1435
1436                                         } else {
1437                                                 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER('%$k%')
1438                                                                 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1439                                                 if (!$not) array_push($search_words, $k);
1440                                         }
1441                                         break;
1442                                 case "unread":
1443                                         if ($commandpair[1]) {
1444                                                 if ($commandpair[1] == "true")
1445                                                         array_push($query_keywords, "($not (unread = true))");
1446                                                 else
1447                                                         array_push($query_keywords, "($not (unread = false))");
1448
1449                                         } else {
1450                                                 array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1451                                                                 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1452                                                 if (!$not) array_push($search_words, $k);
1453                                         }
1454                                         break;
1455                                 default:
1456                                         if (strpos($k, "@") === 0) {
1457
1458                                                 $user_tz_string = get_pref('USER_TIMEZONE', $_SESSION['uid']);
1459                                                 $orig_ts = strtotime(substr($k, 1));
1460                                                 $k = date("Y-m-d", convert_timestamp($orig_ts, $user_tz_string, 'UTC'));
1461
1462                                                 //$k = date("Y-m-d", strtotime(substr($k, 1)));
1463
1464                                                 array_push($query_keywords, "(".SUBSTRING_FOR_DATE."(updated,1,LENGTH('$k')) $not = '$k')");
1465                                         } else {
1466
1467                                                 if (DB_TYPE == "pgsql") {
1468                                                         $k = mb_strtolower($k);
1469                                                         array_push($search_query_leftover, $not ? "!$k" : $k);
1470                                                 } else {
1471                                                         array_push($query_keywords, "(UPPER(ttrss_entries.title) $not LIKE UPPER(".$pdo->quote("%$k%").")
1472                                                                 OR UPPER(ttrss_entries.content) $not LIKE UPPER(".$pdo->quote("%$k%")."))");
1473                                                 }
1474
1475                                                 if (!$not) array_push($search_words, $k);
1476                                         }
1477                         }
1478                 }
1479
1480                 if (count($search_query_leftover) > 0) {
1481                         $search_query_leftover = $pdo->quote(implode(" & ", $search_query_leftover));
1482
1483                         if (DB_TYPE == "pgsql") {
1484                                 array_push($query_keywords,
1485                                         "(tsvector_combined @@ to_tsquery($search_language, $search_query_leftover))");
1486                         }
1487
1488                 }
1489
1490                 $search_query_part = implode("AND", $query_keywords);
1491
1492                 return array($search_query_part, $search_words);
1493         }
1494
1495         function iframe_whitelisted($entry) {
1496                 $whitelist = array("youtube.com", "youtu.be", "vimeo.com", "player.vimeo.com");
1497
1498                 @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1499
1500                 if ($src) {
1501                         foreach ($whitelist as $w) {
1502                                 if ($src == $w || $src == "www.$w")
1503                                         return true;
1504                         }
1505                 }
1506
1507                 return false;
1508         }
1509
1510         // check for locally cached (media) URLs and rewrite to local versions
1511         // this is called separately after sanitize() and plugin render article hooks to allow
1512         // plugins work on original source URLs used before caching
1513
1514         function rewrite_cached_urls($str) {
1515                 $charset_hack = '<head>
1516                                 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1517                         </head>';
1518
1519                 $res = trim($str); if (!$res) return '';
1520
1521                 $doc = new DOMDocument();
1522                 $doc->loadHTML($charset_hack . $res);
1523                 $xpath = new DOMXPath($doc);
1524
1525                 $entries = $xpath->query('(//img[@src]|//video[@poster]|//video/source[@src]|//audio/source[@src])');
1526
1527                 $need_saving = false;
1528
1529                 foreach ($entries as $entry) {
1530
1531                         if ($entry->hasAttribute('src') || $entry->hasAttribute('poster')) {
1532
1533                                 // should be already absolutized because this is called after sanitize()
1534                                 $src = $entry->hasAttribute('poster') ? $entry->getAttribute('poster') : $entry->getAttribute('src');
1535                                 $cached_filename = CACHE_DIR . '/images/' . sha1($src);
1536
1537                                 if (file_exists($cached_filename)) {
1538
1539                                         // this is strictly cosmetic
1540                                         if ($entry->tagName == 'img') {
1541                                                 $suffix = ".png";
1542                                         } else if ($entry->parentNode && $entry->parentNode->tagName == "video") {
1543                                                 $suffix = ".mp4";
1544                                         } else if ($entry->parentNode && $entry->parentNode->tagName == "audio") {
1545                                                 $suffix = ".ogg";
1546                                         } else {
1547                                                 $suffix = "";
1548                                         }
1549
1550                                         $src = get_self_url_prefix() . '/public.php?op=cached_url&hash=' . sha1($src) . $suffix;
1551
1552                                         if ($entry->hasAttribute('poster'))
1553                                                 $entry->setAttribute('poster', $src);
1554                                         else
1555                                                 $entry->setAttribute('src', $src);
1556
1557                                         $need_saving = true;
1558                                 }
1559                         }
1560                 }
1561
1562                 if ($need_saving) {
1563                         $doc->removeChild($doc->firstChild); //remove doctype
1564                         $res = $doc->saveHTML();
1565                 }
1566
1567                 return $res;
1568         }
1569
1570         function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1571                 if (!$owner) $owner = $_SESSION["uid"];
1572
1573                 $res = trim($str); if (!$res) return '';
1574
1575                 $charset_hack = '<head>
1576                                 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
1577                         </head>';
1578
1579                 $res = trim($res); if (!$res) return '';
1580
1581                 libxml_use_internal_errors(true);
1582
1583                 $doc = new DOMDocument();
1584                 $doc->loadHTML($charset_hack . $res);
1585                 $xpath = new DOMXPath($doc);
1586
1587                 $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
1588
1589                 $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src])');
1590
1591                 foreach ($entries as $entry) {
1592
1593                         if ($entry->hasAttribute('href')) {
1594                                 $entry->setAttribute('href',
1595                                         rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1596
1597                                 $entry->setAttribute('rel', 'noopener noreferrer');
1598                         }
1599
1600                         if ($entry->hasAttribute('src')) {
1601                                 $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1602
1603                                 // cache stuff has gone to rewrite_cached_urls()
1604
1605                                 $entry->setAttribute('src', $src);
1606                         }
1607
1608                         if ($entry->nodeName == 'img') {
1609                                 $entry->setAttribute('referrerpolicy', 'no-referrer');
1610
1611                                 $entry->removeAttribute('width');
1612                                 $entry->removeAttribute('height');
1613
1614                                 if ($entry->hasAttribute('src')) {
1615                                         $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1616
1617                                         if (is_prefix_https() && !$is_https_url) {
1618
1619                                                 if ($entry->hasAttribute('srcset')) {
1620                                                         $entry->removeAttribute('srcset');
1621                                                 }
1622
1623                                                 if ($entry->hasAttribute('sizes')) {
1624                                                         $entry->removeAttribute('sizes');
1625                                                 }
1626                                         }
1627                                 }
1628                         }
1629
1630                         if ($entry->hasAttribute('src') &&
1631                                         ($owner && get_pref("STRIP_IMAGES", $owner)) || $force_remove_images || $_SESSION["bw_limit"]) {
1632
1633                                 $p = $doc->createElement('p');
1634
1635                                 $a = $doc->createElement('a');
1636                                 $a->setAttribute('href', $entry->getAttribute('src'));
1637
1638                                 $a->appendChild(new DOMText($entry->getAttribute('src')));
1639                                 $a->setAttribute('target', '_blank');
1640                                 $a->setAttribute('rel', 'noopener noreferrer');
1641
1642                                 $p->appendChild($a);
1643
1644                                 if ($entry->nodeName == 'source') {
1645
1646                                         if ($entry->parentNode && $entry->parentNode->parentNode)
1647                                                 $entry->parentNode->parentNode->replaceChild($p, $entry->parentNode);
1648
1649                                 } else if ($entry->nodeName == 'img') {
1650
1651                                         if ($entry->parentNode)
1652                                                 $entry->parentNode->replaceChild($p, $entry);
1653
1654                                 }
1655                         }
1656
1657                         if (strtolower($entry->nodeName) == "a") {
1658                                 $entry->setAttribute("target", "_blank");
1659                                 $entry->setAttribute("rel", "noopener noreferrer");
1660                         }
1661                 }
1662
1663                 $entries = $xpath->query('//iframe');
1664                 foreach ($entries as $entry) {
1665                         if (!iframe_whitelisted($entry)) {
1666                                 $entry->setAttribute('sandbox', 'allow-scripts');
1667                         } else {
1668                                 if (is_prefix_https()) {
1669                                         $entry->setAttribute("src",
1670                                                 str_replace("http://", "https://",
1671                                                         $entry->getAttribute("src")));
1672                                 }
1673                         }
1674                 }
1675
1676                 $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1677                         'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1678                         'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1679                         'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1680                         'dt', 'em', 'footer', 'figure', 'figcaption',
1681                         'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1682                         'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1683                         'ol', 'p', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1684                         'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1685                         'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1686                         'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace' );
1687
1688                 if ($_SESSION['hasSandbox']) $allowed_elements[] = 'iframe';
1689
1690                 $disallowed_attributes = array('id', 'style', 'class');
1691
1692                 foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1693                         $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1694                         if (is_array($retval)) {
1695                                 $doc = $retval[0];
1696                                 $allowed_elements = $retval[1];
1697                                 $disallowed_attributes = $retval[2];
1698                         } else {
1699                                 $doc = $retval;
1700                         }
1701                 }
1702
1703                 $doc->removeChild($doc->firstChild); //remove doctype
1704                 $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1705
1706                 if ($highlight_words) {
1707                         foreach ($highlight_words as $word) {
1708
1709                                 // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1710
1711                                 $elements = $xpath->query("//*/text()");
1712
1713                                 foreach ($elements as $child) {
1714
1715                                         $fragment = $doc->createDocumentFragment();
1716                                         $text = $child->textContent;
1717
1718                                         while (($pos = mb_stripos($text, $word)) !== false) {
1719                                                 $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1720                                                 $word = mb_substr($text, $pos, mb_strlen($word));
1721                                                 $highlight = $doc->createElement('span');
1722                                                 $highlight->appendChild(new DomText($word));
1723                                                 $highlight->setAttribute('class', 'highlight');
1724                                                 $fragment->appendChild($highlight);
1725                                                 $text = mb_substr($text, $pos + mb_strlen($word));
1726                                         }
1727
1728                                         if (!empty($text)) $fragment->appendChild(new DomText($text));
1729
1730                                         $child->parentNode->replaceChild($fragment, $child);
1731                                 }
1732                         }
1733                 }
1734
1735                 $res = $doc->saveHTML();
1736
1737                 /* strip everything outside of <body>...</body> */
1738
1739                 $res_frag = array();
1740                 if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1741                         return $res_frag[1];
1742                 } else {
1743                         return $res;
1744                 }
1745         }
1746
1747         function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1748                 $xpath = new DOMXPath($doc);
1749                 $entries = $xpath->query('//*');
1750
1751                 foreach ($entries as $entry) {
1752                         if (!in_array($entry->nodeName, $allowed_elements)) {
1753                                 $entry->parentNode->removeChild($entry);
1754                         }
1755
1756                         if ($entry->hasAttributes()) {
1757                                 $attrs_to_remove = array();
1758
1759                                 foreach ($entry->attributes as $attr) {
1760
1761                                         if (strpos($attr->nodeName, 'on') === 0) {
1762                                                 array_push($attrs_to_remove, $attr);
1763                                         }
1764
1765                                         if (strpos($attr->nodeName, "data-") === 0) {
1766                                                 array_push($attrs_to_remove, $attr);
1767                                         }
1768
1769                                         if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1770                                                 array_push($attrs_to_remove, $attr);
1771                                         }
1772
1773                                         if (in_array($attr->nodeName, $disallowed_attributes)) {
1774                                                 array_push($attrs_to_remove, $attr);
1775                                         }
1776                                 }
1777
1778                                 foreach ($attrs_to_remove as $attr) {
1779                                         $entry->removeAttributeNode($attr);
1780                                 }
1781                         }
1782                 }
1783
1784                 return $doc;
1785         }
1786
1787         function trim_array($array) {
1788                 $tmp = $array;
1789                 array_walk($tmp, 'trim');
1790                 return $tmp;
1791         }
1792
1793         function tag_is_valid($tag) {
1794                 if (!$tag || is_numeric($tag) || mb_strlen($tag) > 250)
1795                         return false;
1796
1797                 return true;
1798         }
1799
1800         function render_login_form() {
1801                 header('Cache-Control: public');
1802
1803                 require_once "login_form.php";
1804                 exit;
1805         }
1806
1807         function T_sprintf() {
1808                 $args = func_get_args();
1809                 return vsprintf(__(array_shift($args)), $args);
1810         }
1811
1812         function print_checkpoint($n, $s) {
1813                 $ts = microtime(true);
1814                 echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1815                 return $ts;
1816         }
1817
1818         function sanitize_tag($tag) {
1819                 $tag = trim($tag);
1820
1821                 $tag = mb_strtolower($tag, 'utf-8');
1822
1823                 $tag = preg_replace('/[,\'\"\+\>\<]/', "", $tag);
1824
1825                 if (DB_TYPE == "mysql") {
1826                         $tag = preg_replace('/[\x{10000}-\x{10FFFF}]/u', "\xEF\xBF\xBD", $tag);
1827                 }
1828
1829                 return $tag;
1830         }
1831
1832         function is_server_https() {
1833                 return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1834         }
1835
1836         function is_prefix_https() {
1837                 return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
1838         }
1839
1840         // this returns SELF_URL_PATH sans ending slash
1841         function get_self_url_prefix() {
1842                 if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH)-1) {
1843                         return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH)-1);
1844                 } else {
1845                         return SELF_URL_PATH;
1846                 }
1847         }
1848
1849         function encrypt_password($pass, $salt = '', $mode2 = false) {
1850                 if ($salt && $mode2) {
1851                         return "MODE2:" . hash('sha256', $salt . $pass);
1852                 } else if ($salt) {
1853                         return "SHA1X:" . sha1("$salt:$pass");
1854                 } else {
1855                         return "SHA1:" . sha1($pass);
1856                 }
1857         } // function encrypt_password
1858
1859         function load_filters($feed_id, $owner_uid) {
1860                 $filters = array();
1861
1862                 $feed_id = (int) $feed_id;
1863                 $cat_id = (int)Feeds::getFeedCategory($feed_id);
1864
1865                 if ($cat_id == 0)
1866                         $null_cat_qpart = "cat_id IS NULL OR";
1867                 else
1868                         $null_cat_qpart = "";
1869
1870                 $pdo = Db::pdo();
1871
1872                 $sth = $pdo->prepare("SELECT * FROM ttrss_filters2 WHERE
1873                                 owner_uid = ? AND enabled = true ORDER BY order_id, title");
1874                 $sth->execute([$owner_uid]);
1875
1876                 $check_cats = array_merge(
1877                         Feeds::getParentCategories($cat_id, $owner_uid),
1878                         [$cat_id]);
1879
1880                 $check_cats_str = join(",", $check_cats);
1881                 $check_cats_fullids = array_map(function($a) { return "CAT:$a"; }, $check_cats);
1882
1883                 while ($line = $sth->fetch()) {
1884                         $filter_id = $line["id"];
1885
1886                         $match_any_rule = sql_bool_to_bool($line["match_any_rule"]);
1887
1888                         $sth2 = $pdo->prepare("SELECT
1889                                         r.reg_exp, r.inverse, r.feed_id, r.cat_id, r.cat_filter, r.match_on, t.name AS type_name
1890                                         FROM ttrss_filters2_rules AS r,
1891                                         ttrss_filter_types AS t
1892                                         WHERE
1893                                                 (match_on IS NOT NULL OR
1894                                                   (($null_cat_qpart (cat_id IS NULL AND cat_filter = false) OR cat_id IN ($check_cats_str)) AND
1895                                                   (feed_id IS NULL OR feed_id = ?))) AND
1896                                                 filter_type = t.id AND filter_id = ?");
1897                         $sth2->execute([$feed_id, $filter_id]);
1898
1899                         $rules = array();
1900                         $actions = array();
1901
1902                         while ($rule_line = $sth2->fetch()) {
1903         #                               print_r($rule_line);
1904
1905                                 if ($rule_line["match_on"]) {
1906                                         $match_on = json_decode($rule_line["match_on"], true);
1907
1908                                         if (in_array("0", $match_on) || in_array($feed_id, $match_on) || count(array_intersect($check_cats_fullids, $match_on)) > 0) {
1909
1910                                                 $rule = array();
1911                                                 $rule["reg_exp"] = $rule_line["reg_exp"];
1912                                                 $rule["type"] = $rule_line["type_name"];
1913                                                 $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1914
1915                                                 array_push($rules, $rule);
1916                                         } else if (!$match_any_rule) {
1917                                                 // this filter contains a rule that doesn't match to this feed/category combination
1918                                                 // thus filter has to be rejected
1919
1920                                                 $rules = [];
1921                                                 break;
1922                                         }
1923
1924                                 } else {
1925
1926                                         $rule = array();
1927                                         $rule["reg_exp"] = $rule_line["reg_exp"];
1928                                         $rule["type"] = $rule_line["type_name"];
1929                                         $rule["inverse"] = sql_bool_to_bool($rule_line["inverse"]);
1930
1931                                         array_push($rules, $rule);
1932                                 }
1933                         }
1934
1935                         if (count($rules) > 0) {
1936                                 $sth2 = $pdo->prepare("SELECT a.action_param,t.name AS type_name
1937                                                 FROM ttrss_filters2_actions AS a,
1938                                                 ttrss_filter_actions AS t
1939                                                 WHERE
1940                                                         action_id = t.id AND filter_id = ?");
1941                                 $sth2->execute([$filter_id]);
1942
1943                                 while ($action_line = $sth2->fetch()) {
1944                                         #                               print_r($action_line);
1945
1946                                         $action = array();
1947                                         $action["type"] = $action_line["type_name"];
1948                                         $action["param"] = $action_line["action_param"];
1949
1950                                         array_push($actions, $action);
1951                                 }
1952                         }
1953
1954                         $filter = array();
1955                         $filter["match_any_rule"] = sql_bool_to_bool($line["match_any_rule"]);
1956                         $filter["inverse"] = sql_bool_to_bool($line["inverse"]);
1957                         $filter["rules"] = $rules;
1958                         $filter["actions"] = $actions;
1959
1960                         if (count($rules) > 0 && count($actions) > 0) {
1961                                 array_push($filters, $filter);
1962                         }
1963                 }
1964
1965                 return $filters;
1966         }
1967
1968         function get_score_pic($score) {
1969                 if ($score > 100) {
1970                         return "score_high.png";
1971                 } else if ($score > 0) {
1972                         return "score_half_high.png";
1973                 } else if ($score < -100) {
1974                         return "score_low.png";
1975                 } else if ($score < 0) {
1976                         return "score_half_low.png";
1977                 } else {
1978                         return "score_neutral.png";
1979                 }
1980         }
1981
1982         function init_plugins() {
1983                 PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
1984
1985                 return true;
1986         }
1987
1988         function add_feed_category($feed_cat, $parent_cat_id = false) {
1989
1990                 if (!$feed_cat) return false;
1991
1992                 $feed_cat = mb_substr($feed_cat, 0, 250);
1993                 if (!$parent_cat_id) $parent_cat_id = null;
1994
1995                 $pdo = Db::pdo();
1996                 $tr_in_progress = false;
1997
1998                 try {
1999                         $pdo->beginTransaction();
2000                 } catch (Exception $e) {
2001                         $tr_in_progress = true;
2002                 }
2003
2004                 $sth = $pdo->prepare("SELECT id FROM ttrss_feed_categories
2005                                 WHERE (parent_cat = :parent OR (:parent IS NULL AND parent_cat IS NULL))
2006                                 AND title = :title AND owner_uid = :uid");
2007                 $sth->execute([':parent' => $parent_cat_id, ':title' => $feed_cat, ':uid' => $_SESSION['uid']]);
2008
2009                 if (!$sth->fetch()) {
2010
2011                         $sth = $pdo->prepare("INSERT INTO ttrss_feed_categories (owner_uid,title,parent_cat)
2012                                         VALUES (?, ?, ?)");
2013                         $sth->execute([$_SESSION['uid'], $feed_cat, $parent_cat_id]);
2014
2015                         if (!$tr_in_progress) $pdo->commit();
2016
2017                         return true;
2018                 }
2019
2020                 $pdo->commit();
2021
2022                 return false;
2023         }
2024
2025         /**
2026          * Fixes incomplete URLs by prepending "http://".
2027          * Also replaces feed:// with http://, and
2028          * prepends a trailing slash if the url is a domain name only.
2029          *
2030          * @param string $url Possibly incomplete URL
2031          *
2032          * @return string Fixed URL.
2033          */
2034         function fix_url($url) {
2035
2036                 // support schema-less urls
2037                 if (strpos($url, '//') === 0) {
2038                         $url = 'https:' . $url;
2039                 }
2040
2041                 if (strpos($url, '://') === false) {
2042                         $url = 'http://' . $url;
2043                 } else if (substr($url, 0, 5) == 'feed:') {
2044                         $url = 'http:' . substr($url, 5);
2045                 }
2046
2047                 //prepend slash if the URL has no slash in it
2048                 // "http://www.example" -> "http://www.example/"
2049                 if (strpos($url, '/', strpos($url, ':') + 3) === false) {
2050                         $url .= '/';
2051                 }
2052
2053                 //convert IDNA hostname to punycode if possible
2054                 if (function_exists("idn_to_ascii")) {
2055                         $parts = parse_url($url);
2056                         if (mb_detect_encoding($parts['host']) != 'ASCII')
2057                         {
2058                                 $parts['host'] = idn_to_ascii($parts['host']);
2059                                 $url = build_url($parts);
2060                         }
2061                 }
2062
2063                 if ($url != "http:///")
2064                         return $url;
2065                 else
2066                         return '';
2067         }
2068
2069         function validate_feed_url($url) {
2070                 $parts = parse_url($url);
2071
2072                 return ($parts['scheme'] == 'http' || $parts['scheme'] == 'feed' || $parts['scheme'] == 'https');
2073
2074         }
2075
2076         /* function save_email_address($email) {
2077                 // FIXME: implement persistent storage of emails
2078
2079                 if (!$_SESSION['stored_emails'])
2080                         $_SESSION['stored_emails'] = array();
2081
2082                 if (!in_array($email, $_SESSION['stored_emails']))
2083                         array_push($_SESSION['stored_emails'], $email);
2084         } */
2085
2086
2087         function get_feed_access_key($feed_id, $is_cat, $owner_uid = false) {
2088
2089                 if (!$owner_uid) $owner_uid = $_SESSION["uid"];
2090
2091                 $is_cat = bool_to_sql_bool($is_cat);
2092
2093                 $pdo = Db::pdo();
2094
2095                 $sth = $pdo->prepare("SELECT access_key FROM ttrss_access_keys
2096                                 WHERE feed_id = ? AND is_cat = ?
2097                                 AND owner_uid = ?");
2098                 $sth->execute([$feed_id, $is_cat, $owner_uid]);
2099
2100                 if ($row = $sth->fetch()) {
2101                         return $row["access_key"];
2102                 } else {
2103                         $key = uniqid_short();
2104
2105                         $sth = $pdo->prepare("INSERT INTO ttrss_access_keys
2106                                         (access_key, feed_id, is_cat, owner_uid)
2107                                         VALUES (?, ?, ?, ?)");
2108
2109                         $sth->execute([$key, $feed_id, $is_cat, $owner_uid]);
2110
2111                         return $key;
2112                 }
2113         }
2114
2115         function get_feeds_from_html($url, $content)
2116         {
2117                 $url     = fix_url($url);
2118                 $baseUrl = substr($url, 0, strrpos($url, '/') + 1);
2119
2120                 libxml_use_internal_errors(true);
2121
2122                 $doc = new DOMDocument();
2123                 $doc->loadHTML($content);
2124                 $xpath = new DOMXPath($doc);
2125                 $entries = $xpath->query('/html/head/link[@rel="alternate" and '.
2126                         '(contains(@type,"rss") or contains(@type,"atom"))]|/html/head/link[@rel="feed"]');
2127                 $feedUrls = array();
2128                 foreach ($entries as $entry) {
2129                         if ($entry->hasAttribute('href')) {
2130                                 $title = $entry->getAttribute('title');
2131                                 if ($title == '') {
2132                                         $title = $entry->getAttribute('type');
2133                                 }
2134                                 $feedUrl = rewrite_relative_url(
2135                                         $baseUrl, $entry->getAttribute('href')
2136                                 );
2137                                 $feedUrls[$feedUrl] = $title;
2138                         }
2139                 }
2140                 return $feedUrls;
2141         }
2142
2143         function is_html($content) {
2144                 return preg_match("/<html|DOCTYPE html/i", substr($content, 0, 100)) !== 0;
2145         }
2146
2147         function url_is_html($url, $login = false, $pass = false) {
2148                 return is_html(fetch_file_contents($url, false, $login, $pass));
2149         }
2150
2151         function build_url($parts) {
2152                 return $parts['scheme'] . "://" . $parts['host'] . $parts['path'];
2153         }
2154
2155         function cleanup_url_path($path) {
2156                 $path = str_replace("/./", "/", $path);
2157                 $path = str_replace("//", "/", $path);
2158
2159                 return $path;
2160         }
2161
2162         /**
2163          * Converts a (possibly) relative URL to a absolute one.
2164          *
2165          * @param string $url     Base URL (i.e. from where the document is)
2166          * @param string $rel_url Possibly relative URL in the document
2167          *
2168          * @return string Absolute URL
2169          */
2170         function rewrite_relative_url($url, $rel_url) {
2171                 if (strpos($rel_url, "://") !== false) {
2172                         return $rel_url;
2173                 } else if (strpos($rel_url, "//") === 0) {
2174                         # protocol-relative URL (rare but they exist)
2175                         return $rel_url;
2176                 } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
2177                         # magnet:, feed:, etc
2178                         return $rel_url;
2179                 } else if (strpos($rel_url, "/") === 0) {
2180                         $parts = parse_url($url);
2181                         $parts['path'] = $rel_url;
2182                         $parts['path'] = cleanup_url_path($parts['path']);
2183
2184                         return build_url($parts);
2185
2186                 } else {
2187                         $parts = parse_url($url);
2188                         if (!isset($parts['path'])) {
2189                                 $parts['path'] = '/';
2190                         }
2191                         $dir = $parts['path'];
2192                         if (substr($dir, -1) !== '/') {
2193                                 $dir = dirname($parts['path']);
2194                                 $dir !== '/' && $dir .= '/';
2195                         }
2196                         $parts['path'] = $dir . $rel_url;
2197                         $parts['path'] = cleanup_url_path($parts['path']);
2198
2199                         return build_url($parts);
2200                 }
2201         }
2202
2203         function cleanup_tags($days = 14, $limit = 1000) {
2204
2205                 $days = (int) $days;
2206
2207                 if (DB_TYPE == "pgsql") {
2208                         $interval_query = "date_updated < NOW() - INTERVAL '$days days'";
2209                 } else if (DB_TYPE == "mysql") {
2210                         $interval_query = "date_updated < DATE_SUB(NOW(), INTERVAL $days DAY)";
2211                 }
2212
2213                 $tags_deleted = 0;
2214
2215                 $pdo = Db::pdo();
2216
2217                 while ($limit > 0) {
2218                         $limit_part = 500;
2219
2220                         $sth = $pdo->prepare("SELECT ttrss_tags.id AS id
2221                                         FROM ttrss_tags, ttrss_user_entries, ttrss_entries
2222                                         WHERE post_int_id = int_id AND $interval_query AND
2223                                         ref_id = ttrss_entries.id AND tag_cache != '' LIMIT ?");
2224                         $sth->execute([$limit]);
2225
2226                         $ids = array();
2227
2228                         while ($line = $sth->fetch()) {
2229                                 array_push($ids, $line['id']);
2230                         }
2231
2232                         if (count($ids) > 0) {
2233                                 $ids = join(",", $ids);
2234
2235                                 $usth = $pdo->query("DELETE FROM ttrss_tags WHERE id IN ($ids)");
2236                                 $tags_deleted = $usth->rowCount();
2237                         } else {
2238                                 break;
2239                         }
2240
2241                         $limit -= $limit_part;
2242                 }
2243
2244                 return $tags_deleted;
2245         }
2246
2247         function print_user_stylesheet() {
2248                 $value = get_pref('USER_STYLESHEET');
2249
2250                 if ($value) {
2251                         print "<style type=\"text/css\">";
2252                         print str_replace("<br/>", "\n", $value);
2253                         print "</style>";
2254                 }
2255
2256         }
2257
2258         function filter_to_sql($filter, $owner_uid) {
2259                 $query = array();
2260
2261                 $pdo = Db::pdo();
2262
2263                 if (DB_TYPE == "pgsql")
2264                         $reg_qpart = "~";
2265                 else
2266                         $reg_qpart = "REGEXP";
2267
2268                 foreach ($filter["rules"] AS $rule) {
2269                         $rule['reg_exp'] = str_replace('/', '\/', $rule["reg_exp"]);
2270                         $regexp_valid = preg_match('/' . $rule['reg_exp'] . '/',
2271                                         $rule['reg_exp']) !== FALSE;
2272
2273                         if ($regexp_valid) {
2274
2275                                 $rule['reg_exp'] = $pdo->quote($rule['reg_exp']);
2276
2277                                 switch ($rule["type"]) {
2278                                         case "title":
2279                                                 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2280                                                         $rule['reg_exp'] . "')";
2281                                                 break;
2282                                         case "content":
2283                                                 $qpart = "LOWER(ttrss_entries.content) $reg_qpart LOWER('".
2284                                                         $rule['reg_exp'] . "')";
2285                                                 break;
2286                                         case "both":
2287                                                 $qpart = "LOWER(ttrss_entries.title) $reg_qpart LOWER('".
2288                                                         $rule['reg_exp'] . "') OR LOWER(" .
2289                                                         "ttrss_entries.content) $reg_qpart LOWER('" . $rule['reg_exp'] . "')";
2290                                                 break;
2291                                         case "tag":
2292                                                 $qpart = "LOWER(ttrss_user_entries.tag_cache) $reg_qpart LOWER('".
2293                                                         $rule['reg_exp'] . "')";
2294                                                 break;
2295                                         case "link":
2296                                                 $qpart = "LOWER(ttrss_entries.link) $reg_qpart LOWER('".
2297                                                         $rule['reg_exp'] . "')";
2298                                                 break;
2299                                         case "author":
2300                                                 $qpart = "LOWER(ttrss_entries.author) $reg_qpart LOWER('".
2301                                                         $rule['reg_exp'] . "')";
2302                                                 break;
2303                                 }
2304
2305                                 if (isset($rule['inverse'])) $qpart = "NOT ($qpart)";
2306
2307                                 if (isset($rule["feed_id"]) && $rule["feed_id"] > 0) {
2308                                         $qpart .= " AND feed_id = " . $pdo->quote($rule["feed_id"]);
2309                                 }
2310
2311                                 if (isset($rule["cat_id"])) {
2312
2313                                         if ($rule["cat_id"] > 0) {
2314                                                 $children = Feeds::getChildCategories($rule["cat_id"], $owner_uid);
2315                                                 array_push($children, $rule["cat_id"]);
2316                                                 $children = array_map("intval", $children);
2317
2318                                                 $children = join(",", $children);
2319
2320                                                 $cat_qpart = "cat_id IN ($children)";
2321                                         } else {
2322                                                 $cat_qpart = "cat_id IS NULL";
2323                                         }
2324
2325                                         $qpart .= " AND $cat_qpart";
2326                                 }
2327
2328                                 $qpart .= " AND feed_id IS NOT NULL";
2329
2330                                 array_push($query, "($qpart)");
2331
2332                         }
2333                 }
2334
2335                 if (count($query) > 0) {
2336                         $fullquery = "(" . join($filter["match_any_rule"] ? "OR" : "AND", $query) . ")";
2337                 } else {
2338                         $fullquery = "(false)";
2339                 }
2340
2341                 if ($filter['inverse']) $fullquery = "(NOT $fullquery)";
2342
2343                 return $fullquery;
2344         }
2345
2346         if (!function_exists('gzdecode')) {
2347                 function gzdecode($string) { // no support for 2nd argument
2348                         return file_get_contents('compress.zlib://data:who/cares;base64,'.
2349                                 base64_encode($string));
2350                 }
2351         }
2352
2353         function get_random_bytes($length) {
2354                 if (function_exists('openssl_random_pseudo_bytes')) {
2355                         return openssl_random_pseudo_bytes($length);
2356                 } else {
2357                         $output = "";
2358
2359                         for ($i = 0; $i < $length; $i++)
2360                                 $output .= chr(mt_rand(0, 255));
2361
2362                         return $output;
2363                 }
2364         }
2365
2366         function read_stdin() {
2367                 $fp = fopen("php://stdin", "r");
2368
2369                 if ($fp) {
2370                         $line = trim(fgets($fp));
2371                         fclose($fp);
2372                         return $line;
2373                 }
2374
2375                 return null;
2376         }
2377
2378         function implements_interface($class, $interface) {
2379                 return in_array($interface, class_implements($class));
2380         }
2381
2382         function get_minified_js($files) {
2383
2384                 $rv = '';
2385
2386                 foreach ($files as $js) {
2387                         if (!isset($_GET['debug'])) {
2388                                 $cached_file = CACHE_DIR . "/js/".basename($js);
2389
2390                                 if (file_exists($cached_file) && is_readable($cached_file) && filemtime($cached_file) >= filemtime("js/$js")) {
2391
2392                                         list($header, $contents) = explode("\n", file_get_contents($cached_file), 2);
2393
2394                                         if ($header && $contents) {
2395                                                 list($htag, $hversion) = explode(":", $header);
2396
2397                                                 if ($htag == "tt-rss" && $hversion == VERSION) {
2398                                                         $rv .= $contents;
2399                                                         continue;
2400                                                 }
2401                                         }
2402                                 }
2403
2404                                 $minified = JShrink\Minifier::minify(file_get_contents("js/$js"));
2405                                 file_put_contents($cached_file, "tt-rss:" . VERSION . "\n" . $minified);
2406                                 $rv .= $minified;
2407
2408                         } else {
2409                                 $rv .= file_get_contents("js/$js"); // no cache in debug mode
2410                         }
2411                 }
2412
2413                 return $rv;
2414         }
2415
2416         function calculate_dep_timestamp() {
2417                 $files = array_merge(glob("js/*.js"), glob("css/*.css"));
2418
2419                 $max_ts = -1;
2420
2421                 foreach ($files as $file) {
2422                         if (filemtime($file) > $max_ts) $max_ts = filemtime($file);
2423                 }
2424
2425                 return $max_ts;
2426         }
2427
2428         function T_js_decl($s1, $s2) {
2429                 if ($s1 && $s2) {
2430                         $s1 = preg_replace("/\n/", "", $s1);
2431                         $s2 = preg_replace("/\n/", "", $s2);
2432
2433                         $s1 = preg_replace("/\"/", "\\\"", $s1);
2434                         $s2 = preg_replace("/\"/", "\\\"", $s2);
2435
2436                         return "T_messages[\"$s1\"] = \"$s2\";\n";
2437                 }
2438         }
2439
2440         function init_js_translations() {
2441
2442                 print 'var T_messages = new Object();
2443
2444                         function __(msg) {
2445                                 if (T_messages[msg]) {
2446                                         return T_messages[msg];
2447                                 } else {
2448                                         return msg;
2449                                 }
2450                         }
2451
2452                         function ngettext(msg1, msg2, n) {
2453                                 return __((parseInt(n) > 1) ? msg2 : msg1);
2454                         }';
2455
2456                 $l10n = _get_reader();
2457
2458                 for ($i = 0; $i < $l10n->total; $i++) {
2459                         $orig = $l10n->get_original_string($i);
2460                         if(strpos($orig, "\000") !== FALSE) { // Plural forms
2461                                 $key = explode(chr(0), $orig);
2462                                 print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
2463                                 print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
2464                         } else {
2465                                 $translation = __($orig);
2466                                 print T_js_decl($orig, $translation);
2467                         }
2468                 }
2469         }
2470
2471         function get_theme_path($theme) {
2472                 if ($theme == "default.php")
2473                         return "css/default.css";
2474
2475                 $check = "themes/$theme";
2476                 if (file_exists($check)) return $check;
2477
2478                 $check = "themes.local/$theme";
2479                 if (file_exists($check)) return $check;
2480         }
2481
2482         function theme_valid($theme) {
2483                 $bundled_themes = [ "default.php", "night.css", "compact.css" ];
2484
2485                 if (in_array($theme, $bundled_themes)) return true;
2486
2487                 $file = "themes/" . basename($theme);
2488
2489                 if (!file_exists($file)) $file = "themes.local/" . basename($theme);
2490
2491                 if (file_exists($file) && is_readable($file)) {
2492                         $fh = fopen($file, "r");
2493
2494                         if ($fh) {
2495                                 $header = fgets($fh);
2496                                 fclose($fh);
2497
2498                                 return strpos($header, "supports-version:" . VERSION_STATIC) !== FALSE;
2499                         }
2500                 }
2501
2502                 return false;
2503         }
2504
2505         /**
2506          * @SuppressWarnings(unused)
2507          */
2508         function error_json($code) {
2509                 require_once "errors.php";
2510
2511                 @$message = $ERRORS[$code];
2512
2513                 return json_encode(array("error" =>
2514                         array("code" => $code, "message" => $message)));
2515
2516         }
2517
2518         /*function abs_to_rel_path($dir) {
2519                 $tmp = str_replace(dirname(__DIR__), "", $dir);
2520
2521                 if (strlen($tmp) > 0 && substr($tmp, 0, 1) == "/") $tmp = substr($tmp, 1);
2522
2523                 return $tmp;
2524         }*/
2525
2526         function get_upload_error_message($code) {
2527
2528                 $errors = array(
2529                         0 => __('There is no error, the file uploaded with success'),
2530                         1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
2531                         2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
2532                         3 => __('The uploaded file was only partially uploaded'),
2533                         4 => __('No file was uploaded'),
2534                         6 => __('Missing a temporary folder'),
2535                         7 => __('Failed to write file to disk.'),
2536                         8 => __('A PHP extension stopped the file upload.'),
2537                 );
2538
2539                 return $errors[$code];
2540         }
2541
2542         function base64_img($filename) {
2543                 if (file_exists($filename)) {
2544                         $ext = pathinfo($filename, PATHINFO_EXTENSION);
2545
2546                         return "data:image/$ext;base64," . base64_encode(file_get_contents($filename));
2547                 } else {
2548                         return "";
2549                 }
2550         }
2551
2552         /*      this is essentially a wrapper for readfile() which allows plugins to hook
2553                 output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
2554
2555                 hook function should return true if request was handled (or at least attempted to)
2556
2557                 note that this can be called without user context so the plugin to handle this
2558                 should be loaded systemwide in config.php */
2559         function send_local_file($filename) {
2560                 if (file_exists($filename)) {
2561
2562                         if (is_writable($filename)) touch($filename);
2563
2564                         $tmppluginhost = new PluginHost();
2565
2566                         $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
2567                         $tmppluginhost->load_data();
2568
2569                         foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
2570                                 if ($plugin->hook_send_local_file($filename)) return true;
2571                         }
2572
2573                         $mimetype = mime_content_type($filename);
2574
2575                         // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
2576                         // video files are detected as octet-stream by mime_content_type()
2577
2578                         if ($mimetype == "application/octet-stream")
2579                                 $mimetype = "video/mp4";
2580
2581                         header("Content-type: $mimetype");
2582
2583                         $stamp = gmdate("D, d M Y H:i:s", filemtime($filename)) . " GMT";
2584                         header("Last-Modified: $stamp", true);
2585
2586                         return readfile($filename);
2587                 } else {
2588                         return false;
2589                 }
2590         }
2591
2592         function check_mysql_tables() {
2593                 $pdo = Db::pdo();
2594
2595                 $sth = $pdo->prepare("SELECT engine, table_name FROM information_schema.tables WHERE
2596                         table_schema = ? AND table_name LIKE 'ttrss_%' AND engine != 'InnoDB'");
2597                 $sth->execute([DB_NAME]);
2598
2599                 $bad_tables = [];
2600
2601                 while ($line = $sth->fetch()) {
2602                         array_push($bad_tables, $line);
2603                 }
2604
2605                 return $bad_tables;
2606         }
2607
2608         function validate_field($string, $allowed, $default = "") {
2609                 if (in_array($string, $allowed))
2610                         return $string;
2611                 else
2612                         return $default;
2613         }
2614
2615         function arr_qmarks($arr) {
2616                 return str_repeat('?,', count($arr) - 1) . '?';
2617         }