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