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