Passed
Push — master ( 6b7476...095fed )
by Cody
03:39
created

startup_gettext()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 3
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
define('EXPECTED_CONFIG_VERSION', 26);
3
define('SCHEMA_VERSION', 139);
4
5
define('LABEL_BASE_INDEX', -1024);
6
define('PLUGIN_FEED_BASE_INDEX', -128);
7
8
define('COOKIE_LIFETIME_LONG', 86400 * 365);
9
10
$fetch_last_error = false;
11
$fetch_last_error_code = false;
12
$fetch_last_content_type = false;
13
$fetch_last_error_content = false; // curl only for the time being
14
$fetch_effective_url = false;
15
$fetch_curl_used = false;
16
17
libxml_disable_entity_loader(true);
18
libxml_use_internal_errors(true);
19
20
// separate test because this is included before sanity checks
21
if (function_exists("mb_internal_encoding")) {
22
    mb_internal_encoding("UTF-8");
23
}
24
25
date_default_timezone_set('UTC');
26
if (defined('E_DEPRECATED')) {
27
    error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED);
28
} else {
29
    error_reporting(E_ALL & ~E_NOTICE);
30
}
31
32
ini_set('display_errors', 0);
33
ini_set('display_startup_errors', 0);
34
35
require_once 'config.php';
36
37
/**
38
 * Define a constant if not already defined
39
 */
40
function define_default($name, $value) {
41
    defined($name) or define($name, $value);
42
}
43
44
/* Some tunables you can override in config.php using define():	*/
45
46
define_default('FEED_FETCH_TIMEOUT', 45);
47
// How may seconds to wait for response when requesting feed from a site
48
define_default('FEED_FETCH_NO_CACHE_TIMEOUT', 15);
49
// How may seconds to wait for response when requesting feed from a
50
// site when that feed wasn't cached before
51
define_default('FILE_FETCH_TIMEOUT', 45);
52
// Default timeout when fetching files from remote sites
53
define_default('FILE_FETCH_CONNECT_TIMEOUT', 15);
54
// How many seconds to wait for initial response from website when
55
// fetching files from remote sites
56
define_default('DAEMON_UPDATE_LOGIN_LIMIT', 30);
57
// stop updating feeds if users haven't logged in for X days
58
define_default('DAEMON_FEED_LIMIT', 500);
59
// feed limit for one update batch
60
define_default('DAEMON_SLEEP_INTERVAL', 120);
61
// default sleep interval between feed updates (sec)
62
define_default('MAX_CACHE_FILE_SIZE', 64 * 1024 * 1024);
63
// do not cache files larger than that (bytes)
64
define_default('MAX_DOWNLOAD_FILE_SIZE', 16 * 1024 * 1024);
65
// do not download general files larger than that (bytes)
66
define_default('CACHE_MAX_DAYS', 7);
67
// max age in days for various automatically cached (temporary) files
68
define_default('MAX_CONDITIONAL_INTERVAL', 3600 * 12);
69
// max interval between forced unconditional updates for servers
70
// not complying with http if-modified-since (seconds)
71
define_default('MAX_FETCH_REQUESTS_PER_HOST', 25);
72
// a maximum amount of allowed HTTP requests per destination host
73
// during a single update (i.e. within PHP process lifetime)
74
// this is used to not cause excessive load on the origin server on
75
// e.g. feed subscription when all articles are being processes
76
77
/* tunables end here */
78
79
if (DB_TYPE == "pgsql") {
0 ignored issues
show
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
80
    define('SUBSTRING_FOR_DATE', 'SUBSTRING_FOR_DATE');
81
} else {
82
    define('SUBSTRING_FOR_DATE', 'SUBSTRING');
83
}
84
85
/**
86
 * Return available translations names.
87
 *
88
 * @access public
89
 * @return array A array of available translations.
90
 */
91
function get_translations() {
92
    $tr = array(
93
        "auto"  => __("Detect automatically"),
94
        "ar_SA" => "العربيّة (Arabic)",
95
        "bg_BG" => "Bulgarian",
96
        "da_DA" => "Dansk",
97
        "ca_CA" => "Català",
98
        "cs_CZ" => "Česky",
99
        "en_US" => "English",
100
        "el_GR" => "Ελληνικά",
101
        "es_ES" => "Español (España)",
102
        "es_LA" => "Español",
103
        "de_DE" => "Deutsch",
104
        "fr_FR" => "Français",
105
        "hu_HU" => "Magyar (Hungarian)",
106
        "it_IT" => "Italiano",
107
        "ja_JP" => "日本語 (Japanese)",
108
        "lv_LV" => "Latviešu",
109
        "nb_NO" => "Norwegian bokmål",
110
        "nl_NL" => "Dutch",
111
        "pl_PL" => "Polski",
112
        "ru_RU" => "Русский",
113
        "pt_BR" => "Portuguese/Brazil",
114
        "pt_PT" => "Portuguese/Portugal",
115
        "zh_CN" => "Simplified Chinese",
116
        "zh_TW" => "Traditional Chinese",
117
        "uk_UA" => "Українська",
118
        "sv_SE" => "Svenska",
119
        "fi_FI" => "Suomi",
120
        "tr_TR" => "Türkçe"
121
    );
122
123
    return $tr;
124
}
125
126
require_once "lib/accept-to-gettext.php";
127
require_once "lib/gettext/gettext.inc";
128
129
/**
130
 * @deprecated Loaded in bootstrap
131
 */
132
function startup_gettext()
133
{
134
    user_error(__FUNCTION__.' is deprecated', E_USER_DEPRECATED);
135
}
136
137
require_once 'db-prefs.php';
138
require_once 'controls.php';
139
140
define('SELF_USER_AGENT', 'Tiny Tiny RSS/'.get_version().' (http://tt-rss.org/)');
141
ini_set('user_agent', SELF_USER_AGENT);
142
143
$schema_version = false;
144
145
// TODO: compat wrapper, remove at some point
146
function _debug($msg) {
147
    Debug::log($msg);
148
}
149
150
function reset_fetch_domain_quota() {
151
    global $fetch_domain_hits;
152
153
    $fetch_domain_hits = [];
154
}
155
156
// TODO: max_size currently only works for CURL transfers
157
// TODO: multiple-argument way is deprecated, first parameter is a hash now
158
function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
159
            4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
160
161
    global $fetch_last_error;
162
    global $fetch_last_error_code;
163
    global $fetch_last_error_content;
164
    global $fetch_last_content_type;
165
    global $fetch_last_modified;
166
    global $fetch_effective_url;
167
    global $fetch_curl_used;
168
    global $fetch_domain_hits;
169
170
    $fetch_last_error = false;
171
    $fetch_last_error_code = -1;
172
    $fetch_last_error_content = "";
173
    $fetch_last_content_type = "";
174
    $fetch_curl_used = false;
175
    $fetch_last_modified = "";
176
    $fetch_effective_url = "";
177
178
    if (!is_array($fetch_domain_hits)) {
179
            $fetch_domain_hits = [];
180
    }
181
182
    if (!is_array($options)) {
183
184
        // falling back on compatibility shim
185
        $option_names = ["url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent"];
186
        $tmp = [];
187
188
        for ($i = 0; $i < func_num_args(); $i++) {
189
            $tmp[$option_names[$i]] = func_get_arg($i);
190
        }
191
192
        $options = $tmp;
193
    }
194
195
    $url = $options["url"];
196
    $type = isset($options["type"]) ? $options["type"] : false;
197
    $login = isset($options["login"]) ? $options["login"] : false;
198
    $pass = isset($options["pass"]) ? $options["pass"] : false;
199
    $post_query = isset($options["post_query"]) ? $options["post_query"] : false;
200
    $timeout = isset($options["timeout"]) ? $options["timeout"] : false;
201
    $last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
202
    $useragent = isset($options["useragent"]) ? $options["useragent"] : false;
203
    $followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
204
    $max_size = isset($options["max_size"]) ? $options["max_size"] : MAX_DOWNLOAD_FILE_SIZE; // in bytes
0 ignored issues
show
Bug introduced by
The constant MAX_DOWNLOAD_FILE_SIZE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
205
    $http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
206
    $http_referrer = isset($options["http_referrer"]) ? $options["http_referrer"] : false;
207
208
    $url = ltrim($url, ' ');
209
    $url = str_replace(' ', '%20', $url);
210
211
    if (strpos($url, "//") === 0) {
212
            $url = 'http:'.$url;
213
    }
214
215
    $url_host = parse_url($url, PHP_URL_HOST);
216
    $fetch_domain_hits[$url_host] += 1;
217
218
    if ($fetch_domain_hits[$url_host] > MAX_FETCH_REQUESTS_PER_HOST) {
0 ignored issues
show
Bug introduced by
The constant MAX_FETCH_REQUESTS_PER_HOST was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
219
        user_error("Exceeded fetch request quota for $url_host: ".$fetch_domain_hits[$url_host], E_USER_WARNING);
220
        #return false;
221
    }
222
223
    if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
224
225
        $fetch_curl_used = true;
226
227
        $ch = curl_init($url);
228
229
        $curl_http_headers = [];
230
231
        if ($last_modified && !$post_query) {
232
                    array_push($curl_http_headers, "If-Modified-Since: $last_modified");
233
        }
234
235
        if ($http_accept) {
236
                    array_push($curl_http_headers, "Accept: ".$http_accept);
237
        }
238
239
        if (count($curl_http_headers) > 0) {
240
                    curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_http_headers);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_setopt() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

240
                    curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_HTTPHEADER, $curl_http_headers);
Loading history...
241
        }
242
243
        curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout ? $timeout : FILE_FETCH_CONNECT_TIMEOUT);
0 ignored issues
show
Bug introduced by
The constant FILE_FETCH_CONNECT_TIMEOUT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
244
        curl_setopt($ch, CURLOPT_TIMEOUT, $timeout ? $timeout : FILE_FETCH_TIMEOUT);
0 ignored issues
show
Bug introduced by
The constant FILE_FETCH_TIMEOUT was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
245
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
246
        curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
247
        curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
248
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
249
        curl_setopt($ch, CURLOPT_HEADER, true);
250
        curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
251
        curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent : SELF_USER_AGENT);
252
        curl_setopt($ch, CURLOPT_ENCODING, "");
253
254
        if ($http_referrer) {
255
                    curl_setopt($ch, CURLOPT_REFERER, $http_referrer);
256
        }
257
258
        if ($max_size) {
259
            curl_setopt($ch, CURLOPT_NOPROGRESS, false);
260
            curl_setopt($ch, CURLOPT_BUFFERSIZE, 16384); // needed to get 5 arguments in progress function?
261
262
            // holy shit closures in php
263
            // download & upload are *expected* sizes respectively, could be zero
264
            curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($curl_handle, $download_size, $downloaded, $upload_size, $uploaded) use(&$max_size) {
0 ignored issues
show
Unused Code introduced by
The parameter $upload_size is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

264
            curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($curl_handle, $download_size, $downloaded, /** @scrutinizer ignore-unused */ $upload_size, $uploaded) use(&$max_size) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $uploaded is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

264
            curl_setopt($ch, CURLOPT_PROGRESSFUNCTION, function($curl_handle, $download_size, $downloaded, $upload_size, /** @scrutinizer ignore-unused */ $uploaded) use(&$max_size) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
265
                Debug::log("[curl progressfunction] $downloaded $max_size", Debug::$LOG_EXTENDED);
266
267
                return ($downloaded > $max_size) ? 1 : 0; // if max size is set, abort when exceeding it
268
            });
269
270
        }
271
272
        if (!ini_get("open_basedir")) {
273
            curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
274
        }
275
276
        if (defined('_HTTP_PROXY')) {
277
            curl_setopt($ch, CURLOPT_PROXY, _HTTP_PROXY);
0 ignored issues
show
Bug introduced by
The constant _HTTP_PROXY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
278
        }
279
280
        if ($post_query) {
281
            curl_setopt($ch, CURLOPT_POST, true);
282
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
283
        }
284
285
        if ($login && $pass) {
286
                    curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
287
        }
288
289
        $ret = @curl_exec($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_exec() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

289
        $ret = @curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
290
291
        $headers_length = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_getinfo() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

291
        $headers_length = curl_getinfo(/** @scrutinizer ignore-type */ $ch, CURLINFO_HEADER_SIZE);
Loading history...
292
        $headers = explode("\r\n", substr($ret, 0, $headers_length));
293
        $contents = substr($ret, $headers_length);
294
295
        foreach ($headers as $header) {
296
            if (strstr($header, ": ") !== false) {
297
                [$key, $value] = explode(": ", $header);
298
299
                if (strtolower($key) == "last-modified") {
300
                    $fetch_last_modified = $value;
301
                }
302
            }
303
304
            if (substr(strtolower($header), 0, 7) == 'http/1.') {
305
                $fetch_last_error_code = (int) substr($header, 9, 3);
306
                $fetch_last_error = $header;
307
            }
308
        }
309
310
        if (curl_errno($ch) === 23 || curl_errno($ch) === 61) {
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_errno() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

310
        if (curl_errno(/** @scrutinizer ignore-type */ $ch) === 23 || curl_errno($ch) === 61) {
Loading history...
311
            curl_setopt($ch, CURLOPT_ENCODING, 'none');
312
            $contents = @curl_exec($ch);
313
        }
314
315
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
316
        $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
317
318
        $fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
319
320
        $fetch_last_error_code = $http_code;
321
322
        if ($http_code != 200 || $type && strpos($fetch_last_content_type, "$type") === false) {
323
324
            if (curl_errno($ch) != 0) {
325
                $fetch_last_error .= "; ".curl_errno($ch)." ".curl_error($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_error() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

325
                $fetch_last_error .= "; ".curl_errno($ch)." ".curl_error(/** @scrutinizer ignore-type */ $ch);
Loading history...
326
            }
327
328
            $fetch_last_error_content = $contents;
329
            curl_close($ch);
0 ignored issues
show
Bug introduced by
It seems like $ch can also be of type false; however, parameter $ch of curl_close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

329
            curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
330
            return false;
331
        }
332
333
        if (!$contents) {
334
            $fetch_last_error = curl_errno($ch)." ".curl_error($ch);
335
            curl_close($ch);
336
            return false;
337
        }
338
339
        curl_close($ch);
340
341
        $is_gzipped = RSSUtils::is_gzipped($contents);
342
343
        if ($is_gzipped) {
344
            $tmp = @gzdecode($contents);
0 ignored issues
show
Bug introduced by
It seems like $contents can also be of type true; however, parameter $data of gzdecode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

344
            $tmp = @gzdecode(/** @scrutinizer ignore-type */ $contents);
Loading history...
345
346
            if ($tmp) {
347
                $contents = $tmp;
348
            }
349
        }
350
351
        return $contents;
352
    } else {
353
354
        $fetch_curl_used = false;
355
356
        if ($login && $pass) {
357
            $url_parts = array();
358
359
            preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
360
361
            $pass = urlencode($pass);
362
363
            if ($url_parts[1] && $url_parts[2]) {
364
                $url = $url_parts[1]."://$login:$pass@".$url_parts[2];
365
            }
366
        }
367
368
        // TODO: should this support POST requests or not? idk
369
370
            $context_options = array(
371
                'http' => array(
372
                    'header' => array(
373
                        'Connection: close'
374
                    ),
375
                    'method' => 'GET',
376
                    'ignore_errors' => true,
377
                    'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
378
                    'protocol_version'=> 1.1)
379
                );
380
381
        if (!$post_query && $last_modified) {
382
                    array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
383
        }
384
385
        if ($http_accept) {
386
                    array_push($context_options['http']['header'], "Accept: $http_accept");
387
        }
388
389
        if ($http_referrer) {
390
                    array_push($context_options['http']['header'], "Referer: $http_referrer");
391
        }
392
393
        if (defined('_HTTP_PROXY')) {
394
            $context_options['http']['request_fulluri'] = true;
395
            $context_options['http']['proxy'] = _HTTP_PROXY;
396
        }
397
398
        $context = stream_context_create($context_options);
399
400
        $old_error = error_get_last();
401
402
        $fetch_effective_url = $url;
403
404
        $data = @file_get_contents($url, false, $context);
405
406
        if (isset($http_response_header) && is_array($http_response_header)) {
407
            foreach ($http_response_header as $header) {
408
                if (strstr($header, ": ") !== false) {
409
                    [$key, $value] = explode(": ", $header);
410
411
                    $key = strtolower($key);
412
413
                    if ($key == 'content-type') {
414
                        $fetch_last_content_type = $value;
415
                        // don't abort here b/c there might be more than one
416
                        // e.g. if we were being redirected -- last one is the right one
417
                    } else if ($key == 'last-modified') {
418
                        $fetch_last_modified = $value;
419
                    } else if ($key == 'location') {
420
                        $fetch_effective_url = $value;
421
                    }
422
                }
423
424
                if (substr(strtolower($header), 0, 7) == 'http/1.') {
425
                    $fetch_last_error_code = (int) substr($header, 9, 3);
426
                    $fetch_last_error = $header;
427
                }
428
            }
429
        }
430
431
        if ($fetch_last_error_code != 200) {
432
            $error = error_get_last();
433
434
            if ($error['message'] != $old_error['message']) {
435
                $fetch_last_error .= "; ".$error["message"];
436
            }
437
438
            $fetch_last_error_content = $data;
439
440
            return false;
441
        }
442
443
        $is_gzipped = RSSUtils::is_gzipped($data);
444
445
        if ($is_gzipped) {
446
            $tmp = @gzdecode($data);
447
448
            if ($tmp) {
449
                $data = $tmp;
450
            }
451
        }
452
453
        return $data;
454
    }
455
456
}
457
458
function initialize_user_prefs($uid, $profile = false) {
459
460
    if (get_schema_version() < 63) {
461
        $profile_qpart = "";
0 ignored issues
show
Unused Code introduced by
The assignment to $profile_qpart is dead and can be removed.
Loading history...
462
    }
463
464
    $pdo = DB::pdo();
465
    $in_nested_tr = false;
466
467
    try {
468
        $pdo->beginTransaction();
469
    } catch (Exception $e) {
470
        $in_nested_tr = true;
471
    }
472
473
    $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
474
475
    if (!is_numeric($profile) || !$profile || get_schema_version() < 63) {
476
        $profile = null;
477
    }
478
479
    $u_sth = $pdo->prepare("SELECT pref_name
480
        FROM ttrss_user_prefs WHERE owner_uid = :uid AND
481
            (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
482
    $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
483
484
    $active_prefs = array();
485
486
    while ($line = $u_sth->fetch()) {
487
        array_push($active_prefs, $line["pref_name"]);
488
    }
489
490
    while ($line = $sth->fetch()) {
491
        if (array_search($line["pref_name"], $active_prefs) === false) {
492
//				print "adding " . $line["pref_name"] . "<br>";
493
494
            if (get_schema_version() < 63) {
495
                $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
496
                    (owner_uid,pref_name,value) VALUES
497
                    (?, ?, ?)");
498
                $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
499
500
            } else {
501
                $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
502
                    (owner_uid,pref_name,value, profile) VALUES
503
                    (?, ?, ?, ?)");
504
                $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
505
            }
506
507
        }
508
    }
509
510
    if (!$in_nested_tr) {
511
        $pdo->commit();
512
    }
513
514
}
515
516
function get_ssl_certificate_id() {
517
    if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
518
        return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"].
519
            $_SERVER["REDIRECT_SSL_CLIENT_V_START"].
520
            $_SERVER["REDIRECT_SSL_CLIENT_V_END"].
521
            $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
522
    }
523
    if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
524
        return sha1($_SERVER["SSL_CLIENT_M_SERIAL"].
525
            $_SERVER["SSL_CLIENT_V_START"].
526
            $_SERVER["SSL_CLIENT_V_END"].
527
            $_SERVER["SSL_CLIENT_S_DN"]);
528
    }
529
    return "";
530
}
531
532
function authenticate_user($login, $password, $check_only = false, $service = false) {
533
534
    if (!SINGLE_USER_MODE) {
0 ignored issues
show
Bug introduced by
The constant SINGLE_USER_MODE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
535
        $user_id = false;
536
        $auth_module = false;
537
538
        foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
539
540
            $user_id = (int) $plugin->authenticate($login, $password, $service);
541
542
            if ($user_id) {
543
                $auth_module = strtolower(get_class($plugin));
544
                break;
545
            }
546
        }
547
548
        if ($user_id && !$check_only) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $user_id of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
549
550
            session_start();
551
            session_regenerate_id(true);
552
553
            $_SESSION["uid"] = $user_id;
554
            $_SESSION["auth_module"] = $auth_module;
555
556
            $pdo = DB::pdo();
557
            $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
558
                WHERE id = ?");
559
            $sth->execute([$user_id]);
560
            $row = $sth->fetch();
561
562
            $_SESSION["name"] = $row["login"];
563
            $_SESSION["access_level"] = $row["access_level"];
564
            $_SESSION["csrf_token"] = uniqid_short();
565
566
            $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
567
            $usth->execute([$user_id]);
568
569
            $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
570
            $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
571
            $_SESSION["pwd_hash"] = $row["pwd_hash"];
572
573
            initialize_user_prefs($_SESSION["uid"]);
574
575
            return true;
576
        }
577
578
        return false;
579
580
    } else {
581
582
        $_SESSION["uid"] = 1;
583
        $_SESSION["name"] = "admin";
584
        $_SESSION["access_level"] = 10;
585
586
        $_SESSION["hide_hello"] = true;
587
        $_SESSION["hide_logout"] = true;
588
589
        $_SESSION["auth_module"] = false;
590
591
        if (!$_SESSION["csrf_token"]) {
592
            $_SESSION["csrf_token"] = uniqid_short();
593
        }
594
595
        $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
596
597
        initialize_user_prefs($_SESSION["uid"]);
598
599
        return true;
600
    }
601
}
602
603
// this is used for user http parameters unless HTML code is actually needed
604
function clean($param) {
605
    if (is_array($param)) {
606
        return array_map("strip_tags", $param);
607
    } else if (is_string($param)) {
608
        return strip_tags($param);
609
    } else {
610
        return $param;
611
    }
612
}
613
614
function clean_filename($filename) {
615
    return basename(preg_replace("/\.\.|[\/\\\]/", "", clean($filename)));
616
}
617
618
function make_password($length = 12) {
619
    $password = "";
620
    $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ*%+^";
621
622
    $i = 0;
623
624
    while ($i < $length) {
625
626
        try {
627
            $idx = function_exists("random_int") ? random_int(0, strlen($possible) - 1) : mt_rand(0, strlen($possible) - 1);
628
        } catch (Exception $e) {
629
            $idx = mt_rand(0, strlen($possible) - 1);
630
        }
631
632
        $char = substr($possible, $idx, 1);
633
634
        if (!strstr($password, $char)) {
635
            $password .= $char;
636
            $i++;
637
        }
638
    }
639
640
    return $password;
641
}
642
643
// this is called after user is created to initialize default feeds, labels
644
// or whatever else
645
646
// user preferences are checked on every login, not here
647
648
function initialize_user($uid) {
649
650
    $pdo = DB::pdo();
651
652
    $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
653
        values (?, 'Tiny Tiny RSS: Forum',
654
            'http://tt-rss.org/forum/rss.php')");
655
    $sth->execute([$uid]);
656
}
657
658
function logout_user() {
659
    @session_destroy();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for session_destroy(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

659
    /** @scrutinizer ignore-unhandled */ @session_destroy();

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
660
    if (isset($_COOKIE[session_name()])) {
661
        setcookie(session_name(), '', time() - 42000, '/');
662
    }
663
    session_commit();
664
}
665
666
function validate_csrf($csrf_token) {
667
    return $csrf_token == $_SESSION['csrf_token'];
668
}
669
670
function load_user_plugins($owner_uid, $pluginhost = false) {
671
672
    if (!$pluginhost) {
673
        $pluginhost = PluginHost::getInstance();
674
    }
675
676
    if ($owner_uid && SCHEMA_VERSION >= 100) {
677
        $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
678
679
        $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
680
681
        if (get_schema_version() > 100) {
682
            $pluginhost->load_data();
683
        }
684
    }
685
}
686
687
function login_sequence() {
688
    $pdo = Db::pdo();
689
690
    if (SINGLE_USER_MODE) {
0 ignored issues
show
Bug introduced by
The constant SINGLE_USER_MODE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
691
        @session_start();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for session_start(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

691
        /** @scrutinizer ignore-unhandled */ @session_start();

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
692
        authenticate_user("admin", null);
693
        startup_gettext();
0 ignored issues
show
Deprecated Code introduced by
The function startup_gettext() has been deprecated: Loaded in bootstrap ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

693
        /** @scrutinizer ignore-deprecated */ startup_gettext();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
694
        load_user_plugins($_SESSION["uid"]);
695
    } else {
696
        if (!validate_session()) {
697
            $_SESSION["uid"] = false;
698
        }
699
700
        if (!$_SESSION["uid"]) {
701
702
            if (AUTH_AUTO_LOGIN && authenticate_user(null, null)) {
0 ignored issues
show
Bug introduced by
The constant AUTH_AUTO_LOGIN was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
703
                $_SESSION["ref_schema_version"] = get_schema_version(true);
704
            } else {
705
                    authenticate_user(null, null, true);
706
            }
707
708
            if (!$_SESSION["uid"]) {
709
                logout_user();
710
711
                render_login_form();
712
                exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
713
            }
714
715
        } else {
716
            /* bump login timestamp */
717
            $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
718
            $sth->execute([$_SESSION['uid']]);
719
720
            $_SESSION["last_login_update"] = time();
721
        }
722
723
        if ($_SESSION["uid"]) {
724
            startup_gettext();
0 ignored issues
show
Deprecated Code introduced by
The function startup_gettext() has been deprecated: Loaded in bootstrap ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

724
            /** @scrutinizer ignore-deprecated */ startup_gettext();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
725
            load_user_plugins($_SESSION["uid"]);
726
727
            /* cleanup ccache */
728
729
            $sth = $pdo->prepare("DELETE FROM ttrss_counters_cache WHERE owner_uid = ?
730
                AND
731
                    (SELECT COUNT(id) FROM ttrss_feeds WHERE
732
                        ttrss_feeds.id = feed_id) = 0");
733
734
            $sth->execute([$_SESSION['uid']]);
735
736
            $sth = $pdo->prepare("DELETE FROM ttrss_cat_counters_cache WHERE owner_uid = ?
737
                AND
738
                    (SELECT COUNT(id) FROM ttrss_feed_categories WHERE
739
                        ttrss_feed_categories.id = feed_id) = 0");
740
741
            $sth->execute([$_SESSION['uid']]);
742
        }
743
744
    }
745
}
746
747
function truncate_string($str, $max_len, $suffix = '&hellip;') {
748
    if (mb_strlen($str, "utf-8") > $max_len) {
749
        return mb_substr($str, 0, $max_len, "utf-8").$suffix;
750
    } else {
751
        return $str;
752
    }
753
}
754
755
function mb_substr_replace($original, $replacement, $position, $length) {
756
    $startString = mb_substr($original, 0, $position, "UTF-8");
757
    $endString = mb_substr($original, $position + $length, mb_strlen($original), "UTF-8");
758
759
    $out = $startString.$replacement.$endString;
760
761
    return $out;
762
}
763
764
function truncate_middle($str, $max_len, $suffix = '&hellip;') {
765
    if (mb_strlen($str) > $max_len) {
766
        return mb_substr_replace($str, $suffix, $max_len / 2, mb_strlen($str) - $max_len);
767
    } else {
768
        return $str;
769
    }
770
}
771
772
function convert_timestamp($timestamp, $source_tz, $dest_tz) {
773
774
    try {
775
        $source_tz = new DateTimeZone($source_tz);
776
    } catch (Exception $e) {
777
        $source_tz = new DateTimeZone('UTC');
778
    }
779
780
    try {
781
        $dest_tz = new DateTimeZone($dest_tz);
782
    } catch (Exception $e) {
783
        $dest_tz = new DateTimeZone('UTC');
784
    }
785
786
    $dt = new DateTime(date('Y-m-d H:i:s', $timestamp), $source_tz);
787
    return $dt->format('U') + $dest_tz->getOffset($dt);
788
}
789
790
function make_local_datetime($timestamp, $long, $owner_uid = false,
791
                $no_smart_dt = false, $eta_min = false) {
792
793
    if (!$owner_uid) {
794
        $owner_uid = $_SESSION['uid'];
795
    }
796
    if (!$timestamp) {
797
        $timestamp = '1970-01-01 0:00';
798
    }
799
800
    global $utc_tz;
801
    global $user_tz;
802
803
    if (!$utc_tz) {
804
        $utc_tz = new DateTimeZone('UTC');
805
    }
806
807
    $timestamp = substr($timestamp, 0, 19);
808
809
    # We store date in UTC internally
810
    $dt = new DateTime($timestamp, $utc_tz);
811
812
    $user_tz_string = get_pref('USER_TIMEZONE', $owner_uid);
813
814
    if ($user_tz_string != 'Automatic') {
815
816
        try {
817
            if (!$user_tz) {
818
                $user_tz = new DateTimeZone($user_tz_string);
819
            }
820
        } catch (Exception $e) {
821
            $user_tz = $utc_tz;
822
        }
823
824
        $tz_offset = $user_tz->getOffset($dt);
825
    } else {
826
        $tz_offset = (int) -$_SESSION["clientTzOffset"];
827
    }
828
829
    $user_timestamp = $dt->format('U') + $tz_offset;
830
831
    if (!$no_smart_dt) {
832
        return smart_date_time($user_timestamp,
833
            $tz_offset, $owner_uid, $eta_min);
834
    } else {
835
        if ($long) {
836
                    $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
837
        } else {
838
                    $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
839
        }
840
841
        return date($format, $user_timestamp);
842
    }
843
}
844
845
function smart_date_time($timestamp, $tz_offset = 0, $owner_uid = false, $eta_min = false) {
846
    if (!$owner_uid) {
847
        $owner_uid = $_SESSION['uid'];
848
    }
849
850
    if ($eta_min && time() + $tz_offset - $timestamp < 3600) {
851
        return T_sprintf("%d min", date("i", time() + $tz_offset - $timestamp));
852
    } else if (date("Y.m.d", $timestamp) == date("Y.m.d", time() + $tz_offset)) {
853
        $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
854
        if (strpos((strtolower($format)), "a") === false) {
855
                    return date("G:i", $timestamp);
856
        } else {
857
                    return date("g:i a", $timestamp);
858
        }
859
    } else if (date("Y", $timestamp) == date("Y", time() + $tz_offset)) {
860
        $format = get_pref('SHORT_DATE_FORMAT', $owner_uid);
861
        return date($format, $timestamp);
862
    } else {
863
        $format = get_pref('LONG_DATE_FORMAT', $owner_uid);
864
        return date($format, $timestamp);
865
    }
866
}
867
868
function sql_bool_to_bool($s) {
869
    return $s && ($s !== "f" && $s !== "false"); //no-op for PDO, backwards compat for legacy layer
870
}
871
872
function bool_to_sql_bool($s) {
873
    return $s ? 1 : 0;
874
}
875
876
// Session caching removed due to causing wrong redirects to upgrade
877
// script when get_schema_version() is called on an obsolete session
878
// created on a previous schema version.
879
function get_schema_version($nocache = false) {
880
    global $schema_version;
881
882
    $pdo = DB::pdo();
883
884
    if (!$schema_version && !$nocache) {
885
        $row = $pdo->query("SELECT schema_version FROM ttrss_version")->fetch();
886
        $version = $row["schema_version"];
887
        $schema_version = $version;
888
        return $version;
889
    } else {
890
        return $schema_version;
891
    }
892
}
893
894
function sanity_check() {
895
    require_once 'errors.php';
896
    global $ERRORS;
897
898
    $error_code = 0;
899
    $schema_version = get_schema_version(true);
900
901
    if ($schema_version != SCHEMA_VERSION) {
902
        $error_code = 5;
903
    }
904
905
    return array("code" => $error_code, "message" => $ERRORS[$error_code]);
906
}
907
908
function file_is_locked($filename) {
909
    if (file_exists(LOCK_DIRECTORY."/$filename")) {
0 ignored issues
show
Bug introduced by
The constant LOCK_DIRECTORY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
910
        if (function_exists('flock')) {
911
            $fp = @fopen(LOCK_DIRECTORY."/$filename", "r");
912
            if ($fp) {
0 ignored issues
show
introduced by
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
913
                if (flock($fp, LOCK_EX | LOCK_NB)) {
914
                    flock($fp, LOCK_UN);
915
                    fclose($fp);
916
                    return false;
917
                }
918
                fclose($fp);
919
                return true;
920
            } else {
921
                return false;
922
            }
923
        }
924
        return true; // consider the file always locked and skip the test
925
    } else {
926
        return false;
927
    }
928
}
929
930
931
function make_lockfile($filename) {
932
    $fp = fopen(LOCK_DIRECTORY."/$filename", "w");
0 ignored issues
show
Bug introduced by
The constant LOCK_DIRECTORY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
933
934
    if ($fp && flock($fp, LOCK_EX | LOCK_NB)) {
0 ignored issues
show
introduced by
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
935
        $stat_h = fstat($fp);
936
        $stat_f = stat(LOCK_DIRECTORY."/$filename");
937
938
        if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
939
            if ($stat_h["ino"] != $stat_f["ino"] ||
940
                    $stat_h["dev"] != $stat_f["dev"]) {
941
942
                return false;
943
            }
944
        }
945
946
        if (function_exists('posix_getpid')) {
947
            fwrite($fp, posix_getpid()."\n");
948
        }
949
        return $fp;
950
    } else {
951
        return false;
952
    }
953
}
954
955
function make_stampfile($filename) {
956
    $fp = fopen(LOCK_DIRECTORY."/$filename", "w");
0 ignored issues
show
Bug introduced by
The constant LOCK_DIRECTORY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
957
958
    if (flock($fp, LOCK_EX | LOCK_NB)) {
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of flock() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

958
    if (flock(/** @scrutinizer ignore-type */ $fp, LOCK_EX | LOCK_NB)) {
Loading history...
959
        fwrite($fp, time()."\n");
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

959
        fwrite(/** @scrutinizer ignore-type */ $fp, time()."\n");
Loading history...
960
        flock($fp, LOCK_UN);
961
        fclose($fp);
0 ignored issues
show
Bug introduced by
It seems like $fp can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

961
        fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
962
        return true;
963
    } else {
964
        return false;
965
    }
966
}
967
968
function sql_random_function() {
969
    if (DB_TYPE == "mysql") {
0 ignored issues
show
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
970
        return "RAND()";
971
    } else {
972
        return "RANDOM()";
973
    }
974
}
975
976
function getFeedUnread($feed, $is_cat = false) {
977
    return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
978
}
979
980
function checkbox_to_sql_bool($val) {
981
    return ($val == "on") ? 1 : 0;
982
}
983
984
function uniqid_short() {
985
    return uniqid(base_convert(rand(), 10, 36));
986
}
987
988
function make_init_params() {
989
    $params = array();
990
991
    foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
992
                    "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
993
                    "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
994
                    "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
995
996
        $params[strtolower($param)] = (int) get_pref($param);
997
    }
998
999
    $params["check_for_updates"] = CHECK_FOR_UPDATES;
0 ignored issues
show
Bug introduced by
The constant CHECK_FOR_UPDATES was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1000
    $params["icons_url"] = ICONS_URL;
0 ignored issues
show
Bug introduced by
The constant ICONS_URL was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1001
    $params["cookie_lifetime"] = SESSION_COOKIE_LIFETIME;
0 ignored issues
show
Bug introduced by
The constant SESSION_COOKIE_LIFETIME was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1002
    $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1003
    $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1004
    $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1005
    $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1006
    $params["is_default_pw"] = Pref_Prefs::isdefaultpassword();
1007
    $params["label_base_index"] = (int) LABEL_BASE_INDEX;
1008
1009
    $theme = get_pref("USER_CSS_THEME", false, false);
1010
    $params["theme"] = theme_exists($theme) ? $theme : "";
1011
1012
    $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
1013
1014
    $params["php_platform"] = PHP_OS;
1015
    $params["php_version"] = PHP_VERSION;
1016
1017
    $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1018
1019
    $pdo = Db::pdo();
1020
1021
    $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM ttrss_feeds WHERE owner_uid = ?");
1022
    $sth->execute([$_SESSION['uid']]);
1023
    $row = $sth->fetch();
1024
1025
    $max_feed_id = $row["mid"];
1026
    $num_feeds = $row["nf"];
1027
1028
    $params["max_feed_id"] = (int) $max_feed_id;
1029
    $params["num_feeds"] = (int) $num_feeds;
1030
1031
    $params["hotkeys"] = get_hotkeys_map();
1032
1033
    $params["csrf_token"] = $_SESSION["csrf_token"];
1034
    $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1035
1036
    $params['simple_update'] = defined('SIMPLE_UPDATE_MODE') && SIMPLE_UPDATE_MODE;
0 ignored issues
show
Bug introduced by
The constant SIMPLE_UPDATE_MODE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1037
1038
    $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1039
1040
    $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1041
1042
    return $params;
1043
}
1044
1045
function get_hotkeys_info() {
1046
    $hotkeys = array(
1047
        __("Navigation") => array(
1048
            "next_feed" => __("Open next feed"),
1049
            "prev_feed" => __("Open previous feed"),
1050
            "next_article_or_scroll" => __("Open next article (in combined mode, scroll down)"),
1051
            "prev_article_or_scroll" => __("Open previous article (in combined mode, scroll up)"),
1052
            "next_article_page" => __("Scroll article by one page down"),
1053
            "prev_article_page" => __("Scroll article by one page up"),
1054
            "next_article_noscroll" => __("Open next article"),
1055
            "prev_article_noscroll" => __("Open previous article"),
1056
            "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1057
            "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1058
            "search_dialog" => __("Show search dialog")),
1059
        __("Article") => array(
1060
            "toggle_mark" => __("Toggle starred"),
1061
            "toggle_publ" => __("Toggle published"),
1062
            "toggle_unread" => __("Toggle unread"),
1063
            "edit_tags" => __("Edit tags"),
1064
            "open_in_new_window" => __("Open in new window"),
1065
            "catchup_below" => __("Mark below as read"),
1066
            "catchup_above" => __("Mark above as read"),
1067
            "article_scroll_down" => __("Scroll down"),
1068
            "article_scroll_up" => __("Scroll up"),
1069
            "article_page_down" => __("Scroll down page"),
1070
            "article_page_up" => __("Scroll up page"),
1071
            "select_article_cursor" => __("Select article under cursor"),
1072
            "email_article" => __("Email article"),
1073
            "close_article" => __("Close/collapse article"),
1074
            "toggle_expand" => __("Toggle article expansion (combined mode)"),
1075
            "toggle_widescreen" => __("Toggle widescreen mode"),
1076
            "toggle_embed_original" => __("Toggle embed original")),
1077
        __("Article selection") => array(
1078
            "select_all" => __("Select all articles"),
1079
            "select_unread" => __("Select unread"),
1080
            "select_marked" => __("Select starred"),
1081
            "select_published" => __("Select published"),
1082
            "select_invert" => __("Invert selection"),
1083
            "select_none" => __("Deselect everything")),
1084
        __("Feed") => array(
1085
            "feed_refresh" => __("Refresh current feed"),
1086
            "feed_unhide_read" => __("Un/hide read feeds"),
1087
            "feed_subscribe" => __("Subscribe to feed"),
1088
            "feed_edit" => __("Edit feed"),
1089
            "feed_catchup" => __("Mark as read"),
1090
            "feed_reverse" => __("Reverse headlines"),
1091
            "feed_toggle_vgroup" => __("Toggle headline grouping"),
1092
            "feed_debug_update" => __("Debug feed update"),
1093
            "feed_debug_viewfeed" => __("Debug viewfeed()"),
1094
            "catchup_all" => __("Mark all feeds as read"),
1095
            "cat_toggle_collapse" => __("Un/collapse current category"),
1096
            "toggle_cdm_expanded" => __("Toggle auto expand in combined mode"),
1097
            "toggle_combined_mode" => __("Toggle combined mode")),
1098
        __("Go to") => array(
1099
            "goto_all" => __("All articles"),
1100
            "goto_fresh" => __("Fresh"),
1101
            "goto_marked" => __("Starred"),
1102
            "goto_published" => __("Published"),
1103
            "goto_read" => __("Recently read"),
1104
            "goto_tagcloud" => __("Tag cloud"),
1105
            "goto_prefs" => __("Preferences")),
1106
        __("Other") => array(
1107
            "create_label" => __("Create label"),
1108
            "create_filter" => __("Create filter"),
1109
            "collapse_sidebar" => __("Un/collapse sidebar"),
1110
            "help_dialog" => __("Show help dialog"))
1111
    );
1112
1113
    foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1114
        $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1115
    }
1116
1117
    return $hotkeys;
1118
}
1119
1120
function get_hotkeys_map() {
1121
    $hotkeys = array(
1122
        "k" => "next_feed",
1123
        "j" => "prev_feed",
1124
        "n" => "next_article_noscroll",
1125
        "p" => "prev_article_noscroll",
1126
        //"(33)|PageUp" => "prev_article_page",
1127
        //"(34)|PageDown" => "next_article_page",
1128
        "*(33)|Shift+PgUp" => "article_page_up",
1129
        "*(34)|Shift+PgDn" => "article_page_down",
1130
        "(38)|Up" => "prev_article_or_scroll",
1131
        "(40)|Down" => "next_article_or_scroll",
1132
        "*(38)|Shift+Up" => "article_scroll_up",
1133
        "*(40)|Shift+Down" => "article_scroll_down",
1134
        "^(38)|Ctrl+Up" => "prev_article_noscroll",
1135
        "^(40)|Ctrl+Down" => "next_article_noscroll",
1136
        "/" => "search_dialog",
1137
        "s" => "toggle_mark",
1138
        "S" => "toggle_publ",
1139
        "u" => "toggle_unread",
1140
        "T" => "edit_tags",
1141
        "o" => "open_in_new_window",
1142
        "c p" => "catchup_below",
1143
        "c n" => "catchup_above",
1144
        "N" => "article_scroll_down",
1145
        "P" => "article_scroll_up",
1146
        "a W" => "toggle_widescreen",
1147
        "a e" => "toggle_embed_original",
1148
        "e" => "email_article",
1149
        "a q" => "close_article",
1150
        "a a" => "select_all",
1151
        "a u" => "select_unread",
1152
        "a U" => "select_marked",
1153
        "a p" => "select_published",
1154
        "a i" => "select_invert",
1155
        "a n" => "select_none",
1156
        "f r" => "feed_refresh",
1157
        "f a" => "feed_unhide_read",
1158
        "f s" => "feed_subscribe",
1159
        "f e" => "feed_edit",
1160
        "f q" => "feed_catchup",
1161
        "f x" => "feed_reverse",
1162
        "f g" => "feed_toggle_vgroup",
1163
        "f D" => "feed_debug_update",
1164
        "f G" => "feed_debug_viewfeed",
1165
        "f C" => "toggle_combined_mode",
1166
        "f c" => "toggle_cdm_expanded",
1167
        "Q" => "catchup_all",
1168
        "x" => "cat_toggle_collapse",
1169
        "g a" => "goto_all",
1170
        "g f" => "goto_fresh",
1171
        "g s" => "goto_marked",
1172
        "g p" => "goto_published",
1173
        "g r" => "goto_read",
1174
        "g t" => "goto_tagcloud",
1175
        "g P" => "goto_prefs",
1176
        "r" => "select_article_cursor",
1177
        "c l" => "create_label",
1178
        "c f" => "create_filter",
1179
        "c s" => "collapse_sidebar",
1180
        "?" => "help_dialog",
1181
    );
1182
1183
    foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1184
        $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1185
    }
1186
1187
    $prefixes = array();
1188
1189
    foreach (array_keys($hotkeys) as $hotkey) {
1190
        $pair = explode(" ", $hotkey, 2);
1191
1192
        if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1193
            array_push($prefixes, $pair[0]);
1194
        }
1195
    }
1196
1197
    return array($prefixes, $hotkeys);
1198
}
1199
1200
function make_runtime_info() {
1201
    $data = array();
1202
1203
    $pdo = Db::pdo();
1204
1205
    $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1206
            ttrss_feeds WHERE owner_uid = ?");
1207
    $sth->execute([$_SESSION['uid']]);
1208
    $row = $sth->fetch();
1209
1210
    $max_feed_id = $row['mid'];
1211
    $num_feeds = $row['nf'];
1212
1213
    $data["max_feed_id"] = (int) $max_feed_id;
1214
    $data["num_feeds"] = (int) $num_feeds;
1215
    $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1216
    $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1217
1218
    if (LOG_DESTINATION == 'sql' && $_SESSION['access_level'] >= 10) {
0 ignored issues
show
Bug introduced by
The constant LOG_DESTINATION was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1219
        if (DB_TYPE == 'pgsql') {
0 ignored issues
show
Bug introduced by
The constant DB_TYPE was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1220
            $log_interval = "created_at > NOW() - interval '1 hour'";
1221
        } else {
1222
            $log_interval = "created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)";
1223
        }
1224
1225
        $sth = $pdo->prepare("SELECT COUNT(id) AS cid FROM ttrss_error_log WHERE $log_interval");
1226
        $sth->execute();
1227
1228
        if ($row = $sth->fetch()) {
1229
            $data['recent_log_events'] = $row['cid'];
1230
        }
1231
    }
1232
1233
    if (file_exists(LOCK_DIRECTORY."/update_daemon.lock")) {
0 ignored issues
show
Bug introduced by
The constant LOCK_DIRECTORY was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1234
1235
        $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1236
1237
        if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1238
1239
            $stamp = (int) @file_get_contents(LOCK_DIRECTORY."/update_daemon.stamp");
1240
1241
            if ($stamp) {
1242
                $stamp_delta = time() - $stamp;
1243
1244
                if ($stamp_delta > 1800) {
1245
                    $stamp_check = 0;
1246
                } else {
1247
                    $stamp_check = 1;
1248
                    $_SESSION["daemon_stamp_check"] = time();
1249
                }
1250
1251
                $data['daemon_stamp_ok'] = $stamp_check;
1252
1253
                $stamp_fmt = date("Y.m.d, G:i", $stamp);
1254
1255
                $data['daemon_stamp'] = $stamp_fmt;
1256
            }
1257
        }
1258
    }
1259
1260
    return $data;
1261
}
1262
1263
function iframe_whitelisted($entry) {
1264
    @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1265
1266
    if ($src) {
1267
        foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_IFRAME_WHITELISTED) as $plugin) {
1268
            if ($plugin->hook_iframe_whitelisted($src)) {
1269
                            return true;
1270
            }
1271
        }
1272
    }
1273
1274
    return false;
1275
}
1276
1277
function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1278
    if (!$owner) {
1279
        $owner = $_SESSION["uid"];
1280
    }
1281
1282
    $res = trim($str); if (!$res) {
1283
        return '';
1284
    }
1285
1286
    $doc = new DOMDocument();
1287
    $doc->loadHTML('<?xml encoding="UTF-8">'.$res);
1288
    $xpath = new DOMXPath($doc);
1289
1290
    $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
1291
1292
    $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src]|//picture/source[@src])');
1293
1294
    foreach ($entries as $entry) {
1295
1296
        if ($entry->hasAttribute('href')) {
1297
            $entry->setAttribute('href',
1298
                rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1299
1300
            $entry->setAttribute('rel', 'noopener noreferrer');
1301
        }
1302
1303
        if ($entry->hasAttribute('src')) {
1304
            $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1305
            $entry->setAttribute('src', $src);
1306
        }
1307
1308
        if ($entry->nodeName == 'img') {
1309
            $entry->setAttribute('referrerpolicy', 'no-referrer');
1310
1311
            $entry->removeAttribute('width');
1312
            $entry->removeAttribute('height');
1313
1314
            if ($entry->hasAttribute('src')) {
1315
                $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1316
1317
                if (is_prefix_https() && !$is_https_url) {
1318
1319
                    if ($entry->hasAttribute('srcset')) {
1320
                        $entry->removeAttribute('srcset');
1321
                    }
1322
1323
                    if ($entry->hasAttribute('sizes')) {
1324
                        $entry->removeAttribute('sizes');
1325
                    }
1326
                }
1327
            }
1328
        }
1329
1330
        if ($entry->hasAttribute('src') &&
1331
                ($owner && get_pref("STRIP_IMAGES", $owner)) || $force_remove_images || $_SESSION["bw_limit"]) {
1332
1333
            $p = $doc->createElement('p');
1334
1335
            $a = $doc->createElement('a');
1336
            $a->setAttribute('href', $entry->getAttribute('src'));
1337
1338
            $a->appendChild(new DOMText($entry->getAttribute('src')));
1339
            $a->setAttribute('target', '_blank');
1340
            $a->setAttribute('rel', 'noopener noreferrer');
1341
1342
            $p->appendChild($a);
1343
1344
            if ($entry->nodeName == 'source') {
1345
1346
                if ($entry->parentNode && $entry->parentNode->parentNode) {
1347
                                    $entry->parentNode->parentNode->replaceChild($p, $entry->parentNode);
1348
                }
1349
1350
            } else if ($entry->nodeName == 'img') {
1351
1352
                if ($entry->parentNode) {
1353
                                    $entry->parentNode->replaceChild($p, $entry);
1354
                }
1355
1356
            }
1357
        }
1358
1359
        if (strtolower($entry->nodeName) == "a") {
1360
            $entry->setAttribute("target", "_blank");
1361
            $entry->setAttribute("rel", "noopener noreferrer");
1362
        }
1363
    }
1364
1365
    $entries = $xpath->query('//iframe');
1366
    foreach ($entries as $entry) {
1367
        if (!iframe_whitelisted($entry)) {
1368
            $entry->setAttribute('sandbox', 'allow-scripts');
1369
        } else {
1370
            if (is_prefix_https()) {
1371
                $entry->setAttribute("src",
1372
                    str_replace("http://", "https://",
1373
                        $entry->getAttribute("src")));
1374
            }
1375
        }
1376
    }
1377
1378
    $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1379
        'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1380
        'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1381
        'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1382
        'dt', 'em', 'footer', 'figure', 'figcaption',
1383
        'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1384
        'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1385
        'ol', 'p', 'picture', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1386
        'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1387
        'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1388
        'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace');
1389
1390
    if ($_SESSION['hasSandbox']) {
1391
        $allowed_elements[] = 'iframe';
1392
    }
1393
1394
    $disallowed_attributes = array('id', 'style', 'class');
1395
1396
    foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1397
        $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1398
        if (is_array($retval)) {
1399
            $doc = $retval[0];
1400
            $allowed_elements = $retval[1];
1401
            $disallowed_attributes = $retval[2];
1402
        } else {
1403
            $doc = $retval;
1404
        }
1405
    }
1406
1407
    $doc->removeChild($doc->firstChild); //remove doctype
1408
    $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1409
1410
    if ($highlight_words) {
1411
        foreach ($highlight_words as $word) {
1412
1413
            // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1414
1415
            $elements = $xpath->query("//*/text()");
1416
1417
            foreach ($elements as $child) {
1418
1419
                $fragment = $doc->createDocumentFragment();
1420
                $text = $child->textContent;
1421
1422
                while (($pos = mb_stripos($text, $word)) !== false) {
1423
                    $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1424
                    $word = mb_substr($text, $pos, mb_strlen($word));
1425
                    $highlight = $doc->createElement('span');
1426
                    $highlight->appendChild(new DomText($word));
1427
                    $highlight->setAttribute('class', 'highlight');
1428
                    $fragment->appendChild($highlight);
1429
                    $text = mb_substr($text, $pos + mb_strlen($word));
1430
                }
1431
1432
                if (!empty($text)) {
1433
                    $fragment->appendChild(new DomText($text));
1434
                }
1435
1436
                $child->parentNode->replaceChild($fragment, $child);
1437
            }
1438
        }
1439
    }
1440
1441
    $res = $doc->saveHTML();
1442
1443
    /* strip everything outside of <body>...</body> */
1444
1445
    $res_frag = array();
1446
    if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1447
        return $res_frag[1];
1448
    } else {
1449
        return $res;
1450
    }
1451
}
1452
1453
function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1454
    $xpath = new DOMXPath($doc);
1455
    $entries = $xpath->query('//*');
1456
1457
    foreach ($entries as $entry) {
1458
        if (!in_array($entry->nodeName, $allowed_elements)) {
1459
            $entry->parentNode->removeChild($entry);
1460
        }
1461
1462
        if ($entry->hasAttributes()) {
1463
            $attrs_to_remove = array();
1464
1465
            foreach ($entry->attributes as $attr) {
1466
1467
                if (strpos($attr->nodeName, 'on') === 0) {
1468
                    array_push($attrs_to_remove, $attr);
1469
                }
1470
1471
                if (strpos($attr->nodeName, "data-") === 0) {
1472
                    array_push($attrs_to_remove, $attr);
1473
                }
1474
1475
                if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1476
                    array_push($attrs_to_remove, $attr);
1477
                }
1478
1479
                if (in_array($attr->nodeName, $disallowed_attributes)) {
1480
                    array_push($attrs_to_remove, $attr);
1481
                }
1482
            }
1483
1484
            foreach ($attrs_to_remove as $attr) {
1485
                $entry->removeAttributeNode($attr);
1486
            }
1487
        }
1488
    }
1489
1490
    return $doc;
1491
}
1492
1493
function trim_array($array) {
1494
    $tmp = $array;
1495
    array_walk($tmp, 'trim');
1496
    return $tmp;
1497
}
1498
1499
function render_login_form() {
1500
    header('Cache-Control: public');
1501
1502
    require_once "login_form.php";
1503
    exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
1504
}
1505
1506
function T_sprintf() {
1507
    $args = func_get_args();
1508
    return vsprintf(__(array_shift($args)), $args);
1509
}
1510
1511
function print_checkpoint($n, $s) {
1512
    $ts = microtime(true);
1513
    echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1514
    return $ts;
1515
}
1516
1517
function is_server_https() {
1518
    return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1519
}
1520
1521
function is_prefix_https() {
1522
    return parse_url(SELF_URL_PATH, PHP_URL_SCHEME) == 'https';
0 ignored issues
show
Bug introduced by
The constant SELF_URL_PATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1523
}
1524
1525
// this returns SELF_URL_PATH sans ending slash
1526
function get_self_url_prefix() {
1527
    if (strrpos(SELF_URL_PATH, "/") === strlen(SELF_URL_PATH) - 1) {
0 ignored issues
show
Bug introduced by
The constant SELF_URL_PATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1528
        return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH) - 1);
1529
    } else {
1530
        return SELF_URL_PATH;
1531
    }
1532
}
1533
1534
/* TODO: This needs to use bcrypt */
1535
function encrypt_password($pass, $salt = '', $mode2 = false) {
1536
    if ($salt && $mode2) {
1537
        return "MODE2:".hash('sha256', $salt.$pass);
1538
    } else if ($salt) {
1539
        return "SHA1X:".sha1("$salt:$pass");
1540
    } else {
1541
        return "SHA1:".sha1($pass);
1542
    }
1543
}
1544
1545
function init_plugins() {
1546
    PluginHost::getInstance()->load(PLUGINS, PluginHost::KIND_ALL);
0 ignored issues
show
Bug introduced by
The constant PLUGINS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1547
1548
    return true;
1549
}
1550
1551
function build_url($parts) {
1552
    return $parts['scheme']."://".$parts['host'].$parts['path'];
1553
}
1554
1555
function cleanup_url_path($path) {
1556
    $path = str_replace("/./", "/", $path);
1557
    $path = str_replace("//", "/", $path);
1558
1559
    return $path;
1560
}
1561
1562
/**
1563
 * Converts a (possibly) relative URL to a absolute one.
1564
 *
1565
 * @param string $url     Base URL (i.e. from where the document is)
1566
 * @param string $rel_url Possibly relative URL in the document
1567
 *
1568
 * @return string Absolute URL
1569
 */
1570
function rewrite_relative_url($url, $rel_url) {
1571
    if (strpos($rel_url, "://") !== false) {
1572
        return $rel_url;
1573
    } else if (strpos($rel_url, "//") === 0) {
1574
        # protocol-relative URL (rare but they exist)
1575
        return $rel_url;
1576
    } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
1577
        # magnet:, feed:, etc
1578
        return $rel_url;
1579
    } else if (strpos($rel_url, "/") === 0) {
1580
        $parts = parse_url($url);
1581
        $parts['path'] = $rel_url;
1582
        $parts['path'] = cleanup_url_path($parts['path']);
1583
1584
        return build_url($parts);
1585
1586
    } else {
1587
        $parts = parse_url($url);
1588
        if (!isset($parts['path'])) {
1589
            $parts['path'] = '/';
1590
        }
1591
        $dir = $parts['path'];
1592
        if (substr($dir, -1) !== '/') {
1593
            $dir = dirname($parts['path']);
1594
            $dir !== '/' && $dir .= '/';
1595
        }
1596
        $parts['path'] = $dir.$rel_url;
1597
        $parts['path'] = cleanup_url_path($parts['path']);
1598
1599
        return build_url($parts);
1600
    }
1601
}
1602
1603
function print_user_stylesheet() {
1604
    $value = get_pref('USER_STYLESHEET');
1605
1606
    if ($value) {
1607
        print "<style type='text/css' id='user_css_style'>";
1608
        print str_replace("<br/>", "\n", $value);
1609
        print "</style>";
1610
    }
1611
}
1612
1613
if (!function_exists('gzdecode')) {
1614
    function gzdecode($string) {
1615
        return file_get_contents('compress.zlib://data:who/cares;base64,'.base64_encode($string));
1616
    }
1617
}
1618
1619
function get_random_bytes($length) {
1620
    if (function_exists('openssl_random_pseudo_bytes')) {
1621
        return openssl_random_pseudo_bytes($length);
1622
    } else {
1623
        $output = "";
1624
1625
        for ($i = 0; $i < $length; $i++) {
1626
                    $output .= chr(mt_rand(0, 255));
1627
        }
1628
1629
        return $output;
1630
    }
1631
}
1632
1633
function read_stdin() {
1634
    $fp = fopen("php://stdin", "r");
1635
1636
    if ($fp) {
0 ignored issues
show
introduced by
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
1637
        $line = trim(fgets($fp));
1638
        fclose($fp);
1639
        return $line;
1640
    }
1641
1642
    return null;
1643
}
1644
1645
function implements_interface($class, $interface) {
1646
    return in_array($interface, class_implements($class));
1647
}
1648
1649
function T_js_decl($s1, $s2) {
1650
    if ($s1 && $s2) {
1651
        $s1 = preg_replace("/\n/", "", $s1);
1652
        $s2 = preg_replace("/\n/", "", $s2);
1653
1654
        $s1 = preg_replace("/\"/", "\\\"", $s1);
1655
        $s2 = preg_replace("/\"/", "\\\"", $s2);
1656
1657
        return "T_messages[\"$s1\"] = \"$s2\";\n";
1658
    }
1659
}
1660
1661
function init_js_translations() {
1662
1663
    print 'var T_messages = new Object();
1664
1665
        public function __(msg) {
1666
            if (T_messages[msg]) {
1667
                return T_messages[msg];
1668
            } else {
1669
                return msg;
1670
            }
1671
        }
1672
1673
        public function ngettext(msg1, msg2, n) {
1674
            return __((parseInt(n) > 1) ? msg2 : msg1);
1675
        }';
1676
1677
    global $text_domains;
1678
1679
    foreach (array_keys($text_domains) as $domain) {
1680
        $l10n = _get_reader($domain);
1681
1682
        for ($i = 0; $i < $l10n->total; $i++) {
1683
            $orig = $l10n->get_original_string($i);
1684
            if (strpos($orig, "\000") !== false) { // Plural forms
1685
                $key = explode(chr(0), $orig);
1686
                print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
1687
                print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
1688
            } else {
1689
                $translation = _dgettext($domain, $orig);
1690
                print T_js_decl($orig, $translation);
1691
            }
1692
        }
1693
    }
1694
}
1695
1696
function get_theme_path($theme) {
1697
    if ($theme == "default.php") {
1698
            return "css/default.css";
1699
    }
1700
1701
    $check = "themes/$theme";
1702
    if (file_exists($check)) {
1703
        return $check;
1704
    }
1705
1706
    $check = "themes.local/$theme";
1707
    if (file_exists($check)) {
1708
        return $check;
1709
    }
1710
    }
1711
1712
function theme_exists($theme) {
1713
    return file_exists("themes/$theme") || file_exists("themes.local/$theme");
1714
}
1715
1716
function error_json($code) {
1717
    require_once "errors.php";
1718
1719
    @$message = $ERRORS[$code];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ERRORS seems to be never defined.
Loading history...
1720
1721
    return json_encode(
1722
        array(
1723
            "error" => array(
1724
                "code" => $code,
1725
                "message" => $message
1726
            )
1727
        )
1728
    );
1729
}
1730
1731
function get_upload_error_message($code) {
1732
    $errors = array(
1733
        0 => __('There is no error, the file uploaded with success'),
1734
        1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
1735
        2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
1736
        3 => __('The uploaded file was only partially uploaded'),
1737
        4 => __('No file was uploaded'),
1738
        6 => __('Missing a temporary folder'),
1739
        7 => __('Failed to write file to disk.'),
1740
        8 => __('A PHP extension stopped the file upload.'),
1741
    );
1742
1743
    return $errors[$code];
1744
}
1745
1746
function base64_img($filename) {
1747
    if (file_exists($filename)) {
1748
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
1749
1750
        return "data:image/$ext;base64,".base64_encode(file_get_contents($filename));
1751
    } else {
1752
        return "";
1753
    }
1754
}
1755
1756
/*	this is essentially a wrapper for readfile() which allows plugins to hook
1757
    output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
1758
1759
    hook function should return true if request was handled (or at least attempted to)
1760
1761
    note that this can be called without user context so the plugin to handle this
1762
    should be loaded systemwide in config.php */
1763
function send_local_file($filename) {
1764
    if (file_exists($filename)) {
1765
1766
        if (is_writable($filename)) {
1767
            touch($filename);
1768
        }
1769
1770
        $tmppluginhost = new PluginHost();
1771
1772
        $tmppluginhost->load(PLUGINS, PluginHost::KIND_SYSTEM);
0 ignored issues
show
Bug introduced by
The constant PLUGINS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
1773
        $tmppluginhost->load_data();
1774
1775
        foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
1776
            if ($plugin->hook_send_local_file($filename)) {
1777
                return true;
1778
            }
1779
        }
1780
1781
        $mimetype = mime_content_type($filename);
1782
1783
        // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
1784
        // video files are detected as octet-stream by mime_content_type()
1785
1786
        if ($mimetype == "application/octet-stream") {
1787
                    $mimetype = "video/mp4";
1788
        }
1789
1790
        header("Content-type: $mimetype");
1791
1792
        $stamp = gmdate("D, d M Y H:i:s", filemtime($filename))." GMT";
1793
        header("Last-Modified: $stamp", true);
1794
1795
        return readfile($filename);
1796
    } else {
1797
        return false;
1798
    }
1799
}
1800
1801
function arr_qmarks($arr) {
1802
    return str_repeat('?,', count($arr) - 1).'?';
1803
}
1804
1805
function get_scripts_timestamp() {
1806
    $files = glob("js/*.js");
1807
    $ts = 0;
1808
1809
    foreach ($files as $file) {
1810
        $file_ts = filemtime($file);
1811
        if ($file_ts > $ts) {
1812
            $ts = $file_ts;
1813
        }
1814
    }
1815
1816
    return $ts;
1817
}
1818
1819
/* for package maintainers who don't use git: if version_static.txt exists in tt-rss root
1820
    directory, its contents are displayed instead of git commit-based version, this could be generated
1821
    based on source git tree commit used when creating the package */
1822
1823
function get_version(&$git_commit = false, &$git_timestamp = false) {
1824
    global $ttrss_version;
1825
1826
    if (isset($ttrss_version)) {
1827
            return $ttrss_version;
1828
    }
1829
1830
    $ttrss_version = "UNKNOWN (Unsupported)";
1831
1832
    date_default_timezone_set('UTC');
1833
    $root_dir = dirname(dirname(__FILE__));
1834
1835
    if ('\\' === DIRECTORY_SEPARATOR) {
1836
        $ttrss_version = "UNKNOWN (Unsupported, Windows)";
1837
    } else if (PHP_OS === "Darwin") {
1838
        $ttrss_version = "UNKNOWN (Unsupported, Darwin)";
1839
    } else if (file_exists("$root_dir/version_static.txt")) {
1840
        $ttrss_version = trim(file_get_contents("$root_dir/version_static.txt"))." (Unsupported)";
1841
    } else if (is_dir("$root_dir/.git")) {
1842
        $rc = 0;
1843
        $output = [];
1844
1845
        $cwd = getcwd();
1846
1847
        chdir($root_dir);
1848
        exec('git log --pretty='.escapeshellarg('%ct %h').' -n1 HEAD 2>&1', $output, $rc);
1849
        chdir($cwd);
1850
1851
        if ($rc == 0) {
1852
            if (is_array($output) && count($output) > 0) {
1853
                [$timestamp, $commit] = explode(" ", $output[0], 2);
1854
1855
                $git_commit = $commit;
1856
                $git_timestamp = $timestamp;
1857
1858
                $ttrss_version = strftime("%y.%m", $timestamp)."-$commit";
0 ignored issues
show
Bug introduced by
$timestamp of type string is incompatible with the type integer expected by parameter $timestamp of strftime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1858
                $ttrss_version = strftime("%y.%m", /** @scrutinizer ignore-type */ $timestamp)."-$commit";
Loading history...
1859
            }
1860
        } else {
1861
            user_error("Unable to determine version (using $root_dir): ".implode("\n", $output), E_USER_WARNING);
1862
        }
1863
    }
1864
1865
    return $ttrss_version;
1866
}
1867