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