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