initialize_user()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 8
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/gettext/gettext.inc";
127
128
/**
129
 * @deprecated Loaded in bootstrap
130
 */
131
function startup_gettext()
132
{
133
    user_error(__FUNCTION__.' is deprecated', E_USER_DEPRECATED);
134
}
135
136
require_once 'db-prefs.php';
137
require_once 'controls.php';
138
139
define('SELF_USER_AGENT', 'Tiny Tiny RSS/'.get_version().' (http://tt-rss.org/)');
140
ini_set('user_agent', SELF_USER_AGENT);
141
142
$schema_version = false;
143
144
// TODO: compat wrapper, remove at some point
145
function _debug($msg) {
146
    Debug::log($msg);
147
}
148
149
function reset_fetch_domain_quota() {
150
    global $fetch_domain_hits;
151
152
    $fetch_domain_hits = [];
153
}
154
155
// TODO: max_size currently only works for CURL transfers
156
// TODO: multiple-argument way is deprecated, first parameter is a hash now
157
function fetch_file_contents($options /* previously: 0: $url , 1: $type = false, 2: $login = false, 3: $pass = false,
158
            4: $post_query = false, 5: $timeout = false, 6: $timestamp = 0, 7: $useragent = false*/) {
159
160
    global $fetch_last_error;
161
    global $fetch_last_error_code;
162
    global $fetch_last_error_content;
163
    global $fetch_last_content_type;
164
    global $fetch_last_modified;
165
    global $fetch_effective_url;
166
    global $fetch_curl_used;
167
    global $fetch_domain_hits;
168
169
    $fetch_last_error = false;
170
    $fetch_last_error_code = -1;
171
    $fetch_last_error_content = "";
172
    $fetch_last_content_type = "";
173
    $fetch_curl_used = false;
174
    $fetch_last_modified = "";
175
    $fetch_effective_url = "";
176
177
    if (!is_array($fetch_domain_hits)) {
178
            $fetch_domain_hits = [];
179
    }
180
181
    if (!is_array($options)) {
182
183
        // falling back on compatibility shim
184
        $option_names = ["url", "type", "login", "pass", "post_query", "timeout", "last_modified", "useragent"];
185
        $tmp = [];
186
187
        for ($i = 0; $i < func_num_args(); $i++) {
188
            $tmp[$option_names[$i]] = func_get_arg($i);
189
        }
190
191
        $options = $tmp;
192
    }
193
194
    $url = $options["url"];
195
    $type = isset($options["type"]) ? $options["type"] : false;
196
    $login = isset($options["login"]) ? $options["login"] : false;
197
    $pass = isset($options["pass"]) ? $options["pass"] : false;
198
    $post_query = isset($options["post_query"]) ? $options["post_query"] : false;
199
    $timeout = isset($options["timeout"]) ? $options["timeout"] : false;
200
    $last_modified = isset($options["last_modified"]) ? $options["last_modified"] : "";
201
    $useragent = isset($options["useragent"]) ? $options["useragent"] : false;
202
    $followlocation = isset($options["followlocation"]) ? $options["followlocation"] : true;
203
    $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...
204
    $http_accept = isset($options["http_accept"]) ? $options["http_accept"] : false;
205
    $http_referrer = isset($options["http_referrer"]) ? $options["http_referrer"] : false;
206
207
    $url = ltrim($url, ' ');
208
    $url = str_replace(' ', '%20', $url);
209
210
    if (strpos($url, "//") === 0) {
211
            $url = 'http:'.$url;
212
    }
213
214
    $url_host = parse_url($url, PHP_URL_HOST);
215
    $fetch_domain_hits[$url_host] += 1;
216
217
    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...
218
        user_error("Exceeded fetch request quota for $url_host: ".$fetch_domain_hits[$url_host], E_USER_WARNING);
219
        #return false;
220
    }
221
222
    if (!defined('NO_CURL') && function_exists('curl_init') && !ini_get("open_basedir")) {
223
224
        $fetch_curl_used = true;
225
226
        $ch = curl_init($url);
227
228
        $curl_http_headers = [];
229
230
        if ($last_modified && !$post_query) {
231
                    array_push($curl_http_headers, "If-Modified-Since: $last_modified");
232
        }
233
234
        if ($http_accept) {
235
                    array_push($curl_http_headers, "Accept: ".$http_accept);
236
        }
237
238
        if (count($curl_http_headers) > 0) {
239
                    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

239
                    curl_setopt(/** @scrutinizer ignore-type */ $ch, CURLOPT_HTTPHEADER, $curl_http_headers);
Loading history...
240
        }
241
242
        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...
243
        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...
244
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, !ini_get("open_basedir") && $followlocation);
245
        curl_setopt($ch, CURLOPT_MAXREDIRS, 20);
246
        curl_setopt($ch, CURLOPT_BINARYTRANSFER, true);
247
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
248
        curl_setopt($ch, CURLOPT_HEADER, true);
249
        curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
250
        curl_setopt($ch, CURLOPT_USERAGENT, $useragent ? $useragent : SELF_USER_AGENT);
251
        curl_setopt($ch, CURLOPT_ENCODING, "");
252
253
        if ($http_referrer) {
254
                    curl_setopt($ch, CURLOPT_REFERER, $http_referrer);
255
        }
256
257
        if ($max_size) {
258
            curl_setopt($ch, CURLOPT_NOPROGRESS, false);
259
            curl_setopt($ch, CURLOPT_BUFFERSIZE, 16384); // needed to get 5 arguments in progress function?
260
261
            // holy shit closures in php
262
            // download & upload are *expected* sizes respectively, could be zero
263
            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

263
            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

263
            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...
264
                Debug::log("[curl progressfunction] $downloaded $max_size", Debug::$LOG_EXTENDED);
265
266
                return ($downloaded > $max_size) ? 1 : 0; // if max size is set, abort when exceeding it
267
            });
268
269
        }
270
271
        if (!ini_get("open_basedir")) {
272
            curl_setopt($ch, CURLOPT_COOKIEJAR, "/dev/null");
273
        }
274
275
        if (defined('_HTTP_PROXY')) {
276
            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...
277
        }
278
279
        if ($post_query) {
280
            curl_setopt($ch, CURLOPT_POST, true);
281
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post_query);
282
        }
283
284
        if ($login && $pass) {
285
                    curl_setopt($ch, CURLOPT_USERPWD, "$login:$pass");
286
        }
287
288
        $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

288
        $ret = @curl_exec(/** @scrutinizer ignore-type */ $ch);
Loading history...
289
290
        $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

290
        $headers_length = curl_getinfo(/** @scrutinizer ignore-type */ $ch, CURLINFO_HEADER_SIZE);
Loading history...
291
        $headers = explode("\r\n", substr($ret, 0, $headers_length));
292
        $contents = substr($ret, $headers_length);
293
294
        foreach ($headers as $header) {
295
            if (strstr($header, ": ") !== false) {
296
                [$key, $value] = explode(": ", $header);
297
298
                if (strtolower($key) == "last-modified") {
299
                    $fetch_last_modified = $value;
300
                }
301
            }
302
303
            if (substr(strtolower($header), 0, 7) == 'http/1.') {
304
                $fetch_last_error_code = (int) substr($header, 9, 3);
305
                $fetch_last_error = $header;
306
            }
307
        }
308
309
        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

309
        if (curl_errno(/** @scrutinizer ignore-type */ $ch) === 23 || curl_errno($ch) === 61) {
Loading history...
310
            curl_setopt($ch, CURLOPT_ENCODING, 'none');
311
            $contents = @curl_exec($ch);
312
        }
313
314
        $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
315
        $fetch_last_content_type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
316
317
        $fetch_effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
318
319
        $fetch_last_error_code = $http_code;
320
321
        if ($http_code != 200 || $type && strpos($fetch_last_content_type, "$type") === false) {
322
323
            if (curl_errno($ch) != 0) {
324
                $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

324
                $fetch_last_error .= "; ".curl_errno($ch)." ".curl_error(/** @scrutinizer ignore-type */ $ch);
Loading history...
325
            }
326
327
            $fetch_last_error_content = $contents;
328
            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

328
            curl_close(/** @scrutinizer ignore-type */ $ch);
Loading history...
329
            return false;
330
        }
331
332
        if (!$contents) {
333
            $fetch_last_error = curl_errno($ch)." ".curl_error($ch);
334
            curl_close($ch);
335
            return false;
336
        }
337
338
        curl_close($ch);
339
340
        $is_gzipped = RSSUtils::is_gzipped($contents);
341
342
        if ($is_gzipped) {
343
            $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

343
            $tmp = @gzdecode(/** @scrutinizer ignore-type */ $contents);
Loading history...
344
345
            if ($tmp) {
346
                $contents = $tmp;
347
            }
348
        }
349
350
        return $contents;
351
    } else {
352
353
        $fetch_curl_used = false;
354
355
        if ($login && $pass) {
356
            $url_parts = array();
357
358
            preg_match("/(^[^:]*):\/\/(.*)/", $url, $url_parts);
359
360
            $pass = urlencode($pass);
361
362
            if ($url_parts[1] && $url_parts[2]) {
363
                $url = $url_parts[1]."://$login:$pass@".$url_parts[2];
364
            }
365
        }
366
367
        // TODO: should this support POST requests or not? idk
368
369
            $context_options = array(
370
                'http' => array(
371
                    'header' => array(
372
                        'Connection: close'
373
                    ),
374
                    'method' => 'GET',
375
                    'ignore_errors' => true,
376
                    'timeout' => $timeout ? $timeout : FILE_FETCH_TIMEOUT,
377
                    'protocol_version'=> 1.1)
378
                );
379
380
        if (!$post_query && $last_modified) {
381
                    array_push($context_options['http']['header'], "If-Modified-Since: $last_modified");
382
        }
383
384
        if ($http_accept) {
385
                    array_push($context_options['http']['header'], "Accept: $http_accept");
386
        }
387
388
        if ($http_referrer) {
389
                    array_push($context_options['http']['header'], "Referer: $http_referrer");
390
        }
391
392
        if (defined('_HTTP_PROXY')) {
393
            $context_options['http']['request_fulluri'] = true;
394
            $context_options['http']['proxy'] = _HTTP_PROXY;
395
        }
396
397
        $context = stream_context_create($context_options);
398
399
        $old_error = error_get_last();
400
401
        $fetch_effective_url = $url;
402
403
        $data = @file_get_contents($url, false, $context);
404
405
        if (isset($http_response_header) && is_array($http_response_header)) {
406
            foreach ($http_response_header as $header) {
407
                if (strstr($header, ": ") !== false) {
408
                    [$key, $value] = explode(": ", $header);
409
410
                    $key = strtolower($key);
411
412
                    if ($key == 'content-type') {
413
                        $fetch_last_content_type = $value;
414
                        // don't abort here b/c there might be more than one
415
                        // e.g. if we were being redirected -- last one is the right one
416
                    } else if ($key == 'last-modified') {
417
                        $fetch_last_modified = $value;
418
                    } else if ($key == 'location') {
419
                        $fetch_effective_url = $value;
420
                    }
421
                }
422
423
                if (substr(strtolower($header), 0, 7) == 'http/1.') {
424
                    $fetch_last_error_code = (int) substr($header, 9, 3);
425
                    $fetch_last_error = $header;
426
                }
427
            }
428
        }
429
430
        if ($fetch_last_error_code != 200) {
431
            $error = error_get_last();
432
433
            if ($error['message'] != $old_error['message']) {
434
                $fetch_last_error .= "; ".$error["message"];
435
            }
436
437
            $fetch_last_error_content = $data;
438
439
            return false;
440
        }
441
442
        $is_gzipped = RSSUtils::is_gzipped($data);
443
444
        if ($is_gzipped) {
445
            $tmp = @gzdecode($data);
446
447
            if ($tmp) {
448
                $data = $tmp;
449
            }
450
        }
451
452
        return $data;
453
    }
454
455
}
456
457
function initialize_user_prefs($uid, $profile = false) {
458
459
    if (get_schema_version() < 63) {
460
        $profile_qpart = "";
0 ignored issues
show
Unused Code introduced by
The assignment to $profile_qpart is dead and can be removed.
Loading history...
461
    }
462
463
    $pdo = DB::pdo();
464
    $in_nested_tr = false;
465
466
    try {
467
        $pdo->beginTransaction();
468
    } catch (Exception $e) {
469
        $in_nested_tr = true;
470
    }
471
472
    $sth = $pdo->query("SELECT pref_name,def_value FROM ttrss_prefs");
473
474
    if (!is_numeric($profile) || !$profile || get_schema_version() < 63) {
475
        $profile = null;
476
    }
477
478
    $u_sth = $pdo->prepare("SELECT pref_name
479
        FROM ttrss_user_prefs WHERE owner_uid = :uid AND
480
            (profile = :profile OR (:profile IS NULL AND profile IS NULL))");
481
    $u_sth->execute([':uid' => $uid, ':profile' => $profile]);
482
483
    $active_prefs = array();
484
485
    while ($line = $u_sth->fetch()) {
486
        array_push($active_prefs, $line["pref_name"]);
487
    }
488
489
    while ($line = $sth->fetch()) {
490
        if (array_search($line["pref_name"], $active_prefs) === false) {
491
//				print "adding " . $line["pref_name"] . "<br>";
492
493
            if (get_schema_version() < 63) {
494
                $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
495
                    (owner_uid,pref_name,value) VALUES
496
                    (?, ?, ?)");
497
                $i_sth->execute([$uid, $line["pref_name"], $line["def_value"]]);
498
499
            } else {
500
                $i_sth = $pdo->prepare("INSERT INTO ttrss_user_prefs
501
                    (owner_uid,pref_name,value, profile) VALUES
502
                    (?, ?, ?, ?)");
503
                $i_sth->execute([$uid, $line["pref_name"], $line["def_value"], $profile]);
504
            }
505
506
        }
507
    }
508
509
    if (!$in_nested_tr) {
510
        $pdo->commit();
511
    }
512
513
}
514
515
function get_ssl_certificate_id() {
516
    if ($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"]) {
517
        return sha1($_SERVER["REDIRECT_SSL_CLIENT_M_SERIAL"].
518
            $_SERVER["REDIRECT_SSL_CLIENT_V_START"].
519
            $_SERVER["REDIRECT_SSL_CLIENT_V_END"].
520
            $_SERVER["REDIRECT_SSL_CLIENT_S_DN"]);
521
    }
522
    if ($_SERVER["SSL_CLIENT_M_SERIAL"]) {
523
        return sha1($_SERVER["SSL_CLIENT_M_SERIAL"].
524
            $_SERVER["SSL_CLIENT_V_START"].
525
            $_SERVER["SSL_CLIENT_V_END"].
526
            $_SERVER["SSL_CLIENT_S_DN"]);
527
    }
528
    return "";
529
}
530
531
function authenticate_user($login, $password, $check_only = false, $service = false) {
532
533
    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...
534
        $user_id = false;
535
        $auth_module = false;
536
537
        foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_AUTH_USER) as $plugin) {
538
539
            $user_id = (int) $plugin->authenticate($login, $password, $service);
540
541
            if ($user_id) {
542
                $auth_module = strtolower(get_class($plugin));
543
                break;
544
            }
545
        }
546
547
        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...
548
549
            session_start();
550
            session_regenerate_id(true);
551
552
            $_SESSION["uid"] = $user_id;
553
            $_SESSION["auth_module"] = $auth_module;
554
555
            $pdo = DB::pdo();
556
            $sth = $pdo->prepare("SELECT login,access_level,pwd_hash FROM ttrss_users
557
                WHERE id = ?");
558
            $sth->execute([$user_id]);
559
            $row = $sth->fetch();
560
561
            $_SESSION["name"] = $row["login"];
562
            $_SESSION["access_level"] = $row["access_level"];
563
            $_SESSION["csrf_token"] = uniqid_short();
564
565
            $usth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
566
            $usth->execute([$user_id]);
567
568
            $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
569
            $_SESSION["user_agent"] = sha1($_SERVER['HTTP_USER_AGENT']);
570
            $_SESSION["pwd_hash"] = $row["pwd_hash"];
571
572
            initialize_user_prefs($_SESSION["uid"]);
573
574
            return true;
575
        }
576
577
        return false;
578
579
    } else {
580
581
        $_SESSION["uid"] = 1;
582
        $_SESSION["name"] = "admin";
583
        $_SESSION["access_level"] = 10;
584
585
        $_SESSION["hide_hello"] = true;
586
        $_SESSION["hide_logout"] = true;
587
588
        $_SESSION["auth_module"] = false;
589
590
        if (!$_SESSION["csrf_token"]) {
591
            $_SESSION["csrf_token"] = uniqid_short();
592
        }
593
594
        $_SESSION["ip_address"] = $_SERVER["REMOTE_ADDR"];
595
596
        initialize_user_prefs($_SESSION["uid"]);
597
598
        return true;
599
    }
600
}
601
602
// this is used for user http parameters unless HTML code is actually needed
603
function clean($param) {
604
    if (is_array($param)) {
605
        return array_map("strip_tags", $param);
606
    } else if (is_string($param)) {
607
        return strip_tags($param);
608
    } else {
609
        return $param;
610
    }
611
}
612
613
function clean_filename($filename) {
614
    return basename(preg_replace("/\.\.|[\/\\\]/", "", clean($filename)));
615
}
616
617
function make_password($length = 12) {
618
    $password = "";
619
    $possible = "0123456789abcdfghjkmnpqrstvwxyzABCDFGHJKMNPQRSTVWXYZ*%+^";
620
621
    $i = 0;
622
623
    while ($i < $length) {
624
625
        try {
626
            $idx = function_exists("random_int") ? random_int(0, strlen($possible) - 1) : mt_rand(0, strlen($possible) - 1);
627
        } catch (Exception $e) {
628
            $idx = mt_rand(0, strlen($possible) - 1);
629
        }
630
631
        $char = substr($possible, $idx, 1);
632
633
        if (!strstr($password, $char)) {
634
            $password .= $char;
635
            $i++;
636
        }
637
    }
638
639
    return $password;
640
}
641
642
// this is called after user is created to initialize default feeds, labels
643
// or whatever else
644
645
// user preferences are checked on every login, not here
646
647
function initialize_user($uid) {
648
649
    $pdo = DB::pdo();
650
651
    $sth = $pdo->prepare("insert into ttrss_feeds (owner_uid,title,feed_url)
652
        values (?, 'Tiny Tiny RSS: Forum',
653
            'http://tt-rss.org/forum/rss.php')");
654
    $sth->execute([$uid]);
655
}
656
657
function logout_user() {
658
    @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

658
    /** @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...
659
    if (isset($_COOKIE[session_name()])) {
660
        setcookie(session_name(), '', time() - 42000, '/');
661
    }
662
    session_commit();
663
}
664
665
function validate_csrf($csrf_token) {
666
    return $csrf_token == $_SESSION['csrf_token'];
667
}
668
669
function load_user_plugins($owner_uid, $pluginhost = false) {
670
671
    if (!$pluginhost) {
672
        $pluginhost = PluginHost::getInstance();
673
    }
674
675
    if ($owner_uid && SCHEMA_VERSION >= 100) {
676
        $plugins = get_pref("_ENABLED_PLUGINS", $owner_uid);
677
678
        $pluginhost->load($plugins, PluginHost::KIND_USER, $owner_uid);
679
680
        if (get_schema_version() > 100) {
681
            $pluginhost->load_data();
682
        }
683
    }
684
}
685
686
function login_sequence() {
687
    $pdo = Db::pdo();
688
689
    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...
690
        @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

690
        /** @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...
691
        authenticate_user("admin", null);
692
        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

692
        /** @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...
693
        load_user_plugins($_SESSION["uid"]);
694
    } else {
695
        if (!validate_session()) {
696
            $_SESSION["uid"] = false;
697
        }
698
699
        if (!$_SESSION["uid"]) {
700
701
            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...
702
                $_SESSION["ref_schema_version"] = get_schema_version(true);
703
            } else {
704
                    authenticate_user(null, null, true);
705
            }
706
707
            if (!$_SESSION["uid"]) {
708
                logout_user();
709
710
                render_login_form();
711
                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...
712
            }
713
714
        } else {
715
            /* bump login timestamp */
716
            $sth = $pdo->prepare("UPDATE ttrss_users SET last_login = NOW() WHERE id = ?");
717
            $sth->execute([$_SESSION['uid']]);
718
719
            $_SESSION["last_login_update"] = time();
720
        }
721
722
        if ($_SESSION["uid"]) {
723
            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

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

957
    if (flock(/** @scrutinizer ignore-type */ $fp, LOCK_EX | LOCK_NB)) {
Loading history...
958
        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

958
        fwrite(/** @scrutinizer ignore-type */ $fp, time()."\n");
Loading history...
959
        flock($fp, LOCK_UN);
960
        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

960
        fclose(/** @scrutinizer ignore-type */ $fp);
Loading history...
961
        return true;
962
    } else {
963
        return false;
964
    }
965
}
966
967
function sql_random_function() {
968
    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...
969
        return "RAND()";
970
    } else {
971
        return "RANDOM()";
972
    }
973
}
974
975
function getFeedUnread($feed, $is_cat = false) {
976
    return Feeds::getFeedArticles($feed, $is_cat, true, $_SESSION["uid"]);
977
}
978
979
function checkbox_to_sql_bool($val) {
980
    return ($val == "on") ? 1 : 0;
981
}
982
983
function uniqid_short() {
984
    return uniqid(base_convert(rand(), 10, 36));
985
}
986
987
function make_init_params() {
988
    $params = array();
989
990
    foreach (array("ON_CATCHUP_SHOW_NEXT_FEED", "HIDE_READ_FEEDS",
991
                    "ENABLE_FEED_CATS", "FEEDS_SORT_BY_UNREAD", "CONFIRM_FEED_CATCHUP",
992
                    "CDM_AUTO_CATCHUP", "FRESH_ARTICLE_MAX_AGE",
993
                    "HIDE_READ_SHOWS_SPECIAL", "COMBINED_DISPLAY_MODE") as $param) {
994
995
        $params[strtolower($param)] = (int) get_pref($param);
996
    }
997
998
    $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...
999
    $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...
1000
    $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...
1001
    $params["default_view_mode"] = get_pref("_DEFAULT_VIEW_MODE");
1002
    $params["default_view_limit"] = (int) get_pref("_DEFAULT_VIEW_LIMIT");
1003
    $params["default_view_order_by"] = get_pref("_DEFAULT_VIEW_ORDER_BY");
1004
    $params["bw_limit"] = (int) $_SESSION["bw_limit"];
1005
    $params["is_default_pw"] = Pref_Prefs::isdefaultpassword();
1006
    $params["label_base_index"] = (int) LABEL_BASE_INDEX;
1007
1008
    $theme = get_pref("USER_CSS_THEME", false, false);
1009
    $params["theme"] = theme_exists($theme) ? $theme : "";
1010
1011
    $params["plugins"] = implode(", ", PluginHost::getInstance()->get_plugin_names());
1012
1013
    $params["php_platform"] = PHP_OS;
1014
    $params["php_version"] = PHP_VERSION;
1015
1016
    $params["sanity_checksum"] = sha1(file_get_contents("include/sanity_check.php"));
1017
1018
    $pdo = Db::pdo();
1019
1020
    $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM ttrss_feeds WHERE owner_uid = ?");
1021
    $sth->execute([$_SESSION['uid']]);
1022
    $row = $sth->fetch();
1023
1024
    $max_feed_id = $row["mid"];
1025
    $num_feeds = $row["nf"];
1026
1027
    $params["max_feed_id"] = (int) $max_feed_id;
1028
    $params["num_feeds"] = (int) $num_feeds;
1029
1030
    $params["hotkeys"] = get_hotkeys_map();
1031
1032
    $params["csrf_token"] = $_SESSION["csrf_token"];
1033
    $params["widescreen"] = (int) $_COOKIE["ttrss_widescreen"];
1034
1035
    $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...
1036
1037
    $params["icon_indicator_white"] = base64_img("images/indicator_white.gif");
1038
1039
    $params["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1040
1041
    return $params;
1042
}
1043
1044
function get_hotkeys_info() {
1045
    $hotkeys = array(
1046
        __("Navigation") => array(
1047
            "next_feed" => __("Open next feed"),
1048
            "prev_feed" => __("Open previous feed"),
1049
            "next_article_or_scroll" => __("Open next article (in combined mode, scroll down)"),
1050
            "prev_article_or_scroll" => __("Open previous article (in combined mode, scroll up)"),
1051
            "next_article_page" => __("Scroll article by one page down"),
1052
            "prev_article_page" => __("Scroll article by one page up"),
1053
            "next_article_noscroll" => __("Open next article"),
1054
            "prev_article_noscroll" => __("Open previous article"),
1055
            "next_article_noexpand" => __("Move to next article (don't expand or mark read)"),
1056
            "prev_article_noexpand" => __("Move to previous article (don't expand or mark read)"),
1057
            "search_dialog" => __("Show search dialog")),
1058
        __("Article") => array(
1059
            "toggle_mark" => __("Toggle starred"),
1060
            "toggle_publ" => __("Toggle published"),
1061
            "toggle_unread" => __("Toggle unread"),
1062
            "edit_tags" => __("Edit tags"),
1063
            "open_in_new_window" => __("Open in new window"),
1064
            "catchup_below" => __("Mark below as read"),
1065
            "catchup_above" => __("Mark above as read"),
1066
            "article_scroll_down" => __("Scroll down"),
1067
            "article_scroll_up" => __("Scroll up"),
1068
            "article_page_down" => __("Scroll down page"),
1069
            "article_page_up" => __("Scroll up page"),
1070
            "select_article_cursor" => __("Select article under cursor"),
1071
            "email_article" => __("Email article"),
1072
            "close_article" => __("Close/collapse article"),
1073
            "toggle_expand" => __("Toggle article expansion (combined mode)"),
1074
            "toggle_widescreen" => __("Toggle widescreen mode"),
1075
            "toggle_embed_original" => __("Toggle embed original")),
1076
        __("Article selection") => array(
1077
            "select_all" => __("Select all articles"),
1078
            "select_unread" => __("Select unread"),
1079
            "select_marked" => __("Select starred"),
1080
            "select_published" => __("Select published"),
1081
            "select_invert" => __("Invert selection"),
1082
            "select_none" => __("Deselect everything")),
1083
        __("Feed") => array(
1084
            "feed_refresh" => __("Refresh current feed"),
1085
            "feed_unhide_read" => __("Un/hide read feeds"),
1086
            "feed_subscribe" => __("Subscribe to feed"),
1087
            "feed_edit" => __("Edit feed"),
1088
            "feed_catchup" => __("Mark as read"),
1089
            "feed_reverse" => __("Reverse headlines"),
1090
            "feed_toggle_vgroup" => __("Toggle headline grouping"),
1091
            "feed_debug_update" => __("Debug feed update"),
1092
            "feed_debug_viewfeed" => __("Debug viewfeed()"),
1093
            "catchup_all" => __("Mark all feeds as read"),
1094
            "cat_toggle_collapse" => __("Un/collapse current category"),
1095
            "toggle_cdm_expanded" => __("Toggle auto expand in combined mode"),
1096
            "toggle_combined_mode" => __("Toggle combined mode")),
1097
        __("Go to") => array(
1098
            "goto_all" => __("All articles"),
1099
            "goto_fresh" => __("Fresh"),
1100
            "goto_marked" => __("Starred"),
1101
            "goto_published" => __("Published"),
1102
            "goto_read" => __("Recently read"),
1103
            "goto_tagcloud" => __("Tag cloud"),
1104
            "goto_prefs" => __("Preferences")),
1105
        __("Other") => array(
1106
            "create_label" => __("Create label"),
1107
            "create_filter" => __("Create filter"),
1108
            "collapse_sidebar" => __("Un/collapse sidebar"),
1109
            "help_dialog" => __("Show help dialog"))
1110
    );
1111
1112
    foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_INFO) as $plugin) {
1113
        $hotkeys = $plugin->hook_hotkey_info($hotkeys);
1114
    }
1115
1116
    return $hotkeys;
1117
}
1118
1119
function get_hotkeys_map() {
1120
    $hotkeys = array(
1121
        "k" => "next_feed",
1122
        "j" => "prev_feed",
1123
        "n" => "next_article_noscroll",
1124
        "p" => "prev_article_noscroll",
1125
        //"(33)|PageUp" => "prev_article_page",
1126
        //"(34)|PageDown" => "next_article_page",
1127
        "*(33)|Shift+PgUp" => "article_page_up",
1128
        "*(34)|Shift+PgDn" => "article_page_down",
1129
        "(38)|Up" => "prev_article_or_scroll",
1130
        "(40)|Down" => "next_article_or_scroll",
1131
        "*(38)|Shift+Up" => "article_scroll_up",
1132
        "*(40)|Shift+Down" => "article_scroll_down",
1133
        "^(38)|Ctrl+Up" => "prev_article_noscroll",
1134
        "^(40)|Ctrl+Down" => "next_article_noscroll",
1135
        "/" => "search_dialog",
1136
        "s" => "toggle_mark",
1137
        "S" => "toggle_publ",
1138
        "u" => "toggle_unread",
1139
        "T" => "edit_tags",
1140
        "o" => "open_in_new_window",
1141
        "c p" => "catchup_below",
1142
        "c n" => "catchup_above",
1143
        "N" => "article_scroll_down",
1144
        "P" => "article_scroll_up",
1145
        "a W" => "toggle_widescreen",
1146
        "a e" => "toggle_embed_original",
1147
        "e" => "email_article",
1148
        "a q" => "close_article",
1149
        "a a" => "select_all",
1150
        "a u" => "select_unread",
1151
        "a U" => "select_marked",
1152
        "a p" => "select_published",
1153
        "a i" => "select_invert",
1154
        "a n" => "select_none",
1155
        "f r" => "feed_refresh",
1156
        "f a" => "feed_unhide_read",
1157
        "f s" => "feed_subscribe",
1158
        "f e" => "feed_edit",
1159
        "f q" => "feed_catchup",
1160
        "f x" => "feed_reverse",
1161
        "f g" => "feed_toggle_vgroup",
1162
        "f D" => "feed_debug_update",
1163
        "f G" => "feed_debug_viewfeed",
1164
        "f C" => "toggle_combined_mode",
1165
        "f c" => "toggle_cdm_expanded",
1166
        "Q" => "catchup_all",
1167
        "x" => "cat_toggle_collapse",
1168
        "g a" => "goto_all",
1169
        "g f" => "goto_fresh",
1170
        "g s" => "goto_marked",
1171
        "g p" => "goto_published",
1172
        "g r" => "goto_read",
1173
        "g t" => "goto_tagcloud",
1174
        "g P" => "goto_prefs",
1175
        "r" => "select_article_cursor",
1176
        "c l" => "create_label",
1177
        "c f" => "create_filter",
1178
        "c s" => "collapse_sidebar",
1179
        "?" => "help_dialog",
1180
    );
1181
1182
    foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_HOTKEY_MAP) as $plugin) {
1183
        $hotkeys = $plugin->hook_hotkey_map($hotkeys);
1184
    }
1185
1186
    $prefixes = array();
1187
1188
    foreach (array_keys($hotkeys) as $hotkey) {
1189
        $pair = explode(" ", $hotkey, 2);
1190
1191
        if (count($pair) > 1 && !in_array($pair[0], $prefixes)) {
1192
            array_push($prefixes, $pair[0]);
1193
        }
1194
    }
1195
1196
    return array($prefixes, $hotkeys);
1197
}
1198
1199
function make_runtime_info() {
1200
    $data = array();
1201
1202
    $pdo = Db::pdo();
1203
1204
    $sth = $pdo->prepare("SELECT MAX(id) AS mid, COUNT(*) AS nf FROM
1205
            ttrss_feeds WHERE owner_uid = ?");
1206
    $sth->execute([$_SESSION['uid']]);
1207
    $row = $sth->fetch();
1208
1209
    $max_feed_id = $row['mid'];
1210
    $num_feeds = $row['nf'];
1211
1212
    $data["max_feed_id"] = (int) $max_feed_id;
1213
    $data["num_feeds"] = (int) $num_feeds;
1214
    $data['cdm_expanded'] = get_pref('CDM_EXPANDED');
1215
    $data["labels"] = Labels::get_all_labels($_SESSION["uid"]);
1216
1217
    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...
1218
        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...
1219
            $log_interval = "created_at > NOW() - interval '1 hour'";
1220
        } else {
1221
            $log_interval = "created_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)";
1222
        }
1223
1224
        $sth = $pdo->prepare("SELECT COUNT(id) AS cid FROM ttrss_error_log WHERE $log_interval");
1225
        $sth->execute();
1226
1227
        if ($row = $sth->fetch()) {
1228
            $data['recent_log_events'] = $row['cid'];
1229
        }
1230
    }
1231
1232
    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...
1233
1234
        $data['daemon_is_running'] = (int) file_is_locked("update_daemon.lock");
1235
1236
        if (time() - $_SESSION["daemon_stamp_check"] > 30) {
1237
1238
            $stamp = (int) @file_get_contents(LOCK_DIRECTORY."/update_daemon.stamp");
1239
1240
            if ($stamp) {
1241
                $stamp_delta = time() - $stamp;
1242
1243
                if ($stamp_delta > 1800) {
1244
                    $stamp_check = 0;
1245
                } else {
1246
                    $stamp_check = 1;
1247
                    $_SESSION["daemon_stamp_check"] = time();
1248
                }
1249
1250
                $data['daemon_stamp_ok'] = $stamp_check;
1251
1252
                $stamp_fmt = date("Y.m.d, G:i", $stamp);
1253
1254
                $data['daemon_stamp'] = $stamp_fmt;
1255
            }
1256
        }
1257
    }
1258
1259
    return $data;
1260
}
1261
1262
function iframe_whitelisted($entry) {
1263
    @$src = parse_url($entry->getAttribute("src"), PHP_URL_HOST);
1264
1265
    if ($src) {
1266
        foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_IFRAME_WHITELISTED) as $plugin) {
1267
            if ($plugin->hook_iframe_whitelisted($src)) {
1268
                            return true;
1269
            }
1270
        }
1271
    }
1272
1273
    return false;
1274
}
1275
1276
function sanitize($str, $force_remove_images = false, $owner = false, $site_url = false, $highlight_words = false, $article_id = false) {
1277
    if (!$owner) {
1278
        $owner = $_SESSION["uid"];
1279
    }
1280
1281
    $res = trim($str); if (!$res) {
1282
        return '';
1283
    }
1284
1285
    $doc = new DOMDocument();
1286
    $doc->loadHTML('<?xml encoding="UTF-8">'.$res);
1287
    $xpath = new DOMXPath($doc);
1288
1289
    $rewrite_base_url = $site_url ? $site_url : get_self_url_prefix();
1290
1291
    $entries = $xpath->query('(//a[@href]|//img[@src]|//video/source[@src]|//audio/source[@src]|//picture/source[@src])');
1292
1293
    foreach ($entries as $entry) {
1294
1295
        if ($entry->hasAttribute('href')) {
1296
            $entry->setAttribute('href',
1297
                rewrite_relative_url($rewrite_base_url, $entry->getAttribute('href')));
1298
1299
            $entry->setAttribute('rel', 'noopener noreferrer');
1300
        }
1301
1302
        if ($entry->hasAttribute('src')) {
1303
            $src = rewrite_relative_url($rewrite_base_url, $entry->getAttribute('src'));
1304
            $entry->setAttribute('src', $src);
1305
        }
1306
1307
        if ($entry->nodeName == 'img') {
1308
            $entry->setAttribute('referrerpolicy', 'no-referrer');
1309
1310
            $entry->removeAttribute('width');
1311
            $entry->removeAttribute('height');
1312
1313
            if ($entry->hasAttribute('src')) {
1314
                $is_https_url = parse_url($entry->getAttribute('src'), PHP_URL_SCHEME) === 'https';
1315
1316
                if (is_prefix_https() && !$is_https_url) {
1317
1318
                    if ($entry->hasAttribute('srcset')) {
1319
                        $entry->removeAttribute('srcset');
1320
                    }
1321
1322
                    if ($entry->hasAttribute('sizes')) {
1323
                        $entry->removeAttribute('sizes');
1324
                    }
1325
                }
1326
            }
1327
        }
1328
1329
        if ($entry->hasAttribute('src') &&
1330
                ($owner && get_pref("STRIP_IMAGES", $owner)) || $force_remove_images || $_SESSION["bw_limit"]) {
1331
1332
            $p = $doc->createElement('p');
1333
1334
            $a = $doc->createElement('a');
1335
            $a->setAttribute('href', $entry->getAttribute('src'));
1336
1337
            $a->appendChild(new DOMText($entry->getAttribute('src')));
1338
            $a->setAttribute('target', '_blank');
1339
            $a->setAttribute('rel', 'noopener noreferrer');
1340
1341
            $p->appendChild($a);
1342
1343
            if ($entry->nodeName == 'source') {
1344
1345
                if ($entry->parentNode && $entry->parentNode->parentNode) {
1346
                                    $entry->parentNode->parentNode->replaceChild($p, $entry->parentNode);
1347
                }
1348
1349
            } else if ($entry->nodeName == 'img') {
1350
1351
                if ($entry->parentNode) {
1352
                                    $entry->parentNode->replaceChild($p, $entry);
1353
                }
1354
1355
            }
1356
        }
1357
1358
        if (strtolower($entry->nodeName) == "a") {
1359
            $entry->setAttribute("target", "_blank");
1360
            $entry->setAttribute("rel", "noopener noreferrer");
1361
        }
1362
    }
1363
1364
    $entries = $xpath->query('//iframe');
1365
    foreach ($entries as $entry) {
1366
        if (!iframe_whitelisted($entry)) {
1367
            $entry->setAttribute('sandbox', 'allow-scripts');
1368
        } else {
1369
            if (is_prefix_https()) {
1370
                $entry->setAttribute("src",
1371
                    str_replace("http://", "https://",
1372
                        $entry->getAttribute("src")));
1373
            }
1374
        }
1375
    }
1376
1377
    $allowed_elements = array('a', 'abbr', 'address', 'acronym', 'audio', 'article', 'aside',
1378
        'b', 'bdi', 'bdo', 'big', 'blockquote', 'body', 'br',
1379
        'caption', 'cite', 'center', 'code', 'col', 'colgroup',
1380
        'data', 'dd', 'del', 'details', 'description', 'dfn', 'div', 'dl', 'font',
1381
        'dt', 'em', 'footer', 'figure', 'figcaption',
1382
        'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hr', 'html', 'i',
1383
        'img', 'ins', 'kbd', 'li', 'main', 'mark', 'nav', 'noscript',
1384
        'ol', 'p', 'picture', 'pre', 'q', 'ruby', 'rp', 'rt', 's', 'samp', 'section',
1385
        'small', 'source', 'span', 'strike', 'strong', 'sub', 'summary',
1386
        'sup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'time',
1387
        'tr', 'track', 'tt', 'u', 'ul', 'var', 'wbr', 'video', 'xml:namespace');
1388
1389
    if ($_SESSION['hasSandbox']) {
1390
        $allowed_elements[] = 'iframe';
1391
    }
1392
1393
    $disallowed_attributes = array('id', 'style', 'class');
1394
1395
    foreach (PluginHost::getInstance()->get_hooks(PluginHost::HOOK_SANITIZE) as $plugin) {
1396
        $retval = $plugin->hook_sanitize($doc, $site_url, $allowed_elements, $disallowed_attributes, $article_id);
1397
        if (is_array($retval)) {
1398
            $doc = $retval[0];
1399
            $allowed_elements = $retval[1];
1400
            $disallowed_attributes = $retval[2];
1401
        } else {
1402
            $doc = $retval;
1403
        }
1404
    }
1405
1406
    $doc->removeChild($doc->firstChild); //remove doctype
1407
    $doc = strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes);
1408
1409
    if ($highlight_words) {
1410
        foreach ($highlight_words as $word) {
1411
1412
            // http://stackoverflow.com/questions/4081372/highlight-keywords-in-a-paragraph
1413
1414
            $elements = $xpath->query("//*/text()");
1415
1416
            foreach ($elements as $child) {
1417
1418
                $fragment = $doc->createDocumentFragment();
1419
                $text = $child->textContent;
1420
1421
                while (($pos = mb_stripos($text, $word)) !== false) {
1422
                    $fragment->appendChild(new DomText(mb_substr($text, 0, $pos)));
1423
                    $word = mb_substr($text, $pos, mb_strlen($word));
1424
                    $highlight = $doc->createElement('span');
1425
                    $highlight->appendChild(new DomText($word));
1426
                    $highlight->setAttribute('class', 'highlight');
1427
                    $fragment->appendChild($highlight);
1428
                    $text = mb_substr($text, $pos + mb_strlen($word));
1429
                }
1430
1431
                if (!empty($text)) {
1432
                    $fragment->appendChild(new DomText($text));
1433
                }
1434
1435
                $child->parentNode->replaceChild($fragment, $child);
1436
            }
1437
        }
1438
    }
1439
1440
    $res = $doc->saveHTML();
1441
1442
    /* strip everything outside of <body>...</body> */
1443
1444
    $res_frag = array();
1445
    if (preg_match('/<body>(.*)<\/body>/is', $res, $res_frag)) {
1446
        return $res_frag[1];
1447
    } else {
1448
        return $res;
1449
    }
1450
}
1451
1452
function strip_harmful_tags($doc, $allowed_elements, $disallowed_attributes) {
1453
    $xpath = new DOMXPath($doc);
1454
    $entries = $xpath->query('//*');
1455
1456
    foreach ($entries as $entry) {
1457
        if (!in_array($entry->nodeName, $allowed_elements)) {
1458
            $entry->parentNode->removeChild($entry);
1459
        }
1460
1461
        if ($entry->hasAttributes()) {
1462
            $attrs_to_remove = array();
1463
1464
            foreach ($entry->attributes as $attr) {
1465
1466
                if (strpos($attr->nodeName, 'on') === 0) {
1467
                    array_push($attrs_to_remove, $attr);
1468
                }
1469
1470
                if (strpos($attr->nodeName, "data-") === 0) {
1471
                    array_push($attrs_to_remove, $attr);
1472
                }
1473
1474
                if ($attr->nodeName == 'href' && stripos($attr->value, 'javascript:') === 0) {
1475
                    array_push($attrs_to_remove, $attr);
1476
                }
1477
1478
                if (in_array($attr->nodeName, $disallowed_attributes)) {
1479
                    array_push($attrs_to_remove, $attr);
1480
                }
1481
            }
1482
1483
            foreach ($attrs_to_remove as $attr) {
1484
                $entry->removeAttributeNode($attr);
1485
            }
1486
        }
1487
    }
1488
1489
    return $doc;
1490
}
1491
1492
function trim_array($array) {
1493
    $tmp = $array;
1494
    array_walk($tmp, 'trim');
1495
    return $tmp;
1496
}
1497
1498
function render_login_form() {
1499
    header('Cache-Control: public');
1500
1501
    require_once "login_form.php";
1502
    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...
1503
}
1504
1505
function T_sprintf() {
1506
    $args = func_get_args();
1507
    return vsprintf(__(array_shift($args)), $args);
1508
}
1509
1510
function print_checkpoint($n, $s) {
1511
    $ts = microtime(true);
1512
    echo sprintf("<!-- CP[$n] %.4f seconds -->\n", $ts - $s);
1513
    return $ts;
1514
}
1515
1516
function is_server_https() {
1517
    return (!empty($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] != 'off')) || $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https';
1518
}
1519
1520
function is_prefix_https() {
1521
    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...
1522
}
1523
1524
// this returns SELF_URL_PATH sans ending slash
1525
function get_self_url_prefix() {
1526
    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...
1527
        return substr(SELF_URL_PATH, 0, strlen(SELF_URL_PATH) - 1);
1528
    } else {
1529
        return SELF_URL_PATH;
1530
    }
1531
}
1532
1533
/* TODO: This needs to use bcrypt */
1534
function encrypt_password($pass, $salt = '', $mode2 = false) {
1535
    if ($salt && $mode2) {
1536
        return "MODE2:".hash('sha256', $salt.$pass);
1537
    } else if ($salt) {
1538
        return "SHA1X:".sha1("$salt:$pass");
1539
    } else {
1540
        return "SHA1:".sha1($pass);
1541
    }
1542
}
1543
1544
function init_plugins() {
1545
    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...
1546
1547
    return true;
1548
}
1549
1550
function build_url($parts) {
1551
    return $parts['scheme']."://".$parts['host'].$parts['path'];
1552
}
1553
1554
function cleanup_url_path($path) {
1555
    $path = str_replace("/./", "/", $path);
1556
    $path = str_replace("//", "/", $path);
1557
1558
    return $path;
1559
}
1560
1561
/**
1562
 * Converts a (possibly) relative URL to a absolute one.
1563
 *
1564
 * @param string $url     Base URL (i.e. from where the document is)
1565
 * @param string $rel_url Possibly relative URL in the document
1566
 *
1567
 * @return string Absolute URL
1568
 */
1569
function rewrite_relative_url($url, $rel_url) {
1570
    if (strpos($rel_url, "://") !== false) {
1571
        return $rel_url;
1572
    } else if (strpos($rel_url, "//") === 0) {
1573
        # protocol-relative URL (rare but they exist)
1574
        return $rel_url;
1575
    } else if (preg_match("/^[a-z]+:/i", $rel_url)) {
1576
        # magnet:, feed:, etc
1577
        return $rel_url;
1578
    } else if (strpos($rel_url, "/") === 0) {
1579
        $parts = parse_url($url);
1580
        $parts['path'] = $rel_url;
1581
        $parts['path'] = cleanup_url_path($parts['path']);
1582
1583
        return build_url($parts);
1584
1585
    } else {
1586
        $parts = parse_url($url);
1587
        if (!isset($parts['path'])) {
1588
            $parts['path'] = '/';
1589
        }
1590
        $dir = $parts['path'];
1591
        if (substr($dir, -1) !== '/') {
1592
            $dir = dirname($parts['path']);
1593
            $dir !== '/' && $dir .= '/';
1594
        }
1595
        $parts['path'] = $dir.$rel_url;
1596
        $parts['path'] = cleanup_url_path($parts['path']);
1597
1598
        return build_url($parts);
1599
    }
1600
}
1601
1602
function print_user_stylesheet() {
1603
    $value = get_pref('USER_STYLESHEET');
1604
1605
    if ($value) {
1606
        print "<style type='text/css' id='user_css_style'>";
1607
        print str_replace("<br/>", "\n", $value);
1608
        print "</style>";
1609
    }
1610
}
1611
1612
if (!function_exists('gzdecode')) {
1613
    function gzdecode($string) {
1614
        return file_get_contents('compress.zlib://data:who/cares;base64,'.base64_encode($string));
1615
    }
1616
}
1617
1618
function get_random_bytes($length) {
1619
    if (function_exists('openssl_random_pseudo_bytes')) {
1620
        return openssl_random_pseudo_bytes($length);
1621
    } else {
1622
        $output = "";
1623
1624
        for ($i = 0; $i < $length; $i++) {
1625
                    $output .= chr(mt_rand(0, 255));
1626
        }
1627
1628
        return $output;
1629
    }
1630
}
1631
1632
function read_stdin() {
1633
    $fp = fopen("php://stdin", "r");
1634
1635
    if ($fp) {
0 ignored issues
show
introduced by
$fp is of type false|resource, thus it always evaluated to false.
Loading history...
1636
        $line = trim(fgets($fp));
1637
        fclose($fp);
1638
        return $line;
1639
    }
1640
1641
    return null;
1642
}
1643
1644
function implements_interface($class, $interface) {
1645
    return in_array($interface, class_implements($class));
1646
}
1647
1648
function T_js_decl($s1, $s2) {
1649
    if ($s1 && $s2) {
1650
        $s1 = preg_replace("/\n/", "", $s1);
1651
        $s2 = preg_replace("/\n/", "", $s2);
1652
1653
        $s1 = preg_replace("/\"/", "\\\"", $s1);
1654
        $s2 = preg_replace("/\"/", "\\\"", $s2);
1655
1656
        return "T_messages[\"$s1\"] = \"$s2\";\n";
1657
    }
1658
}
1659
1660
function init_js_translations() {
1661
1662
    print 'var T_messages = new Object();
1663
1664
        public function __(msg) {
1665
            if (T_messages[msg]) {
1666
                return T_messages[msg];
1667
            } else {
1668
                return msg;
1669
            }
1670
        }
1671
1672
        public function ngettext(msg1, msg2, n) {
1673
            return __((parseInt(n) > 1) ? msg2 : msg1);
1674
        }';
1675
1676
    global $text_domains;
1677
1678
    foreach (array_keys($text_domains) as $domain) {
1679
        $l10n = _get_reader($domain);
1680
1681
        for ($i = 0; $i < $l10n->total; $i++) {
1682
            $orig = $l10n->get_original_string($i);
1683
            if (strpos($orig, "\000") !== false) { // Plural forms
1684
                $key = explode(chr(0), $orig);
1685
                print T_js_decl($key[0], _ngettext($key[0], $key[1], 1)); // Singular
1686
                print T_js_decl($key[1], _ngettext($key[0], $key[1], 2)); // Plural
1687
            } else {
1688
                $translation = _dgettext($domain, $orig);
1689
                print T_js_decl($orig, $translation);
1690
            }
1691
        }
1692
    }
1693
}
1694
1695
function get_theme_path($theme) {
1696
    if ($theme == "default.php") {
1697
            return "css/default.css";
1698
    }
1699
1700
    $check = "themes/$theme";
1701
    if (file_exists($check)) {
1702
        return $check;
1703
    }
1704
1705
    $check = "themes.local/$theme";
1706
    if (file_exists($check)) {
1707
        return $check;
1708
    }
1709
    }
1710
1711
function theme_exists($theme) {
1712
    return file_exists("themes/$theme") || file_exists("themes.local/$theme");
1713
}
1714
1715
function error_json($code) {
1716
    require_once "errors.php";
1717
1718
    @$message = $ERRORS[$code];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ERRORS seems to be never defined.
Loading history...
1719
1720
    return json_encode(
1721
        array(
1722
            "error" => array(
1723
                "code" => $code,
1724
                "message" => $message
1725
            )
1726
        )
1727
    );
1728
}
1729
1730
function get_upload_error_message($code) {
1731
    $errors = array(
1732
        0 => __('There is no error, the file uploaded with success'),
1733
        1 => __('The uploaded file exceeds the upload_max_filesize directive in php.ini'),
1734
        2 => __('The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form'),
1735
        3 => __('The uploaded file was only partially uploaded'),
1736
        4 => __('No file was uploaded'),
1737
        6 => __('Missing a temporary folder'),
1738
        7 => __('Failed to write file to disk.'),
1739
        8 => __('A PHP extension stopped the file upload.'),
1740
    );
1741
1742
    return $errors[$code];
1743
}
1744
1745
function base64_img($filename) {
1746
    if (file_exists($filename)) {
1747
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
1748
1749
        return "data:image/$ext;base64,".base64_encode(file_get_contents($filename));
1750
    } else {
1751
        return "";
1752
    }
1753
}
1754
1755
/*	this is essentially a wrapper for readfile() which allows plugins to hook
1756
    output with httpd-specific "fast" implementation i.e. X-Sendfile or whatever else
1757
1758
    hook function should return true if request was handled (or at least attempted to)
1759
1760
    note that this can be called without user context so the plugin to handle this
1761
    should be loaded systemwide in config.php */
1762
function send_local_file($filename) {
1763
    if (file_exists($filename)) {
1764
1765
        if (is_writable($filename)) {
1766
            touch($filename);
1767
        }
1768
1769
        $tmppluginhost = new PluginHost();
1770
1771
        $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...
1772
        $tmppluginhost->load_data();
1773
1774
        foreach ($tmppluginhost->get_hooks(PluginHost::HOOK_SEND_LOCAL_FILE) as $plugin) {
1775
            if ($plugin->hook_send_local_file($filename)) {
1776
                return true;
1777
            }
1778
        }
1779
1780
        $mimetype = mime_content_type($filename);
1781
1782
        // this is hardly ideal but 1) only media is cached in images/ and 2) seemingly only mp4
1783
        // video files are detected as octet-stream by mime_content_type()
1784
1785
        if ($mimetype == "application/octet-stream") {
1786
                    $mimetype = "video/mp4";
1787
        }
1788
1789
        header("Content-type: $mimetype");
1790
1791
        $stamp = gmdate("D, d M Y H:i:s", filemtime($filename))." GMT";
1792
        header("Last-Modified: $stamp", true);
1793
1794
        return readfile($filename);
1795
    } else {
1796
        return false;
1797
    }
1798
}
1799
1800
function arr_qmarks($arr) {
1801
    return str_repeat('?,', count($arr) - 1).'?';
1802
}
1803
1804
function get_scripts_timestamp() {
1805
    $files = glob("js/*.js");
1806
    $ts = 0;
1807
1808
    foreach ($files as $file) {
1809
        $file_ts = filemtime($file);
1810
        if ($file_ts > $ts) {
1811
            $ts = $file_ts;
1812
        }
1813
    }
1814
1815
    return $ts;
1816
}
1817
1818
/* for package maintainers who don't use git: if version_static.txt exists in tt-rss root
1819
    directory, its contents are displayed instead of git commit-based version, this could be generated
1820
    based on source git tree commit used when creating the package */
1821
1822
function get_version(&$git_commit = false, &$git_timestamp = false) {
1823
    global $ttrss_version;
1824
1825
    if (isset($ttrss_version)) {
1826
            return $ttrss_version;
1827
    }
1828
1829
    $ttrss_version = "UNKNOWN (Unsupported)";
1830
1831
    date_default_timezone_set('UTC');
1832
    $root_dir = dirname(dirname(__FILE__));
1833
1834
    if ('\\' === DIRECTORY_SEPARATOR) {
1835
        $ttrss_version = "UNKNOWN (Unsupported, Windows)";
1836
    } else if (PHP_OS === "Darwin") {
1837
        $ttrss_version = "UNKNOWN (Unsupported, Darwin)";
1838
    } else if (file_exists("$root_dir/version_static.txt")) {
1839
        $ttrss_version = trim(file_get_contents("$root_dir/version_static.txt"))." (Unsupported)";
1840
    } else if (is_dir("$root_dir/.git")) {
1841
        $rc = 0;
1842
        $output = [];
1843
1844
        $cwd = getcwd();
1845
1846
        chdir($root_dir);
1847
        exec('git log --pretty='.escapeshellarg('%ct %h').' -n1 HEAD 2>&1', $output, $rc);
1848
        chdir($cwd);
1849
1850
        if ($rc == 0) {
1851
            if (is_array($output) && count($output) > 0) {
1852
                [$timestamp, $commit] = explode(" ", $output[0], 2);
1853
1854
                $git_commit = $commit;
1855
                $git_timestamp = $timestamp;
1856
1857
                $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

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