dieGif()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 9
nc 3
nop 1
dl 0
loc 18
ccs 2
cts 2
cp 1
crap 3
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to, well, everything.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
use ElkArte\Cache\Cache;
18
use ElkArte\Debug;
19
use ElkArte\Helper\Censor;
20
use ElkArte\Helper\ConstructPageIndex;
21
use ElkArte\Helper\GenericList;
22
use ElkArte\Helper\TokenHash;
23
use ElkArte\Helper\Util;
24
use ElkArte\Hooks;
25
use ElkArte\Http\Headers;
26
use ElkArte\Languages\Loader;
27
use ElkArte\Notifications\Notifications;
28
use ElkArte\Request;
29
use ElkArte\Search\Search;
30
use ElkArte\Themes\DefaultTheme\Theme;
31
use ElkArte\UrlGenerator\UrlGenerator;
32
use ElkArte\User;
33
34
/**
35
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
36
 *
37
 * What it does:
38
 *
39
 * - Updates both the settings table and $modSettings array.
40
 * - All of changeArray's indexes and values are assumed to have escaped apostrophes (')!
41
 * - If a variable is already set to what you want to change it to, that
42
 *   Variable will be skipped over; it would be unnecessary to reset.
43
 * - When update is true, UPDATEs will be used instead of REPLACE.
44
 * - When update is true, the value can be true or false to increment
45
 *  or decrement it, respectively.
46 47
 *
47
 * @param array $changeArray An associative array of what we're changing in 'setting' => 'value' format
48 47
 * @param bool $update Use an UPDATE query instead of a REPLACE query
49 47
 */
50
function updateSettings($changeArray, $update = false)
51 47
{
52
	global $modSettings;
53
54
	$db = database();
55
	$cache = Cache::instance();
56
57 47
	if (empty($changeArray) || !is_array($changeArray))
58
	{
59 28
		return;
60
	}
61 28
62
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
63 28
	if ($update)
64
	{
65
		foreach ($changeArray as $variable => $value)
66 28
		{
67 28
			$db->query('', '
68
				UPDATE {db_prefix}settings
69
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
70
				WHERE variable = {string:variable}',
71 28
				[
72
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
73
					'variable' => $variable,
74
				]
75 28
			);
76
77 28
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
78
		}
79
80 35
		// Clean out the cache and make sure the cobwebs are gone too.
81 35
		$cache->remove('modSettings');
82
83
		return;
84 35
	}
85
86 14
	$replaceArray = [];
87
	foreach ($changeArray as $variable => $value)
88
	{
89
		// Don't bother if it's already like that ;).
90 35
		if (isset($modSettings[$variable]) && $modSettings[$variable] === $value)
91
		{
92
			continue;
93
		}
94
95 35
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
96
		if (!isset($modSettings[$variable]) && empty($value))
97 35
		{
98
			continue;
99
		}
100 35
101
		$replaceArray[] = [$variable, $value];
102 8
103
		$modSettings[$variable] = $value;
104
	}
105 35
106 35
	if (empty($replaceArray))
107 35
	{
108 15
		return;
109 35
	}
110
111
	$db->replace(
112
		'{db_prefix}settings',
113 35
		['variable' => 'string-255', 'value' => 'string-65534'],
114 35
		$replaceArray,
115
		['variable']
116
	);
117
118
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
119
	$cache->remove('modSettings');
120
}
121
122
/**
123 3
 * Deletes one setting from the settings table and takes care of $modSettings as well
124
 *
125 3
 * @param string|string[] $toRemove the setting or the settings to be removed
126
 */
127 3
function removeSettings($toRemove)
128
{
129
	global $modSettings;
130
131
	$db = database();
132 3
133
	if (empty($toRemove))
134 3
	{
135
		return;
136
	}
137
138 3
	if (!is_array($toRemove))
139
	{
140
		$toRemove = [$toRemove];
141
	}
142 3
143
	// Remove the setting from the db
144
	$db->query('', '
145
		DELETE FROM {db_prefix}settings
146
		WHERE variable IN ({array_string:setting_name})',
147 3
		[
148
			'setting_name' => $toRemove,
149 3
		]
150
	);
151 2
152
	// Remove it from $modSettings now so it does not persist
153
	foreach ($toRemove as $setting)
154
	{
155
		if (isset($modSettings[$setting]))
156 3
		{
157 3
			unset($modSettings[$setting]);
158
		}
159
	}
160
161
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
162
	Cache::instance()->remove('modSettings');
163
}
164
165
/**
166
 * Constructs a page list.
167
 *
168
 * @depreciated since 2.0
169
 *
170
 */
171
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show = [])
172
{
173
	$pageindex = new ConstructPageIndex($base_url, $start, $max_value, $num_per_page, $flexible_start, $show);
174
	return $pageindex->getPageIndex();
175
}
176
177
/**
178
 * Formats a number.
179
 *
180
 * What it does:
181
 *
182
 * - Uses the format of number_format to decide how to format the number.
183
 *   for example, it might display "1 234,50".
184
 * - Caches the formatting data from the setting for optimization.
185
 *
186
 * @param float $number The float value to apply comma formatting
187
 * @param int|bool $override_decimal_count = false or number of decimals
188
 *
189 18
 * @return string
190
 */
191
function comma_format($number, $override_decimal_count = false)
192 18
{
193 18
	global $txt;
194
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
195 18
196
	// Cache these values...
197
	if ($decimal_separator === null)
198
	{
199 18
		// Not set for whatever reason?
200
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
201
		{
202 18
			return $number;
203
		}
204
205
		// Cache these each load...
206
		$thousands_separator = $matches[1];
207 18
		$decimal_separator = $matches[2];
208
		$decimal_count = strlen($matches[3]);
209 6
	}
210
211
	// Format the string with our friend, number_format.
212
	$decimals = ((float) $number === $number) ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0;
213
	return number_format((float) $number, (int) $decimals, $decimal_separator, $thousands_separator);
214 12
}
215
216
/**
217 18
 * Formats a number to a multiple of thousands x, x k, x M, x G, x T
218
 *
219 18
 * @param float $number The value to format
220
 * @param int|bool $override_decimal_count = false or number of decimals
221
 *
222 18
 * @return string
223
 */
224
function thousands_format($number, $override_decimal_count = false)
225
{
226
	foreach (['', ' k', ' M', ' G', ' T'] as $kb)
227
	{
228
		if ($number < 1000)
229
		{
230
			break;
231
		}
232
233
		$number /= 1000;
234
	}
235
236
	return comma_format($number, $override_decimal_count) . $kb;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $kb seems to be defined by a foreach iteration on line 226. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
237
}
238
239
/**
240
 * Formats a number to a computer byte size value xB, xKB, xMB, xGB
241
 *
242
 * @param int $number
243
 *
244 18
 * @return string
245
 */
246
function byte_format($number)
247 18
{
248
	global $txt;
249
250
	foreach (['byte', 'kilobyte', 'megabyte', 'gigabyte'] as $kb)
251
	{
252
		if ($number < 1024)
253 18
		{
254
			break;
255
		}
256
257 18
		$number /= 1024;
258
	}
259
260
	return comma_format($number) . ' ' . $txt[$kb];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $kb seems to be defined by a foreach iteration on line 250. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
261
}
262
263 18
/**
264
 * Format a time to make it look purdy.
265
 *
266
 * What it does:
267
 *
268
 * - Returns a pretty formatted version of time based on the user's format in User::$info->time_format.
269
 * - Applies all necessary time offsets to the timestamp, unless offset_type is set.
270
 * - If todayMod is set and show_today was not specified or true, an
271
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
272
 * - Performs localization (more than just strftime would do alone.)
273
 *
274
 * @param int $log_time A unix timestamp
275
 * @param string|bool $show_today = true show "Today"/"Yesterday",
276
 *   false shows the date, a string can force a date format to use %b %d, %Y
277
 * @param string|bool $offset_type = false If false, uses both user time offset and forum offset.
278
 *   If 'forum', uses only the forum offset. Otherwise no offset is applied.
279
 *
280
 * @return string
281
 */
282 18
function standardTime($log_time, $show_today = true, $offset_type = false)
283
{
284 18
	global $txt, $modSettings;
285
	static $non_twelve_hour, $is_win = null;
286
287
	if ($is_win === null)
288
	{
289
		$is_win = detectServer()->is('windows');
290
	}
291
292 18
	// Offset the time.
293
	if (!$offset_type)
294 18
	{
295
		$time = $log_time + (User::$info->time_offset + $modSettings['time_offset']) * 3600;
0 ignored issues
show
Bug Best Practice introduced by
The property time_offset does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
296
	}
297
	// Just the forum offset?
298
	elseif ($offset_type === 'forum')
299
	{
300
		$time = $log_time + $modSettings['time_offset'] * 3600;
301
	}
302 18
	else
303 18
	{
304
		$time = $log_time;
305 18
	}
306
307
	// We can't have a negative date (on Windows, at least.)
308
	if ($log_time < 0)
309
	{
310
		$log_time = 0;
311
	}
312
313 18
	// Today and Yesterday?
314
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
315
	{
316
		// Get the current time.
317
		$nowtime = forum_time();
318
319
		$then = @getdate($time);
320
		$now = @getdate($nowtime);
321
322
		// Try to make something of a time format string...
323
		$s = strpos(User::$info->time_format, '%S') === false ? '' : ':%S';
0 ignored issues
show
Bug Best Practice introduced by
The property time_format does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
It seems like ElkArte\User::info->time_format can also be of type null; however, parameter $haystack of strpos() 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

323
		$s = strpos(/** @scrutinizer ignore-type */ User::$info->time_format, '%S') === false ? '' : ':%S';
Loading history...
324
		if (strpos(User::$info->time_format, '%H') === false && strpos(User::$info->time_format, '%T') === false)
325
		{
326
			$h = strpos(User::$info->time_format, '%l') === false ? '%I' : '%l';
327
			$today_fmt = $h . ':%M' . $s . ' %p';
328
		}
329
		else
330
		{
331
			$today_fmt = '%H:%M' . $s;
332 18
		}
333
334
		// Same day of the year, same year.... Today!
335
		if ($then['yday'] === $now['yday'] && $then['year'] === $now['year'])
336
		{
337
			return sprintf($txt['today'], standardTime($log_time, $today_fmt, $offset_type));
338 18
		}
339
340
		// Day-of-year is one less and same year, or it's the first of the year and that's the last of the year...
341
		if ((int) $modSettings['todayMod'] === 2
342
			&& (($then['yday'] === $now['yday'] - 1 && $then['year'] === $now['year'])
343
				|| (($now['yday'] === 0 && $then['year'] === $now['year'] - 1) && $then['mon'] === 12 && $then['mday'] === 31)))
344
		{
345 18
			return sprintf($txt['yesterday'], standardTime($log_time, $today_fmt, $offset_type));
346
		}
347
	}
348
349
	$str = is_bool($show_today) ? User::$info->time_format : $show_today;
350
351
	// Windows requires a slightly different language code identifier (LCID).
352
	// https://msdn.microsoft.com/en-us/library/cc233982.aspx
353
	if ($is_win)
354
	{
355
		$txt['lang_locale'] = str_replace('_', '-', $txt['lang_locale']);
356
	}
357 18
358
	if (setlocale(LC_TIME, $txt['lang_locale']))
359
	{
360
		if (!isset($non_twelve_hour))
361
		{
362
			$non_twelve_hour = trim(Util::strftime('%p')) === '';
363
		}
364
365
		if ($non_twelve_hour && strpos($str, '%p') !== false)
366
		{
367
			$str = str_replace('%p', (Util::strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
368
		}
369
370
		foreach (['%a', '%A', '%b', '%B'] as $token)
371
		{
372
			if (strpos($str, $token) !== false)
373
			{
374
				$str = str_replace($token, empty($txt['lang_capitalize_dates']) ? Util::strftime($token, $time) : Util::ucwords(Util::strftime($token, $time)), $str);
375
			}
376 10
		}
377 10
	}
378
	else
379
	{
380 10
		// Do-it-yourself time localization.  Fun.
381
		foreach (['%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months'] as $token => $text_label)
382
		{
383 2
			if (strpos($str, $token) !== false)
384
			{
385
				$str = str_replace($token, $txt[$text_label][(int) Util::strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
386
			}
387
		}
388
389 2
		if (strpos($str, '%p') !== false)
390 2
		{
391 2
			$str = str_replace('%p', (Util::strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
392
		}
393
	}
394
395 10
	// Windows doesn't support %e; on some versions, strftime fails altogether if used, so let's prevent that.
396
	if ($is_win && strpos($str, '%e') !== false)
397
	{
398
		$str = str_replace('%e', ltrim(Util::strftime('%d', $time), '0'), $str);
399
	}
400
401
	// Format any other characters..
402
	return Util::strftime($str, $time);
403
}
404
405
/**
406
 * Used to render a timestamp to html5 <time> tag format.
407
 *
408
 * @param int $timestamp A unix timestamp
409
 *
410
 * @return string
411
 */
412
function htmlTime($timestamp)
413
{
414
	global $txt, $context;
415
416
	if (empty($timestamp))
417
	{
418
		return '';
419
	}
420
421
	$forumtime = forum_time(false, $timestamp);
422
	$timestamp = forum_time(true, $timestamp);
423
	$time = date('Y-m-d H:i', $timestamp);
424
	$stdtime = standardTime($timestamp, true, true);
425
426
	// @todo maybe htmlspecialchars on the title attribute?
427
	return '<time title="' . (empty($context['using_relative_time']) ? $txt['last_post'] : $stdtime) . '" datetime="' . $time . '" data-timestamp="' . $timestamp . '" data-forumtime="' . $forumtime . '">' . $stdtime . '</time>';
428
}
429
430 2
/**
431
 * Convert a given timestamp to UTC time in the format of Atom date format.
432 2
 *
433 2
 * This method takes a unix timestamp as input and converts it to UTC time in the format of
434
 * Atom date format (YYYY-MM-DDTHH:MM:SS+00:00).
435 2
 *
436
 * It considers the user's time offset, system's time offset, and the default timezone setting
437 2
 * from the modifications/settings administration panel.
438
 *
439
 * @param int $timestamp The timestamp to convert to UTC time.
440 2
 * @param int $userAdjust The timestamp is not to be adjusted for user offset
441
 * @return string The UTC time in the format of Atom date format.
442
 */
443 2
function utcTime($timestamp, $userAdjust = false)
444
{
445
	global $user_info, $modSettings;
446
447
	// Back out user time
448
	if ($userAdjust === true && !empty(User::$info->time_offset))
0 ignored issues
show
Bug Best Practice introduced by
The property time_offset does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
introduced by
The condition $userAdjust === true is always false.
Loading history...
449
	{
450
		$timestamp -= ($modSettings['time_offset'] + User::$info->time_offset) * 3600;
451
	}
452
453
	// Using the system timezone offset, format the date
454
	try
455
	{
456
		$tz = empty($modSettings['default_timezone']) ? 'UTC' : $modSettings['default_timezone'];
457
		$date = new DateTime('@' . $timestamp, new DateTimeZone($tz));
458
	}
459
	catch (Exception)
460
	{
461
		return standardTime($timestamp);
462
	}
463
464
	// Something like 2012-12-21T11:11:00+00:00
465
	return $date->format(DateTimeInterface::ATOM);
466
}
467 22
468 22
/**
469
 * Gets the current time with offset.
470 22
 *
471
 * What it does:
472 2
 *
473
 * - Always applies the offset in the time_offset setting.
474
 *
475
 * @param bool $use_user_offset = true if use_user_offset is true, applies the user's offset as well
476 22
 * @param int|null $timestamp = null A unix timestamp (null to use current time)
477
 *
478 16
 * @return int seconds since the unix epoch
479
 */
480
function forum_time($use_user_offset = true, $timestamp = null)
481 18
{
482
	global $modSettings;
483
484
	if ($timestamp === null)
485
	{
486
		$timestamp = time();
487 18
	}
488
	elseif ($timestamp === 0)
489
	{
490
		return 0;
491 22
	}
492
493
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? User::$info->time_offset : 0)) * 3600;
0 ignored issues
show
Bug Best Practice introduced by
The property time_offset does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
494
}
495
496
/**
497 22
 * Removes special entities from strings.  Compatibility...
498
 *
499
 * - Faster than html_entity_decode
500 22
 * - Removes the base entities ( &amp; &quot; &#039; &lt; and &gt;. ) from text with htmlspecialchars_decode
501
 * - Additionally converts &nbsp with str_replace
502 22
 *
503 22
 * @param string $string The string to apply htmlspecialchars_decode
504
 *
505
 * @return string string without entities
506 22
 */
507 22
function un_htmlspecialchars($string)
508
{
509 22
	if (empty($string))
510 22
	{
511
		return $string;
512
	}
513
514
	$string = htmlspecialchars_decode($string, ENT_QUOTES);
515
516
	return str_replace('&nbsp;', ' ', $string);
517
}
518 22
519
/**
520 16
 * Lexicographic permutation function.
521
 *
522
 * This is a special type of permutation which involves the order of the set. The next
523
 * lexicographic permutation of '32541' is '34125'. Numerically, it is simply the smallest
524 8
 * set larger than the current one.
525
 *
526
 * The benefit of this over a recursive solution is that the whole list does NOT need
527
 * to be held in memory. So it's actually possible to run 30! permutations without
528
 * causing a memory overflow.
529
 *
530 22
 * Source: O'Reilly PHP Cookbook
531
 *
532
 * @param array $p The array keys to apply permutation
533
 * @param int $size The size of our permutation array
534 22
 *
535
 * @return array|bool the next permutation of the passed array $p
536
 */
537
function pc_next_permutation($p, $size)
538
{
539 22
	// Slide down the array looking for where we're smaller than the next guy
540
	for ($i = $size - 1; isset($p[$i]) && $p[$i] >= $p[$i + 1]; --$i)
541
	{
542
		// Required to set $i
543
	}
544
545
	// If this doesn't occur, we've finished our permutations
546
	// the array is reversed: (1, 2, 3, 4) => (4, 3, 2, 1)
547
	if ($i === -1)
548
	{
549
		return false;
550
	}
551
552
	// Slide down the array looking for a bigger number than what we found before
553
	for ($j = $size; $p[$j] <= $p[$i]; --$j)
554
	{
555
		// Required to set $j
556
	}
557
558
	// Swap them
559
	$tmp = $p[$i];
560
	$p[$i] = $p[$j];
561 22
	$p[$j] = $tmp;
562
563 22
	// Now reverse the elements in between by swapping the ends
564
	for (++$i, $j = $size; $i < $j; ++$i, --$j)
565 11
	{
566
		$tmp = $p[$i];
567
		$p[$i] = $p[$j];
568
		$p[$j] = $tmp;
569 22
	}
570
571 16
	return $p;
572
}
573
574
/**
575
 * Ends execution and redirects the user to a new location
576 22
 *
577
 * What it does:
578
 *
579
 * - Makes sure the browser doesn't come back and repost the form data.
580
 * - Should be used whenever anything is posted.
581
 * - Diverts final execution to obExit() which means a end to processing and sending of final output
582 22
 *
583
 * @event integrate_redirect called before headers are sent
584
 * @param string $setLocation = '' The URL to redirect to
585
 *
586
 * @return void|string will return $setLocation string when run via the testbed
587
 */
588
function redirectexit($setLocation = '')
589
{
590
	global $db_show_debug;
591
592
	// Note to developers.  The testbed will automatically add the following, allowing phpunit test returns
593
	//if (defined("PHPUNITBOOTSTRAP") && defined("STDIN")){return $setLocation;} see setup-elkarte.sh
594 18
595
	// Send headers, call integration, do maintenance
596 18
	Headers::instance()
597
		->removeHeader('all')
598 2
		->redirect($setLocation)
599
		->send();
600
601 18
	// Debugging.
602 18
	if ($db_show_debug === true)
603 18
	{
604
		$_SESSION['debug_redirect'] = Debug::instance()->get_db();
605
	}
606 18
607
	obExit(false);
608
}
609
610
/**
611
 * Ends execution.
612
 *
613
 * What it does:
614
 *
615
 * - Takes care of template loading and remembering the previous URL.
616
 * - Calls ob_start() with ob_sessrewrite to fix URLs if necessary.
617
 *
618
 * @event integrate_invalid_old_url allows adding to "from" urls we don't save
619
 * @event integrate_exit inform portal, etc. that we're integrated with to exit
620
 * @param bool|null $header = null Output the header
621
 * @param bool|null $do_footer = null Output the footer
622
 * @param bool $from_index = false If we're coming from index.php
623 25
 * @param bool $from_fatal_error = false If we are exiting due to a fatal error
624
 * @throws \ElkArte\Exceptions\Exception
625 25
 */
626
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
627 25
{
628
	global $context, $txt, $db_show_debug;
629 22
630
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
631
632
	// Attempt to prevent a recursive loop.
633
	++$level;
634 25
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
635
	{
636
		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...
637
	}
638
639
	if ($from_fatal_error)
640
	{
641
		$has_fatal_error = true;
642
	}
643
644
	$do_header = $header ?? !$header_done;
645
	$do_footer = $do_footer ?? $do_header;
646
647
	// Has the template/header been done yet?
648
	if ($do_header)
649
	{
650 58
		handleMaintenance();
651
652 58
		// Was the page title set last minute? Also update the HTML safe one.
653
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
654
		{
655
			$context['page_title_html_safe'] = Util::htmlspecialchars(un_htmlspecialchars($context['page_title'])) . (empty($context['current_page']) ? '' : ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1));
656
		}
657
658
		// Start up the session URL fixer.
659
		ob_start('ob_sessrewrite');
660
661
		call_integration_buffer();
662
663
		// Display the screen in the logical order.
664
		template_header();
665
		$header_done = true;
666
	}
667
668
	if ($do_footer)
669
	{
670
		// Show the footer.
671
		theme()->getTemplates()->loadSubTemplate($context['sub_template'] ?? 'main');
672
673
		// Just so we don't get caught in an endless loop of errors from the footer...
674
		if (!$footer_done)
675
		{
676 4
			$footer_done = true;
677
			template_footer();
678
679
			// Add $db_show_debug = true; to Settings.php if you want to show the debugging information.
680
			// (since this is just debugging... it's okay that it's after </html>.)
681
			if (($db_show_debug === true)
682 4
				&& !isset($_REQUEST['api'])
683
				&& ((!isset($_GET['action']) || $_GET['action'] !== 'viewquery') && !isset($_GET['api'])))
684 4
			{
685
				Debug::instance()->display();
686
			}
687
		}
688 4
	}
689
690
	// Need user agent
691
	$req = Request::instance();
692
693 4
	setOldUrl();
694 4
695 4
	// For session check verification.... don't switch browsers...
696
	$_SESSION['USER_AGENT'] = $req->user_agent();
697
698 4
	// Hand off the output to the portal, etc. we're integrated with.
699
	call_integration_hook('integrate_exit', [$do_footer]);
700 4
701 4
	// Note to developers.  The testbed will add the following, allowing phpunit test returns
702 4
	//if (defined("PHPUNITBOOTSTRAP") && defined("STDIN")){return;}
703
704
	// Don't exit if we're coming from index.php; that will pass through normally.
705 4
	if (!$from_index)
706
	{
707
		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...
708
	}
709
}
710
711
/**
712
 * Takes care of a few dynamic maintenance items
713
 */
714
function handleMaintenance()
715
{
716
	global $context;
717
718
	// Clear out the stat cache.
719
	trackStats();
720
721
	// Send off any notifications accumulated
722
	Notifications::instance()->send();
723
724
	// Queue any mail that needs to be sent
725
	if (!empty($context['flush_mail']))
726 16
	{
727
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
728
		AddMailQueue(true);
729 16
	}
730
}
731
732
/**
733
 * @param string $index
734
 */
735 16
function setOldUrl($index = 'old_url')
736
{
737 16
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
738
	$invalid_old_url = [
739 16
		'action=dlattach',
740
		'action=jsoption',
741 16
		';api=xml',
742
	];
743
	call_integration_hook('integrate_invalid_old_url', [&$invalid_old_url]);
744
	$make_old = true;
745 16
	foreach ($invalid_old_url as $url)
746
	{
747 16
		if (strpos($_SERVER['REQUEST_URL'], $url) !== false)
748
		{
749
			$make_old = false;
750
			break;
751
		}
752
	}
753
754
	if ($make_old)
755
	{
756 16
		$_SESSION[$index] = $_SERVER['REQUEST_URL'];
757
	}
758
}
759 16
760
/**
761 16
 * Sets the class of the current topic based on is_very_hot, veryhot, hot, etc
762
 *
763
 * @param array $topic_context array of topic information
764
 */
765
function determineTopicClass(&$topic_context)
766
{
767
	$topic_context['class'] = empty($topic_context['is_poll']) ? 'i-normal' : 'i-poll';
768
769
	// Set topic class depending on locked status and number of replies.
770
	if ($topic_context['is_very_hot'])
771
	{
772
		$topic_context['class'] = 'i-hot colorize-red';
773
	}
774
	elseif ($topic_context['is_hot'])
775
	{
776
		$topic_context['class'] = 'i-hot colorize-yellow';
777
	}
778
779
	if ($topic_context['is_sticky'])
780
	{
781
		$topic_context['class'] = 'i-sticky';
782
	}
783
784
	if ($topic_context['is_locked'])
785
	{
786
		$topic_context['class'] = 'i-locked';
787
	}
788
}
789
790
/**
791
 * Sets up the basic theme context stuff.
792
 *
793
 * @param bool $forceload defaults to false
794
 */
795
function setupThemeContext($forceload = false)
796
{
797
	theme()->setupThemeContext($forceload);
798
}
799
800
/**
801
 * Helper function to convert memory string settings to bytes
802
 *
803
 * @param string|bool $val The byte string, like 256M or 1G
804
 *
805
 * @return int The string converted to a proper integer in bytes
806
 */
807
function memoryReturnBytes($val)
808
{
809
	// Treat blank values as 0
810
	$val = is_bool($val) || empty($val) ? 0 : trim($val);
811
812
	// Separate the number from the designator, if any
813
	preg_match('~(\d+)(.*)~', $val, $val);
0 ignored issues
show
Bug introduced by
$val of type integer|string is incompatible with the type string[] expected by parameter $matches of preg_match(). ( Ignorable by Annotation )

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

813
	preg_match('~(\d+)(.*)~', $val, /** @scrutinizer ignore-type */ $val);
Loading history...
814
	$num = (int) $val[1];
815
	$last = strtolower(substr($val[2] ?? '', 0, 1));
816
817
	// Convert to bytes
818
	switch ($last)
819
	{
820
		// fall through select g = 1024*1024*1024
821
		case 'g':
822
			$num *= 1024;
823
		// fall through select m = 1024*1024
824
		case 'm':
825
			$num *= 1024;
826
		// fall through select k = 1024
827
		case 'k':
828
			$num *= 1024;
829
	}
830
831
	return $num;
832
}
833
834
/**
835
 * This is the only template included in the sources.
836
 * @return void
837
 */
838
function template_rawdata()
839
{
840
	theme()->template_rawdata();
841
}
842
843
/**
844
 * The header template
845
 * @return void
846
 */
847
function template_header()
848
{
849
	theme()->template_header();
850
}
851
852
/**
853
 * Show the copyright.
854
 * @return void
855
 */
856
function theme_copyright()
857
{
858
	theme()->theme_copyright();
859
}
860
861
/**
862
 * The template footer
863
 * @return void
864
 */
865
function template_footer()
866
{
867
	theme()->template_footer();
868
}
869
870
/**
871
 * Output the Javascript files
872
 *
873
 * @depreciated since 2.0, only for old theme support
874
 * @return void
875
 */
876
function template_javascript()
877
{
878
	theme()->themeJs()->template_javascript();
879
}
880
881
/**
882
 * Output the CSS files
883
 *
884
 * @depreciated since 2.0, only for old theme suppot
885
 * @return void
886
 */
887
function template_css()
888
{
889
	theme()->themecss->template_css();
0 ignored issues
show
Bug introduced by
The property themecss does not seem to exist on ElkArte\Themes\DefaultTheme\Theme.
Loading history...
890
}
891
892
/**
893
 * Calls on template_show_error from index.template.php to show warnings
894
 * and security errors for admins
895
 * @return void
896
 */
897
function template_admin_warning_above()
898
{
899
	theme()->template_admin_warning_above();
900
}
901
902
/**
903
 * Convert IP address to IP range
904
 *
905
 *  - Internal function used to convert a user-readable format to a format suitable for the database.
906
 *
907
 * @param string $fullip The IP address to convert
908
 * @return array The IP range in the format [ ['low' => 'low_value_1', 'high' => 'high_value_1'], ... ]
909
 * If the input IP address is invalid or cannot be converted, an empty array is returned.
910
 */
911
function ip2range($fullip)
912
{
913
	// If its IPv6, validate it first.
914
	if (isValidIPv6($fullip))
915
	{
916
		$ip_parts = explode(':', expandIPv6($fullip, false));
0 ignored issues
show
Bug introduced by
It seems like expandIPv6($fullip, false) can also be of type boolean; however, parameter $string of explode() 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

916
		$ip_parts = explode(':', /** @scrutinizer ignore-type */ expandIPv6($fullip, false));
Loading history...
917
		$ip_array = [];
918
919
		if (count($ip_parts) !== 8)
920
		{
921
			return [];
922
		}
923
924
		for ($i = 0; $i < 8; $i++)
925
		{
926
			if ($ip_parts[$i] === '*')
927
			{
928
				$ip_array[$i] = ['low' => '0', 'high' => hexdec('ffff')];
929
			}
930
			elseif (preg_match('/^([0-9A-Fa-f]{1,4})-([0-9A-Fa-f]{1,4})$/', $ip_parts[$i], $range) === 1)
931
			{
932 4
				$ip_array[$i] = ['low' => hexdec($range[1]), 'high' => hexdec($range[2])];
933
			}
934
			elseif (is_numeric(hexdec($ip_parts[$i])))
935
			{
936 4
				$ip_array[$i] = ['low' => hexdec($ip_parts[$i]), 'high' => hexdec($ip_parts[$i])];
937
			}
938
		}
939
940
		return $ip_array;
941
	}
942 4
943
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
944
	if ($fullip === 'unknown')
945 4
	{
946
		$fullip = '255.255.255.255';
947 4
	}
948
949
	$ip_parts = explode('.', $fullip);
950
	$ip_array = [];
951
952 4
	if (count($ip_parts) !== 4)
953
	{
954
		return [];
955
	}
956 4
957
	for ($i = 0; $i < 4; $i++)
958
	{
959
		if ($ip_parts[$i] === '*')
960
		{
961
			$ip_array[$i] = ['low' => '0', 'high' => '255'];
962
		}
963
		elseif (preg_match('/^(\d{1,3})-(\d{1,3})$/', $ip_parts[$i], $range) === 1)
964
		{
965
			$ip_array[$i] = ['low' => $range[1], 'high' => $range[2]];
966
		}
967
		elseif (is_numeric($ip_parts[$i]))
968
		{
969
			$ip_array[$i] = ['low' => $ip_parts[$i], 'high' => $ip_parts[$i]];
970
		}
971
	}
972
973
	// Makes it simpler to work with.
974
	$ip_array[4] = ['low' => 0, 'high' => 0];
975
	$ip_array[5] = ['low' => 0, 'high' => 0];
976
	$ip_array[6] = ['low' => 0, 'high' => 0];
977
	$ip_array[7] = ['low' => 0, 'high' => 0];
978
979 10
	return $ip_array;
980
}
981
982
/**
983
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
984
 *
985 10
 * @param string $ip A full dot notation IP address
986 10
 *
987 10
 * @return string
988
 */
989
function host_from_ip($ip)
990 5
{
991
	global $modSettings;
992
993 10
	$cache = Cache::instance();
994 10
995
	$host = '';
996 10
	if (empty($ip) || $cache->getVar($host, 'hostlookup-' . $ip, 600))
997 10
	{
998
		return $host;
999
	}
1000 10
1001
	$t = microtime(true);
1002
1003 10
	// Check if shell_exec is on the list of disabled functions.
1004
	if (function_exists('shell_exec'))
1005
	{
1006
		// Try the Linux host command, perhaps?
1007
		if (PHP_OS_FAMILY !== 'Windows' && mt_rand(0, 1) === 1)
1008
		{
1009
			if (!isset($modSettings['host_to_dis']))
1010
			{
1011
				$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
1012
			}
1013
			else
1014
			{
1015
				$test = @shell_exec('host ' . @escapeshellarg($ip));
1016
			}
1017
1018
			$test = $test ?? '';
1019
1020
			// Did host say it didn't find anything?
1021
			if (stripos($test, 'not found') !== false)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false; however, parameter $haystack of stripos() 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

1021
			if (stripos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
1022
			{
1023
				$host = '';
1024
			}
1025
			// Invalid server option?
1026
			elseif ((stripos($test, 'invalid option') || stripos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
1027
			{
1028
				updateSettings(['host_to_dis' => 1]);
1029
			}
1030
			// Maybe it found something, after all?
1031
			elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false; however, parameter $subject of preg_match() 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

1031
			elseif (preg_match('~\s([^\s]+?)\.\s~', /** @scrutinizer ignore-type */ $test, $match) == 1)
Loading history...
1032
			{
1033
				$host = $match[1];
1034
			}
1035
		}
1036
1037
		// This is nslookup; usually default on Windows, and possibly some Unix with bind-utils
1038
		if ((empty($host) || PHP_OS_FAMILY === 'Windows') && mt_rand(0, 1) === 1)
1039
		{
1040
			$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
1041
1042
			if (stripos($test, 'Non-existent domain') !== false)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false and null; however, parameter $haystack of stripos() 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

1042
			if (stripos(/** @scrutinizer ignore-type */ $test, 'Non-existent domain') !== false)
Loading history...
1043
			{
1044
				$host = '';
1045
			}
1046
			elseif (preg_match('~(?:Name:|Name =)\s+([^\s]+)~i', $test, $match) === 1)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false and null; however, parameter $subject of preg_match() 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

1046
			elseif (preg_match('~(?:Name:|Name =)\s+([^\s]+)~i', /** @scrutinizer ignore-type */ $test, $match) === 1)
Loading history...
1047
			{
1048
				$host = $match[1];
1049
			}
1050
		}
1051
	}
1052
1053
	// This is the last try :/.
1054
	if (!isset($host))
1055
	{
1056
		$host = @gethostbyaddr($ip);
1057
	}
1058
1059
	// It took a long time, so let's cache it!
1060
	if (microtime(true) - $t > 0.5)
1061
	{
1062
		$cache->put('hostlookup-' . $ip, $host, 600);
1063
	}
1064
1065
	return $host;
1066
}
1067
1068
/**
1069
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
1070
 *
1071
 * @param string $text The string to process
1072
 *     - if encrypt = true this is the maximum number of bytes to use in integer hashes (for searching)
1073
 *     - if encrypt = false this is the maximum number of letters in each word
1074
 * @param bool $encrypt = false Used for custom search indexes to return an int[] array representing the words
1075
 *
1076
 * @return array
1077
 */
1078
function text2words($text, $encrypt = false)
1079
{
1080
	// Step 0: prepare numbers so they are good for search & index 1000.45 -> 1000_45
1081
	$words = preg_replace('~([\d]+)[.-/]+(?=[\d])~u', '$1_', $text);
1082
1083
	// Step 1: Remove entities/things we don't consider words:
1084
	$words = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', strtr($words, ['<br />' => ' ']));
1085
1086
	// Step 2: Entities we left to letters, where applicable, lowercase.
1087
	$words = un_htmlspecialchars(Util::strtolower($words));
1088
1089
	// Step 3: Ready to split apart and index!
1090
	$words = explode(' ', $words);
1091
1092
	if ($encrypt)
1093
	{
1094
		$blocklist = getBlocklist();
1095
		$returned_ints = [];
1096
1097
		// Only index unique words
1098
		$words = array_unique($words);
1099
		foreach ($words as $word)
1100
		{
1101
			$word = trim($word, "-_'");
1102
			if ($word !== '' && !in_array($word, $blocklist) && Util::strlen($word) > 2)
1103
			{
1104
				// Get a hex representation of this word using a database indexing hash
1105
				// designed to be fast while maintaining a very low collision rate
1106
				$encrypted = hash('FNV1A32', $word);
1107
1108
				// Create an integer representation, the hash is an 8 char hex
1109
				// so the largest int will be 4294967295 which fits in db int(10)
1110
				$returned_ints[$word] = hexdec($encrypted);
1111
			}
1112
		}
1113
1114
		return $returned_ints;
1115
	}
1116
1117
	// Trim characters before and after and add slashes for database insertion.
1118
	$returned_words = [];
1119
	foreach ($words as $word)
1120
	{
1121
		if (($word = trim($word, "-_'")) !== '')
1122
		{
1123
			$returned_words[] = substr($word, 0, 20);
1124
		}
1125
	}
1126
1127
	// Filter out all words that occur more than once.
1128
	return array_unique($returned_words);
1129
}
1130
1131
/**
1132
 * Get the block list from the search controller.
1133
 *
1134
 * @return array
1135
 */
1136
function getBlocklist()
1137
{
1138
	static $blocklist;
1139
1140
	if (!isset($blocklist))
1141
	{
1142
		$search = new Search();
1143
		$blocklist = $search->getBlockListedWords();
1144
		unset($search);
1145
	}
1146
1147
	return $blocklist;
1148
}
1149
1150
/**
1151
 * Sets up all of the top menu buttons
1152
 *
1153
 * What it does:
1154
 *
1155
 * - Defines every master item in the menu, as well as any sub-items
1156
 * - Ensures the chosen action is set so the menu is highlighted
1157
 * - Saves them in the cache if it is available and on
1158
 * - Places the results in $context
1159
 */
1160
function setupMenuContext()
1161
{
1162
	return theme()->setupMenuContext();
0 ignored issues
show
Bug introduced by
The method setupMenuContext() does not exist on ElkArte\Themes\DefaultTheme\Theme. ( Ignorable by Annotation )

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

1162
	return theme()->/** @scrutinizer ignore-call */ setupMenuContext();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1163
}
1164
1165
/**
1166
 * Process functions of an integration hook.
1167
 *
1168
 * What it does:
1169
 *
1170
 * - Calls all functions of the given hook.
1171
 * - Supports static class method calls.
1172
 *
1173
 * @param string $hook The name of the hook to call
1174
 * @param array $parameters = array() Parameters to pass to the hook
1175
 *
1176
 * @return array the results of the functions
1177
 */
1178
function call_integration_hook($hook, $parameters = [])
1179
{
1180
	return Hooks::instance()->hook($hook, $parameters);
1181
}
1182
1183
/**
1184
 * Includes files for hooks that only do that (i.e. integrate_pre_include)
1185
 *
1186
 * @param string $hook The name to include
1187
 */
1188
function call_integration_include_hook($hook)
1189
{
1190
	Hooks::instance()->include_hook($hook);
1191
}
1192
1193
/**
1194
 * Special hook call executed during obExit
1195
 */
1196
function call_integration_buffer()
1197
{
1198
	Hooks::instance()->buffer_hook();
1199
}
1200
1201
/**
1202
 * Add a function for integration hook.
1203
 *
1204
 * - Does nothing if the function is already added.
1205
 *
1206
 * @param string $hook The name of the hook to add
1207
 * @param string $function The function associated with the hook
1208
 * @param string $file The file that contains the function
1209
 * @param bool $permanent = true if true, updates the value in settings table
1210
 */
1211
function add_integration_function($hook, $function, $file = '', $permanent = true)
1212
{
1213
	Hooks::instance()->add($hook, $function, $file, $permanent);
1214
}
1215
1216
/**
1217
 * Remove an integration hook function.
1218
 *
1219
 * What it does:
1220
 *
1221
 * - Removes the given function from the given hook.
1222
 * - Does nothing if the function is not available.
1223
 *
1224
 * @param string $hook The name of the hook to remove
1225
 * @param string $function The name of the function
1226
 * @param string $file The file its located in
1227
 */
1228
function remove_integration_function($hook, $function, $file = '')
1229
{
1230
	Hooks::instance()->remove($hook, $function, $file);
1231
}
1232
1233
/**
1234
 * Decode numeric html entities to their UTF8 equivalent character.
1235
 *
1236
 * What it does:
1237
 *
1238
 * - Callback function for preg_replace_callback in subs-members
1239
 * - Uses capture group 2 in the supplied array
1240
 * - Does basic scan to ensure characters are inside a valid range
1241
 *
1242
 * @param array $matches matches from a preg_match_all
1243
 *
1244
 * @return string $string
1245
 */
1246
function replaceEntities__callback($matches)
1247
{
1248
	if (!isset($matches[2]))
1249
	{
1250
		return '';
1251
	}
1252
1253
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
0 ignored issues
show
Bug introduced by
$matches[2] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

1253
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
1254
1255
	// remove left to right / right to left overrides
1256 26
	if ($num === 0x202D || $num === 0x202E)
1257
	{
1258
		return '';
1259 26
	}
1260
1261
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
1262 26
	if (in_array($num, [0x22, 0x26, 0x27, 0x3C, 0x3E]))
1263
	{
1264 26
		return '&#' . $num . ';';
1265
	}
1266
1267
	// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
1268
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
1269
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
1270
	{
1271
		return '';
1272
	}
1273
1274
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1275
	if ($num < 0x80)
1276
	{
1277
		return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, 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

1277
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
1278
	}
1279
1280
	// <0x800 (2048)
1281
	if ($num < 0x800)
1282
	{
1283
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1284
	}
1285
1286
	// < 0x10000 (65536)
1287
	if ($num < 0x10000)
1288
	{
1289
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1290
	}
1291
1292
	// <= 0x10FFFF (1114111)
1293 26
	return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1294 26
}
1295
1296 26
/**
1297
 * Converts html entities to utf8 equivalents
1298 26
 *
1299
 * What it does:
1300
 *
1301
 * - Callback function for preg_replace_callback
1302
 * - Uses capture group 1 in the supplied array
1303 26
 * - Does basic checks to keep characters inside a viewable range.
1304
 *
1305
 * @param array $matches array of matches as output from preg_match_all
1306
 *
1307
 * @return string $string
1308
 */
1309
function fixchar__callback($matches)
1310
{
1311
	if (!isset($matches[1]))
1312
	{
1313
		return '';
1314
	}
1315
1316
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
0 ignored issues
show
Bug introduced by
$matches[1] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

1316
	$num = $matches[1][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[1], 1)) : (int) $matches[1];
Loading history...
1317
1318
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
1319
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
1320
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
1321
	{
1322
		return '';
1323
	}
1324
1325
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1326
	if ($num < 0x80)
1327
	{
1328
		return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, 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

1328
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
1329
	}
1330
1331
	// <0x800 (2048)
1332
	if ($num < 0x800)
1333
	{
1334
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1335
	}
1336
1337 299
	// < 0x10000 (65536)
1338
	if ($num < 0x10000)
1339
	{
1340
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1341
	}
1342
1343
	// <= 0x10FFFF (1114111)
1344
	return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1345
}
1346
1347 231
/**
1348 231
 * Strips out invalid html entities, replaces others with html style &#123; codes
1349
 *
1350
 * What it does:
1351
 *
1352
 * - Callback function used of preg_replace_callback in various $ent_checks,
1353
 * - For example strpos, strlen, substr etc
1354
 *
1355
 * @param array $matches array of matches for a preg_match_all
1356
 *
1357
 * @return string
1358
 */
1359
function entity_fix__callback($matches)
1360
{
1361
	if (!isset($matches[2]))
1362
	{
1363
		return '';
1364
	}
1365
1366
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
0 ignored issues
show
Bug introduced by
$matches[2] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

1366
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
1367
1368
	// We don't allow control characters, characters out of range, byte markers, etc
1369
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
1370 4
	{
1371 4
		return '';
1372
	}
1373
1374
	return '&#' . $num . ';';
1375
}
1376
1377
/**
1378
 * Retrieve additional search engines, if there are any, as an array.
1379
 *
1380
 * @return array array of engines
1381
 */
1382
function prepareSearchEngines()
1383
{
1384
	global $modSettings;
1385
1386
	$engines = [];
1387 2
	if (!empty($modSettings['additional_search_engines']))
1388 2
	{
1389
		$search_engines = Util::unserialize($modSettings['additional_search_engines']);
1390
		foreach ($search_engines as $engine)
1391
		{
1392
			$engines[strtolower(preg_replace('~[^A-Za-z0-9 ]~', '', $engine['name']))] = $engine;
1393
		}
1394
	}
1395
1396
	return $engines;
1397
}
1398
1399
/**
1400
 * This function receives a request handle and attempts to retrieve the next result.
1401
 *
1402
 * What it does:
1403
 *
1404
 * - It is used by the controller callbacks from the template, such as
1405
 * posts in topic display page, posts search results page, or personal messages.
1406
 *
1407
 * @param resource $messages_request holds a query result
1408
 * @param bool $reset
1409
 *
1410
 * @return int|bool
1411
 * @throws Exception
1412
 */
1413
function currentContext($messages_request, $reset = false)
1414
{
1415
	// Start from the beginning...
1416
	if ($reset)
1417
	{
1418
		return $messages_request->data_seek(0);
1419
	}
1420
1421
	// If the query has already returned false, get out of here
1422
	if ($messages_request->hasResults())
1423
	{
1424
		return false;
1425
	}
1426
1427
	// Attempt to get the next message.
1428
	$message = $messages_request->fetch_assoc();
1429
	if (!$message)
1430
	{
1431
		$messages_request->free_result();
1432
1433
		return false;
1434
	}
1435
1436
	return $message;
1437
}
1438
1439
/**
1440
 * Helper function to insert an array in to an existing array
1441
 *
1442
 * What it does:
1443
 *
1444
 * - Intended for addon use to allow such things as
1445
 * - Adding in a new menu item to an existing menu array
1446
 *
1447
 * @param array $input the array we will insert to
1448
 * @param string $key the key in the array that we are looking to find for the insert action
1449
 * @param array $insert the actual data to insert before or after the key
1450
 * @param string $where adding before or after
1451
 * @param bool $assoc if the array is a assoc array with named keys or a basic index array
1452
 * @param bool $strict search for identical elements, this means it will also check the types of the needle.
1453
 *
1454
 * @return array
1455
 */
1456
function elk_array_insert($input, $key, $insert, $where = 'before', $assoc = true, $strict = false)
1457
{
1458
	$position = $assoc ? array_search($key, array_keys($input), $strict) : array_search($key, $input, $strict);
1459
1460
	// If the key is not found, just insert it at the end
1461
	if ($position === false)
0 ignored issues
show
introduced by
The condition $position === false is always false.
Loading history...
1462
	{
1463
		return array_merge($input, $insert);
1464
	}
1465
1466
	if ($where === 'after')
1467
	{
1468
		$position++;
1469
	}
1470
1471
	// Insert as first
1472
	if (empty($position))
1473
	{
1474
		return array_merge($insert, $input);
1475
	}
1476
1477
	return array_merge(array_slice($input, 0, $position), $insert, array_slice($input, $position));
0 ignored issues
show
Bug introduced by
It seems like $position can also be of type string; however, parameter $length of array_slice() does only seem to accept integer|null, 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

1477
	return array_merge(array_slice($input, 0, /** @scrutinizer ignore-type */ $position), $insert, array_slice($input, $position));
Loading history...
Bug introduced by
It seems like $position can also be of type string; however, parameter $offset of array_slice() does only seem to accept integer, 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

1477
	return array_merge(array_slice($input, 0, $position), $insert, array_slice($input, /** @scrutinizer ignore-type */ $position));
Loading history...
1478
}
1479
1480
/**
1481
 * Run a scheduled task now
1482
 *
1483
 * What it does:
1484
 *
1485
 * - From time to time it may be necessary to fire a scheduled task ASAP
1486
 * - This function sets the scheduled task to be called before any other one
1487
 *
1488
 * @param string $task the name of a scheduled task
1489
 */
1490
function scheduleTaskImmediate($task)
1491
{
1492
	global $modSettings;
1493
1494
	if (!isset($modSettings['scheduleTaskImmediate']))
1495
	{
1496
		$scheduleTaskImmediate = [];
1497
	}
1498
	else
1499
	{
1500
		$scheduleTaskImmediate = Util::unserialize($modSettings['scheduleTaskImmediate']);
1501
	}
1502
1503
	// If it has not been scheduled, the do so now
1504
	if (!isset($scheduleTaskImmediate[$task]))
1505
	{
1506
		$scheduleTaskImmediate[$task] = 0;
1507
		updateSettings(['scheduleTaskImmediate' => serialize($scheduleTaskImmediate)]);
1508
1509
		require_once(SUBSDIR . '/ScheduledTasks.subs.php');
1510
1511
		// Ensure the task is on
1512
		toggleTaskStatusByName($task, true);
1513
1514
		// Before trying to run it **NOW** :P
1515
		calculateNextTrigger($task, true);
1516
	}
1517
}
1518 8
1519
/**
1520
 * For diligent people: remove scheduleTaskImmediate when done, otherwise
1521
 * a maximum of 10 executions is allowed
1522
 *
1523 8
 * @param string $task the name of a scheduled task
1524
 * @param bool $calculateNextTrigger if recalculate the next task to execute
1525
 */
1526 8
function removeScheduleTaskImmediate($task, $calculateNextTrigger = true)
1527
{
1528 4
	global $modSettings;
1529
1530
	// Not on, bail
1531 4
	if (!isset($modSettings['scheduleTaskImmediate']))
1532
	{
1533
		return;
1534
	}
1535
1536
	$scheduleTaskImmediate = Util::unserialize($modSettings['scheduleTaskImmediate']);
1537
1538
	// Clear / remove the task if it was set
1539
	if (isset($scheduleTaskImmediate[$task]))
1540
	{
1541
		unset($scheduleTaskImmediate[$task]);
1542
		updateSettings(['scheduleTaskImmediate' => serialize($scheduleTaskImmediate)]);
1543
1544
		// Recalculate the next task to execute
1545
		if ($calculateNextTrigger)
1546
		{
1547
			require_once(SUBSDIR . '/ScheduledTasks.subs.php');
1548
			calculateNextTrigger($task);
1549
		}
1550
	}
1551
}
1552
1553
/**
1554
 * Helper function to replace commonly used urls in text strings
1555
 *
1556
 * @event integrate_basic_url_replacement add additional place holder replacements
1557
 * @param string $string the string to inject URLs into
1558
 *
1559
 * @return string the input string with the place-holders replaced with
1560
 *           the correct URLs
1561
 */
1562
function replaceBasicActionUrl($string)
1563
{
1564
	global $scripturl, $context, $boardurl;
1565
	static $find_replace = null;
1566
1567
	if ($find_replace === null)
1568
	{
1569
		$find_replace = [
1570
			'{forum_name}' => $context['forum_name'],
1571
			'{forum_name_html_safe}' => $context['forum_name_html_safe'],
1572
			'{forum_name_html_unsafe}' => un_htmlspecialchars($context['forum_name_html_safe']),
1573
			'{script_url}' => $scripturl,
1574
			'{board_url}' => $boardurl,
1575
			'{login_url}' => getUrl('action', ['action' => 'login']),
1576
			'{register_url}' => getUrl('action', ['action' => 'register']),
1577
			'{activate_url}' => getUrl('action', ['action' => 'register', 'sa' => 'activate']),
1578
			'{help_url}' => getUrl('action', ['action' => 'help']),
1579
			'{admin_url}' => getUrl('admin', ['action' => 'admin']),
1580
			'{moderate_url}' => getUrl('moderate', ['action' => 'moderate']),
1581
			'{recent_url}' => getUrl('action', ['action' => 'recent']),
1582
			'{search_url}' => getUrl('action', ['action' => 'search']),
1583
			'{who_url}' => getUrl('action', ['action' => 'who']),
1584
			'{credits_url}' => getUrl('action', ['action' => 'about', 'sa' => 'credits']),
1585
			'{calendar_url}' => getUrl('action', ['action' => 'calendar']),
1586
			'{memberlist_url}' => getUrl('action', ['action' => 'memberlist']),
1587
			'{stats_url}' => getUrl('action', ['action' => 'stats']),
1588
		];
1589
		call_integration_hook('integrate_basic_url_replacement', [&$find_replace]);
1590
	}
1591
1592
	return str_replace(array_keys($find_replace), array_values($find_replace), $string);
1593
}
1594
1595
/**
1596
 * This function creates a new GenericList from all the passed options.
1597
 *
1598
 * What it does:
1599
 *
1600
 * - Calls integration hook integrate_list_"unique_list_id" to allow easy modifying
1601
 *
1602
 * @event integrate_list_$listID called before every createlist to allow access to its listoptions
1603
 * @param array $listOptions associative array of option => value
1604
 */
1605
function createList($listOptions)
1606
{
1607
	call_integration_hook('integrate_list_' . $listOptions['id'], [&$listOptions]);
1608
1609
	$list = new GenericList($listOptions);
1610
1611
	$list->buildList();
1612
}
1613
1614
/**
1615
 * This handy function retrieves a Request instance and passes it on.
1616
 *
1617
 * What it does:
1618
 *
1619
 * - To get hold of a Request, you can use this function or directly Request::instance().
1620
 * - This is for convenience, it simply delegates to Request::instance().
1621
 */
1622
function request()
1623
{
1624
	return Request::instance();
1625
}
1626
1627
/**
1628
 * Meant to replace any usage of $db_last_error.
1629
 *
1630
 * What it does:
1631
 *
1632
 * - Reads the file db_last_error.txt, if a time() is present returns it,
1633
 * otherwise returns 0.
1634
 */
1635
function db_last_error()
1636
{
1637
	$time = trim(file_get_contents(BOARDDIR . '/db_last_error.txt'));
1638
1639
	if (preg_match('~^\d{10}$~', $time) === 1)
1640
	{
1641
		return $time;
1642
	}
1643
1644
	return 0;
1645
}
1646
1647
/**
1648
 * This function has the only task to retrieve the correct prefix to be used
1649
 * in responses.
1650
 *
1651
 * @return string - The prefix in the default language of the forum
1652
 */
1653
function response_prefix()
1654
{
1655
	global $language, $txt;
1656
	static $response_prefix = null;
1657
1658
	$cache = Cache::instance();
1659
1660
	// Get a response prefix, but in the forum's default language.
1661
	if ($response_prefix === null && (!$cache->getVar($response_prefix, 'response_prefix') || !$response_prefix))
1662
	{
1663
		if ($language === User::$info->language)
0 ignored issues
show
Bug Best Practice introduced by
The property language does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
1664
		{
1665
			$response_prefix = $txt['response_prefix'];
1666
		}
1667
		else
1668
		{
1669
			$mtxt = [];
1670
			$lang_loader = new Loader($language, $mtxt, database());
1671
			$lang_loader->load('index');
1672
			$response_prefix = $mtxt['response_prefix'];
1673
		}
1674
1675
		$cache->put('response_prefix', $response_prefix, 600);
1676
	}
1677
1678
	return $response_prefix;
1679
}
1680
1681
/**
1682
 * A very simple function to determine if an email address is "valid" for Elkarte.
1683
 *
1684
 * - A valid email for ElkArte is something that resembles an email (filter_var) and
1685
 * is less than 255 characters (for database limits)
1686
 *
1687
 * @param string $value - The string to evaluate as valid email
1688
 *
1689
 * @return string|false - The email if valid, false if not a valid email
1690
 */
1691
function isValidEmail($value)
1692
{
1693
	$value = trim($value);
1694
	if (!filter_var($value, FILTER_VALIDATE_EMAIL))
1695
	{
1696
		return false;
1697
	}
1698
1699
	if (Util::strlen($value) >= 255)
1700
	{
1701
		return false;
1702
	}
1703
1704
	return $value;
1705
}
1706
1707
/**
1708
 * Adds a protocol (http/s, ftp/mailto) to the beginning of an url if missing
1709
 *
1710
 * @param string $url - The url
1711
 * @param string[] $protocols - A list of protocols to check, the first is
1712
 *                 added if none is found (optional, default array('http://', 'https://'))
1713
 *
1714
 * @return string - The url with the protocol
1715
 */
1716
function addProtocol($url, $protocols = [])
1717
{
1718
	if (empty($protocols))
1719
	{
1720
		$pattern = '~^(http://|https://)~i';
1721
		$protocols = ['http://'];
1722
	}
1723
	else
1724
	{
1725
		$pattern = '~^(' . implode('|', array_map(static fn($val) => preg_quote($val, '~'), $protocols)) . ')~i';
1726
	}
1727
1728
	$found = false;
1729 5
	$urlNew = preg_replace_callback($pattern, static function ($match) use (&$found) {
1730 5
		$found = true;
1731
1732 5
		return strtolower($match[0]);
1733
	}, $url);
1734
1735 3
	if ($found)
1736 3
	{
1737 3
		return $urlNew;
1738 3
	}
1739 3
1740 3
	return $protocols[0] . $urlNew;
1741 3
}
1742 3
1743 3
/**
1744 3
 * Validate if a URL is allowed to be a "dofollow"
1745 3
 *
1746 3
 * @param string $checkUrl The URL to be checked
1747 3
 * @return bool Returns true if the URL is allowed, false otherwise
1748 3
 */
1749 3
function validateURLAllowList($checkUrl)
1750 3
{
1751 3
	global $modSettings, $boardurl;
1752 3
	static $allowList = null;
1753
1754 3
	if ($allowList === null)
1755
	{
1756
		$allowList = empty($modSettings['nofollow_allowlist']) ? [] : json_decode($modSettings['nofollow_allowlist']);
1757 5
1758
		// Always allow your own site
1759
		$parse = parse_url($boardurl);
1760
		$allowList[] = $parse['host'];
1761
1762
		$allowList = array_unique($allowList);
1763
	}
1764
1765
	$parsed = parse_url($checkUrl);
1766
	if (empty($parsed['host']))
1767
	{
1768
		return false;
1769
	}
1770
1771
	foreach ($allowList as $validDomain)
1772 10
	{
1773
		if (substr($parsed['host'], -strlen($validDomain)) === $validDomain)
1774 10
		{
1775
			return true;
1776 10
		}
1777 10
	}
1778
1779
	return false;
1780
}
1781
1782
/**
1783
 * Removes all, or those over a limit, of nested quotes from a text string.
1784
 *
1785
 * @param string $text - The body we want to remove nested quotes from
1786
 *
1787
 * @return string - The same body, just without nested quotes
1788
 */
1789 251
function removeNestedQuotes($text)
1790
{
1791
	global $modSettings;
1792
1793
	if (!isset($modSettings['removeNestedQuotes']))
1794
	{
1795
		return $text;
1796
	}
1797
1798
	// How many levels will we allow?
1799
	$max_depth = (int) $modSettings['removeNestedQuotes'];
1800
1801
	// Remove quotes over our limit, then we need to find them all
1802
	preg_match_all('~(\[/?quote(.*?)?])~i', $text, $matches, PREG_OFFSET_CAPTURE);
1803
	$depth = 0;
1804
	$remove = [];
1805
	$start_pos = 0;
1806
1807
	// Mark ones that are in excess of the limit.  $match[0] will be the found tag
1808
	// such as [quote=some author] or [/quote], $match[1] is the starting position of that tag.
1809
	foreach ($matches[0] as $match)
1810
	{
1811
		// Closing quote
1812
		if ($match[0][1] === '/')
1813
		{
1814
			--$depth;
1815
1816
			// To many, mark it for removal
1817
			if ($depth === $max_depth)
1818
			{
1819
				// This quote position in the string, note [/quote] = 8
1820 4
				$end_pos = $match[1] + 8;
1821 4
				$length = $end_pos - $start_pos;
1822
				$remove[] = [$start_pos, $length];
1823 4
			}
1824
1825
			continue;
1826 4
		}
1827
1828 2
		// Another quote level inward
1829
		++$depth;
1830 2
		if ($depth === $max_depth + 1)
1831
		{
1832
			$start_pos = $match[1];
1833
		}
1834
	}
1835
1836
	// Time to cull the herd
1837
	foreach (array_reverse($remove) as [$start_pos, $length])
1838
	{
1839 2
		$text = substr_replace($text, '', $start_pos, $length);
1840
	}
1841
1842 4
	return trim($text);
0 ignored issues
show
Bug introduced by
It seems like $text can also be of type array; however, parameter $string of trim() 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

1842
	return trim(/** @scrutinizer ignore-type */ $text);
Loading history...
1843
}
1844
1845
/**
1846
 * Change a \t to a span that will show a tab
1847
 *
1848
 * @param string $string
1849
 *
1850
 * @return string
1851
 */
1852
function tabToHtmlTab($string)
1853
{
1854
	return str_replace("\t", "<span class=\"tab\">\t</span>", $string);
1855
}
1856
1857 2
/**
1858 2
 * Remove <br />
1859
 *
1860 2
 * @param string $string
1861
 *
1862
 * @return string
1863
 */
1864
function removeBr($string)
1865
{
1866
	return str_replace('<br />', '', $string);
1867
}
1868
1869
/**
1870
 * Replace all vulgar words with respective proper words. (substring or whole words..)
1871
 *
1872
 * What it does:
1873
 *  - it censors the passed string.
1874
 *  - if the admin setting allow_no_censored is on it does not censor unless force is also set.
1875
 *  - if the admin setting allow_no_censored is off will censor words unless the user has set
1876
 * it to not censor in their profile and force is off
1877 14
 *  - it caches the list of censored words to reduce parsing.
1878
 *  - Returns the censored text
1879 14
 *
1880 14
 * @param string $text
1881
 * @param bool $force = false
1882
 *
1883
 * @return string
1884
 */
1885
function censor($text, $force = false)
1886
{
1887
	global $modSettings;
1888
	static $censor = null;
1889 14
1890
	if ($censor === null)
1891 14
	{
1892
		$censor = new Censor(explode("\n", $modSettings['censor_vulgar']), explode("\n", $modSettings['censor_proper']), $modSettings);
1893 14
	}
1894 14
1895
	return $censor->censor($text, $force);
1896 14
}
1897
1898 14
/**
1899
 * Helper function able to determine if the current member can see at least
1900
 * one button of a button strip.
1901 2
 *
1902
 * @param array $button_strip
1903
 *
1904
 * @return bool
1905
 */
1906
function can_see_button_strip($button_strip)
1907
{
1908
	global $context;
1909
1910
	foreach ($button_strip as $value)
1911
	{
1912
		if (!isset($value['test']) || !empty($context[$value['test']]))
1913
		{
1914
			return true;
1915
		}
1916
	}
1917
1918
	return false;
1919
}
1920
1921
/**
1922
 * Get the current theme instance.
1923
 *
1924
 * @return Theme The current theme instance.
1925
 */
1926
function theme()
1927
{
1928
	return $GLOBALS['context']['theme_instance'];
1929
}
1930
1931
/**
1932
 * Set the JSON template for sending JSON response.
1933 4
 *
1934
 * This method prepares the template layers, loads the 'Json' template,
1935
 * and sets the sub_template to 'send_json' in the global $context array.
1936
 * The JSON data is initialized to null.
1937
 *
1938
 * @return void
1939
 */
1940
function setJsonTemplate()
1941
{
1942
	global $context;
1943
1944
	$template_layers = $GLOBALS['context']['theme_instance']->getLayers();
1945
	$template_layers->removeAll();
1946
	$GLOBALS['context']['theme_instance']->getTemplates()->load('Json');
1947
	$context['sub_template'] = 'send_json';
1948
1949
	$context['json_data'] = null;
1950
}
1951
1952
function setPWACacheStale($refresh = false)
1953
{
1954
	global $modSettings;
1955
1956
	// We need a PWA cache stale to keep things moving, changing this will trigger a PWA cache flush
1957
	if (empty($modSettings['elk_pwa_cache_stale']) || $refresh)
1958
	{
1959 229
		$tokenizer = new TokenHash();
1960
		$elk_pwa_cache_stale = $tokenizer->generate_hash(8);
1961
		updateSettings(['elk_pwa_cache_stale' => $elk_pwa_cache_stale]);
1962 229
	}
1963
}
1964
1965
/**
1966
 * Send a 1x1 GIF response and terminate the script execution
1967 229
 *
1968
 * @param bool $expired Flag to determine if header Expires should be sent
1969
 *
1970
 * @return void
1971
 */
1972
function dieGif($expired = false): never
1973
{
1974
	// The following is an attempt at stopping the behavior identified in #2391
1975
	if (function_exists('fastcgi_finish_request'))
1976
	{
1977
		die();
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...
1978
	}
1979
1980
	$headers = Headers::instance();
1981
	if ($expired)
1982
	{
1983
		$headers
1984
			->header('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')
1985
			->header('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
1986
	}
1987
1988 24
	$headers->contentType('image/gif')->sendHeaders();
1989 24
	die("\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B");
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...
1990
}
1991 24
1992
/**
1993 2
 * Prepare ob_start with or without gzip compression
1994
 *
1995
 * @param bool $use_compression Starts compressed headers.
1996 24
 */
1997
function obStart($use_compression = false)
1998
{
1999
	// This is done to clear any output that was made before now.
2000
	while (ob_get_level() > 0)
2001
	{
2002
		@ob_end_clean();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ob_end_clean(). 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

2002
		/** @scrutinizer ignore-unhandled */ @ob_end_clean();

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...
2003
	}
2004
2005
	if ($use_compression)
2006
	{
2007
		ob_start('ob_gzhandler');
2008
	}
2009
	else
2010
	{
2011
		ob_start();
2012
		Headers::instance()->header('Content-Encoding', 'none');
2013
	}
2014
}
2015
2016
/**
2017
 * Returns a URL based on the parameters passed and the selected generator
2018
 *
2019
 * @param string $type The type of the URL (depending on the type, the
2020
 *                     generator can act differently
2021
 * @param array $params All the parameters of the URL
2022
 *
2023
 * @return string An URL
2024
 */
2025
function getUrl($type, $params)
2026
{
2027 337
	static $generator = null;
2028
2029
	if ($generator === null)
2030
	{
2031
		$generator = initUrlGenerator();
2032
	}
2033
2034
	return $generator->get($type, $params);
2035
}
2036
2037
/**
2038
 * Returns the query part of a URL based on the parameters passed and the selected generator
2039
 *
2040
 * @param string $type The type of the URL (depending on the type, the
2041
 *                     generator can act differently
2042
 * @param array $params All the parameters of the URL
2043
 *
2044
 * @return string The query part of an URL
2045
 */
2046
function getUrlQuery($type, $params)
2047
{
2048
	static $generator = null;
2049
2050
	if ($generator === null)
2051
	{
2052
		$generator = initUrlGenerator();
2053
	}
2054
2055
	return $generator->getQuery($type, $params);
2056
}
2057
2058
/**
2059
 * Initialize the URL generator
2060
 *
2061
 * @return object The URL generator object
2062
 */
2063
function initUrlGenerator()
2064
{
2065
	global $scripturl, $context, $url_format;
2066
2067
	$generator = new UrlGenerator([
2068
		'generator' => ucfirst($url_format ?? 'standard'),
2069
		'scripturl' => $scripturl,
2070
		'replacements' => [
2071
			'{session_data}' => isset($context['session_var']) ? $context['session_var'] . '=' . $context['session_id'] : ''
2072
		]
2073
	]);
2074
2075
	$generator->register('Topic');
2076
	$generator->register('Board');
2077
	$generator->register('Profile');
2078
2079
	return $generator;
2080
}
2081
2082
/**
2083
 * This function only checks if a certain feature (in core features)
2084
 * is enabled or not.
2085
 *
2086
 * @param string $feature The abbreviated code of a core feature
2087
 * @return bool true/false for enabled/disabled
2088 41
 */
2089
function featureEnabled($feature)
2090 41
{
2091
	global $modSettings, $context;
2092 1
	static $features = null;
2093
2094
	if ($features === null)
2095 41
	{
2096
		// This allows us to change the way things look for the admin.
2097
		$features = explode(',', $modSettings['admin_features'] ?? 'cd,cp,k,w,rg,ml,pm');
2098
2099
		// @deprecated since 2.0 - Next line is just for backward compatibility to remove before release
2100
		$context['admin_features'] = $features;
2101
	}
2102
2103
	return in_array($feature, $features, true);
2104
}
2105
2106
/**
2107
 * Clean up the XML to make sure it doesn't contain invalid characters.
2108
 *
2109
 * What it does:
2110
 *
2111
 * - Removes invalid XML characters to assure the input string being parsed properly.
2112
 *
2113
 * @param string $string The string to clean
2114
 *
2115
 * @return string The clean string
2116
 */
2117
function cleanXml($string)
2118
{
2119
	// https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Char
2120
	$string = preg_replace('~[\x00-\x08\x0B\x0C\x0E-\x1F\x{FFFE}\x{FFFF}]~u', '', $string);
2121
2122
	// Discouraged
2123
	return preg_replace('~[\x7F-\x84\x86-\x9F\x{FDD0}-\x{FDEF}\x{1FFFE}-\x{1FFFF}\x{2FFFE}-\x{2FFFF}\x{3FFFE}-\x{3FFFF}\x{4FFFE}-\x{4FFFF}\x{5FFFE}-\x{5FFFF}\x{6FFFE}-\x{6FFFF}\x{7FFFE}-\x{7FFFF}\x{8FFFE}-\x{8FFFF}\x{9FFFE}-\x{9FFFF}\x{AFFFE}-\x{AFFFF}\x{BFFFE}-\x{BFFFF}\x{CFFFE}-\x{CFFFF}\x{DFFFE}-\x{DFFFF}\x{EFFFE}-\x{EFFFF}\x{FFFFE}-\x{FFFFF}\x{10FFFE}-\x{10FFFF}]~u', '', $string);
2124
}
2125
2126 2
/**
2127
 * Validates a IPv6 address. returns true if it is ipv6.
2128 2
 *
2129 2
 * @param string $ip ip address to be validated
2130 2
 *
2131
 * @return bool true|false
2132 2
 */
2133
function isValidIPv6($ip)
2134
{
2135
	return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
2136 2
}
2137 2
2138 2
/**
2139
 * Converts IPv6s to numbers.  These make ban checks much easier.
2140 2
 *
2141
 * @param string $ip ip address to be converted
2142
 *
2143
 * @return int[] array
2144
 */
2145
function convertIPv6toInts($ip)
2146
{
2147
	static $expanded = [];
2148
2149
	// Check if we have done this already.
2150
	if (isset($expanded[$ip]))
2151
	{
2152 17
		return $expanded[$ip];
2153 17
	}
2154
2155 17
	// Expand the IP out.
2156
	$expanded_ip = explode(':', expandIPv6($ip));
0 ignored issues
show
Bug introduced by
It seems like expandIPv6($ip) can also be of type boolean; however, parameter $string of explode() 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

2156
	$expanded_ip = explode(':', /** @scrutinizer ignore-type */ expandIPv6($ip));
Loading history...
2157
2158 1
	$new_ip = [];
2159 1
	foreach ($expanded_ip as $int)
2160
	{
2161
		$new_ip[] = hexdec($int);
2162 1
	}
2163
2164
	// Save this in case of repeated use.
2165 17
	$expanded[$ip] = $new_ip;
2166
2167
	return $expanded[$ip];
2168
}
2169
2170
/**
2171
 * Expands a IPv6 address to its full form.
2172
 *
2173
 * @param string $addr ipv6 address string
2174
 * @param bool $strict_check checks length to expanded address for compliance
2175
 *
2176
 * @return bool|string expanded ipv6 address.
2177
 */
2178
function expandIPv6($addr, $strict_check = true)
2179
{
2180
	static $converted = [];
2181
2182
	// Check if we have done this already.
2183
	if (isset($converted[$addr]))
2184
	{
2185
		return $converted[$addr];
2186
	}
2187
2188
	// Check if there are segments missing, insert if necessary.
2189
	if (strpos($addr, '::') !== false)
2190
	{
2191
		$part = explode('::', $addr);
2192
		$part[0] = explode(':', $part[0]);
2193
		$part[1] = explode(':', $part[1]);
2194
		$missing = [];
2195 3
2196
		// Looks like this is an IPv4 address
2197
		if (isset($part[1][1]) && strpos($part[1][1], '.') !== false)
2198
		{
2199
			$ipoct = explode('.', $part[1][1]);
2200
			$p1 = dechex($ipoct[0]) . dechex($ipoct[1]);
0 ignored issues
show
Bug introduced by
$ipoct[0] of type string is incompatible with the type integer expected by parameter $num of dechex(). ( Ignorable by Annotation )

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

2200
			$p1 = dechex(/** @scrutinizer ignore-type */ $ipoct[0]) . dechex($ipoct[1]);
Loading history...
2201
			$p2 = dechex($ipoct[2]) . dechex($ipoct[3]);
2202
2203
			$part[1] = [
2204
				$part[1][0],
2205
				$p1,
2206
				$p2
2207
			];
2208
		}
2209
2210
		$limit = count($part[0]) + count($part[1]);
2211
		for ($i = 0; $i < (8 - $limit); $i++)
2212
		{
2213
			$missing[] = '0000';
2214
		}
2215
2216
		$part = array_merge($part[0], $missing, $part[1]);
2217
	}
2218
	else
2219
	{
2220
		$part = explode(':', $addr);
2221
	}
2222
2223
	// Pad each segment until it has 4 digits.
2224
	foreach ($part as &$p)
2225
	{
2226
		while (strlen($p) < 4)
2227
		{
2228
			$p = '0' . $p;
2229
		}
2230
	}
2231
2232
	unset($p);
2233
2234
	// Join segments.
2235
	$result = implode(':', $part);
2236
2237
	// Save this in case of repeated use.
2238
	$converted[$addr] = $result;
2239
2240
	// Quick check to make sure the length is as expected.
2241
	if (!$strict_check || strlen($result) == 39)
2242
	{
2243
		return $result;
2244
	}
2245
2246
	return false;
2247
}
2248
2249
/**
2250
 * Removed in 2.0, always returns false.
2251
 *
2252
 * Logs the depreciation notice, returns false, sets context value such that
2253
 * old themes don't go sour ;)
2254
 *
2255
 * @param string $browser the browser we are checking for.
2256
 */
2257
function isBrowser($browser)
0 ignored issues
show
Unused Code introduced by
The parameter $browser 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

2257
function isBrowser(/** @scrutinizer ignore-unused */ $browser)

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...
2258
{
2259
	global $context;
2260
2261
	\ElkArte\Errors::instance()->log_deprecated('isBrowser()', 'Nothing');
2262
2263
	$context['browser_body_id'] = 'elkarte';
2264
2265
	return false;
2266
}
2267