ip2range()   C
last analyzed

Complexity

Conditions 13
Paths 18

Size

Total Lines 69
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 25.6195

Importance

Changes 0
Metric Value
cc 13
eloc 31
c 0
b 0
f 0
nc 18
nop 1
dl 0
loc 69
rs 6.6166
ccs 11
cts 19
cp 0.5789
crap 25.6195

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

325
		$s = !str_contains(/** @scrutinizer ignore-type */ User::$info->time_format, '%S') ? '' : ':%S';
Loading history...
326
		if (!str_contains(User::$info->time_format, '%H') && !str_contains(User::$info->time_format, '%T'))
327
		{
328
			$h = !str_contains(User::$info->time_format, '%l') ? '%I' : '%l';
329
			$today_fmt = $h . ':%M' . $s . ' %p';
330
		}
331
		else
332 18
		{
333
			$today_fmt = '%H:%M' . $s;
334
		}
335
336
		// Same day of the year, same year.... Today!
337
		if ($then['yday'] === $now['yday'] && $then['year'] === $now['year'])
338 18
		{
339
			return sprintf($txt['today'], standardTime($log_time, $today_fmt, $offset_type));
340
		}
341
342
		// Day-of-year is one less and the same year, or it's the first of the year and that's the last of the year...
343
		if ((int) $modSettings['todayMod'] === 2
344
			&& (($then['yday'] === $now['yday'] - 1 && $then['year'] === $now['year'])
345 18
				|| (($now['yday'] === 0 && $then['year'] === $now['year'] - 1) && $then['mon'] === 12 && $then['mday'] === 31)))
346
		{
347
			return sprintf($txt['yesterday'], standardTime($log_time, $today_fmt, $offset_type));
348
		}
349
	}
350
351
	$str = is_bool($show_today) ? User::$info->time_format : $show_today;
352
353
	// Windows requires a slightly different language code identifier (LCID).
354
	// https://msdn.microsoft.com/en-us/library/cc233982.aspx
355
	if ($is_win)
356
	{
357 18
		$txt['lang_locale'] = str_replace('_', '-', $txt['lang_locale']);
358
	}
359
360
	if (setlocale(LC_TIME, $txt['lang_locale']))
361
	{
362
		if (!isset($non_twelve_hour))
363
		{
364
			$non_twelve_hour = trim(Util::strftime('%p')) === '';
365
		}
366
367
		if ($non_twelve_hour && str_contains($str, '%p'))
368
		{
369
			$str = str_replace('%p', (Util::strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
370
		}
371
372
		foreach (['%a', '%A', '%b', '%B'] as $token)
373
		{
374
			if (str_contains($str, $token))
375
			{
376 10
				$str = str_replace($token, empty($txt['lang_capitalize_dates']) ? Util::strftime($token, $time) : Util::ucwords(Util::strftime($token, $time)), $str);
377 10
			}
378
		}
379
	}
380 10
	else
381
	{
382
		// Do-it-yourself time localization.  Fun.
383 2
		foreach (['%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months'] as $token => $text_label)
384
		{
385
			if (str_contains($str, $token))
386
			{
387
				$str = str_replace($token, $txt[$text_label][(int) Util::strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
388
			}
389 2
		}
390 2
391 2
		if (str_contains($str, '%p'))
392
		{
393
			$str = str_replace('%p', (Util::strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
394
		}
395 10
	}
396
397
	// Windows doesn't support %e; on some versions, strftime fails altogether if used, so let's prevent that.
398
	if ($is_win && str_contains($str, '%e'))
399
	{
400
		$str = str_replace('%e', ltrim(Util::strftime('%d', $time), '0'), $str);
401
	}
402
403
	// Format any other characters.
404
	return Util::strftime($str, $time);
405
}
406
407
/**
408
 * Used to render a timestamp to html5 <time> tag format.
409
 *
410
 * @param int $timestamp A unix timestamp
411
 *
412
 * @return string
413
 */
414
function htmlTime($timestamp)
415
{
416
	global $txt, $context;
417
418
	if (empty($timestamp))
419
	{
420
		return '';
421
	}
422
423
	$forumtime = forum_time(false, $timestamp);
424
	$timestamp = forum_time(true, $timestamp);
425
	$time = date('Y-m-d H:i', $timestamp);
426
	$stdtime = standardTime($timestamp, true, true);
427
428
	// @todo maybe htmlspecialchars on the title attribute?
429
	return '<time title="' . (empty($context['using_relative_time']) ? $txt['last_post'] : $stdtime) . '" datetime="' . $time . '" data-timestamp="' . $timestamp . '" data-forumtime="' . $forumtime . '">' . $stdtime . '</time>';
430 2
}
431
432 2
/**
433 2
 * Convert a given timestamp to UTC time in the format of Atom date format.
434
 *
435 2
 * This method takes a unix timestamp as input and converts it to UTC time in the format of
436
 * Atom date format (YYYY-MM-DDTHH:MM:SS+00:00).
437 2
 *
438
 * It considers the user's time offset, system's time offset, and the default timezone setting
439
 * from the modifications/settings administration panel.
440 2
 *
441
 * @param int $timestamp The timestamp to convert to UTC time.
442
 * @param int $userAdjust The timestamp is not to be adjusted for user offset
443 2
 * @return string The UTC time in the format of Atom date format.
444
 */
445
function utcTime($timestamp, $userAdjust = false)
446
{
447
	global $modSettings;
448
449
	// Back out user time
450
	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...
451
	{
452
		$timestamp -= ($modSettings['time_offset'] + User::$info->time_offset) * 3600;
453
	}
454
455
	// Using the system timezone offset, format the date
456
	try
457
	{
458
		$tz = empty($modSettings['default_timezone']) ? 'UTC' : $modSettings['default_timezone'];
459
		$date = new DateTime('@' . $timestamp, new DateTimeZone($tz));
460
	}
461
	catch (Exception)
462
	{
463
		return standardTime($timestamp);
464
	}
465
466
	// Something like 2012-12-21T11:11:00+00:00
467 22
	return $date->format(DateTimeInterface::ATOM);
468 22
}
469
470 22
/**
471
 * Gets the current time with offset.
472 2
 *
473
 * What it does:
474
 *
475
 * - Always applies the offset in the time_offset setting.
476 22
 *
477
 * @param bool $use_user_offset = true if use_user_offset is true, applies the user's offset as well
478 16
 * @param int|null $timestamp = null A unix timestamp (null to use current time)
479
 *
480
 * @return int seconds since the unix epoch
481 18
 */
482
function forum_time($use_user_offset = true, $timestamp = null): int
483
{
484
	global $modSettings;
485
486
	if ($timestamp === null)
487 18
	{
488
		$timestamp = time();
489
	}
490
	elseif ($timestamp === 0)
491 22
	{
492
		return 0;
493
	}
494
495
	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...
496
}
497 22
498
/**
499
 * Removes special entities from strings.  Compatibility...
500 22
 *
501
 * - Faster than html_entity_decode
502 22
 * - Removes the base entities (&amp; &quot; &#039; &lt; and &gt;.) from text with htmlspecialchars_decode
503 22
 * - Additionally, converts &nbsp with str_replace
504
 *
505
 * @param string $string The string to apply htmlspecialchars_decode
506 22
 *
507 22
 * @return string string without entities
508
 */
509 22
function un_htmlspecialchars($string)
510 22
{
511
	if (empty($string))
512
	{
513
		return $string;
514
	}
515
516
	$string = htmlspecialchars_decode($string, ENT_QUOTES);
517
518 22
	return str_replace('&nbsp;', ' ', $string);
519
}
520 16
521
/**
522
 * Lexicographic permutation function.
523
 *
524 8
 * This is a special type of permutation that involves the order of the set. The next
525
 * lexicographic permutation of '32541' is '34125'. Numerically, it is simply the smallest
526
 * set larger than the current one.
527
 *
528
 * The benefit of this over a recursive solution is that the whole list does NOT need
529
 * to be held in memory. So it's actually possible to run 30! permutations without
530 22
 * causing a memory overflow.
531
 *
532
 * Source: O'Reilly PHP Cookbook
533
 *
534 22
 * @param array $p The array keys to apply permutation
535
 * @param int $size The size of our permutation array
536
 *
537
 * @return array|bool the next permutation of the passed array $p
538
 */
539 22
function pc_next_permutation($p, $size)
540
{
541
	// Slide down the array looking for where we're smaller than the next guy
542
	for ($i = $size - 1; isset($p[$i]) && $p[$i] >= $p[$i + 1]; --$i)
543
	{
544
		// Required to set $i
545
	}
546
547
	// If this doesn't occur, we've finished our permutations
548
	// the array is reversed: (1, 2, 3, 4) => (4, 3, 2, 1)
549
	if ($i === -1)
550
	{
551
		return false;
552
	}
553
554
	// Slide down the array looking for a bigger number than what we found before
555
	for ($j = $size; $p[$j] <= $p[$i]; --$j)
556
	{
557
		// Required to set $j
558
	}
559
560
	// Swap them
561 22
	$tmp = $p[$i];
562
	$p[$i] = $p[$j];
563 22
	$p[$j] = $tmp;
564
565 11
	// Now reverse the elements in between by swapping the ends
566
	for (++$i, $j = $size; $i < $j; ++$i, --$j)
567
	{
568
		$tmp = $p[$i];
569 22
		$p[$i] = $p[$j];
570
		$p[$j] = $tmp;
571 16
	}
572
573
	return $p;
574
}
575
576 22
/**
577
 * Ends execution and redirects the user to a new location
578
 *
579
 * What it does:
580
 *
581
 * - Makes sure the browser doesn't come back and repost the form data.
582 22
 * - Should be used whenever anything is posted.
583
 * - Diverts final execution to obExit() which means an end to processing and sending of final output
584
 *
585
 * @event integrate_redirect called before headers are sent
586
 * @param string $setLocation = '' The URL to redirect to
587
 *
588
 * Never returns.  However, it will return the $setLocation string when run via the testbed so that
589
 * controller tests can complete.
590
 */
591
function redirectexit($setLocation = ''): never
592
{
593
	global $db_show_debug;
594 18
595
	// Note to developers.  The testbed will automatically add the following, allowing phpunit test returns
596 18
	//if (defined("PHPUNITBOOTSTRAP") && defined("STDIN")){return $setLocation;} see setup-elkarte.sh
597
598 2
	// Send headers, call integration, do maintenance
599
	Headers::instance()
600
		->removeHeader('all')
601 18
		->redirect($setLocation)
602 18
		->send();
603 18
604
	// Debugging.
605
	if ($db_show_debug === true)
606 18
	{
607
		$_SESSION['debug_redirect'] = Debug::instance()->get_db();
608
	}
609
610
	obExit(false);
611
}
612
613
/**
614
 * Ends execution.
615
 *
616
 * What it does:
617
 *
618
 * - Takes care of template loading and remembering the previous URL.
619
 * - Calls ob_start() with ob_sessrewrite to fix URLs if necessary.
620
 *
621
 * @event integrate_invalid_old_url allows adding to "from" urls we don't save
622
 * @event integrate_exit inform portal, etc. that we're integrated with to exit
623 25
 * @param bool|null $header = null Output the header
624
 * @param bool|null $do_footer = null Output the footer
625 25
 * @param bool $from_index = false If we're coming from index.php
626
 * @param bool $from_fatal_error = false If we are exiting due to a fatal error
627 25
 * @throws \ElkArte\Exceptions\Exception
628
 */
629 22
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false): never
0 ignored issues
show
Unused Code introduced by
The parameter $from_index 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

629
function obExit($header = null, $do_footer = null, /** @scrutinizer ignore-unused */ $from_index = false, $from_fatal_error = false): never

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...
630
{
631
	global $context, $txt, $db_show_debug;
632
633
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
634 25
635
	// Attempt to prevent a recursive loop.
636
	++$level;
637
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
638
	{
639
		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...
640
	}
641
642
	if ($from_fatal_error)
643
	{
644
		$has_fatal_error = true;
645
	}
646
647
	$do_header = $header ?? !$header_done;
648
	$do_footer = $do_footer ?? $do_header;
649
650 58
	// Has the template/header been done yet?
651
	if ($do_header)
652 58
	{
653
		handleMaintenance();
654
655
		// Was the page title set last minute? Also update the HTML safe one.
656
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
657
		{
658
			$context['page_title_html_safe'] = Util::htmlspecialchars(un_htmlspecialchars($context['page_title'])) . (empty($context['current_page']) ? '' : ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1));
659
		}
660
661
		// Start up the session URL fixer.
662
		ob_start('ob_sessrewrite');
663
664
		call_integration_buffer();
665
666
		// Display the screen in the logical order.
667
		template_header();
668
		$header_done = true;
669
	}
670
671
	if ($do_footer)
672
	{
673
		// Show the footer.
674
		theme()->getTemplates()->loadSubTemplate($context['sub_template'] ?? 'main');
675
676 4
		// Just so we don't get caught in an endless loop of errors from the footer...
677
		if (!$footer_done)
678
		{
679
			$footer_done = true;
680
			template_footer();
681
682 4
			// Add $db_show_debug = true; to Settings.php if you want to show the debugging information.
683
			// (since this is just debugging... it's okay that it's after </html>.)
684 4
			if (($db_show_debug === true)
685
				&& !isset($_REQUEST['api'])
686
				&& ((!isset($_GET['action']) || $_GET['action'] !== 'viewquery') && !isset($_GET['api'])))
687
			{
688 4
				Debug::instance()->display();
689
			}
690
		}
691
	}
692
693 4
	// Need a user agent
694 4
	$req = Request::instance();
695 4
696
	setOldUrl();
697
698 4
	// For session check verification... don't switch browsers...
699
	$_SESSION['USER_AGENT'] = $req->user_agent();
700 4
701 4
	// Hand off the output to the portal, etc. we're integrated with.
702 4
	call_integration_hook('integrate_exit', [$do_footer]);
703
704
	// Note to developers.  The testbed will add the following, allowing phpunit test returns
705 4
	//if (defined("PHPUNITBOOTSTRAP") && defined("STDIN")){return;}
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
 * Takes care of a few dynamic maintenance items
712
 */
713
function handleMaintenance()
714
{
715
	global $context;
716
717
	// Clear out the stat cache.
718
	trackStats();
719
720
	// Send off any notifications accumulated
721
	Notifications::instance()->send();
722
723
	// Queue any mail that needs to be sent
724
	if (!empty($context['flush_mail']))
725
	{
726 16
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
727
		AddMailQueue(true);
728
	}
729 16
}
730
731
/**
732
 * Sets the old URL in the session for reference if certain conditions are met.
733
 *
734
 * This method tracks the current URL and stores it in the session under the provided index,
735 16
 * unless the URL matches a set of invalid patterns. A hook allows customization of the
736
 * invalid patterns.
737 16
 *
738
 * @param string $index The session index under which the URL will be stored. Defaults to 'old_url'.
739 16
 * @return void
740
 */
741 16
function setOldUrl($index = 'old_url'): void
742
{
743
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
744
	$invalid_old_url = [
745 16
		'action=dlattach',
746
		'action=jsoption',
747 16
		';api=xml',
748
		';api=json',
749
		'action=mentions',
750
	];
751
	call_integration_hook('integrate_invalid_old_url', [&$invalid_old_url]);
752
	$make_old = true;
753
	foreach ($invalid_old_url as $url)
754
	{
755
		if (str_contains($_SERVER['REQUEST_URL'], $url))
756 16
		{
757
			$make_old = false;
758
			break;
759 16
		}
760
	}
761 16
762
	if ($make_old)
763
	{
764
		$_SESSION[$index] = $_SERVER['REQUEST_URL'];
765
	}
766
}
767
768
/**
769
 * Sets the class of the current topic based on is_very_hot, veryhot, hot, etc.
770
 *
771
 * @param array $topic_context array of topic information
772
 */
773
function determineTopicClass(&$topic_context)
774
{
775
	$topic_context['class'] = empty($topic_context['is_poll']) ? 'i-normal' : 'i-poll';
776
777
	// Set a topic class depending on locked status and number of replies.
778
	if ($topic_context['is_very_hot'])
779
	{
780
		$topic_context['class'] = 'i-hot colorize-red';
781
	}
782
	elseif ($topic_context['is_hot'])
783
	{
784
		$topic_context['class'] = 'i-hot colorize-yellow';
785
	}
786
787
	if ($topic_context['is_sticky'])
788
	{
789
		$topic_context['class'] = 'i-sticky';
790
	}
791
792
	if ($topic_context['is_locked'])
793
	{
794
		$topic_context['class'] = 'i-locked';
795
	}
796
}
797
798
/**
799
 * Sets up the basic theme context stuff.
800
 *
801
 * @param bool $forceload defaults to false
802
 */
803
function setupThemeContext($forceload = false)
804
{
805
	theme()->setupThemeContext($forceload);
806
}
807
808
/**
809
 * Helper function to convert memory string settings to bytes
810
 *
811
 * @param string|bool $val The byte string, like 256M or 1G
812
 *
813
 * @return int The string converted to a proper integer in bytes
814
 */
815
function memoryReturnBytes($val)
816
{
817
	// Treat blank values as 0
818
	$val = is_bool($val) || empty($val) ? '0' : trim($val);
819
820
	// Separate the number from the designator, if any
821
	preg_match('~(\d+)(.*)~', $val, $matches);
822
	$num = (int) $matches[1];
823
	$last = strtolower(substr($matches[2] ?? '', 0, 1));
824
825
	// Convert to bytes
826
	switch ($last)
827
	{
828
		// fall through select g = 1024*1024*1024
829
		case 'g':
830
			$num *= 1024;
831
		// fall through select m = 1024*1024
832
		case 'm':
833
			$num *= 1024;
834
		// fall through select k = 1024
835
		case 'k':
836
			$num *= 1024;
837
	}
838
839
	return $num;
840
}
841
842
/**
843
 * This is the only template included in the sources.
844
 * @return void
845
 */
846
function template_rawdata()
847
{
848
	theme()->template_rawdata();
849
}
850
851
/**
852
 * The header template
853
 * @return void
854
 */
855
function template_header()
856
{
857
	theme()->template_header();
858
}
859
860
/**
861
 * Show the copyright.
862
 * @return void
863
 */
864
function theme_copyright()
865
{
866
	theme()->theme_copyright();
867
}
868
869
/**
870
 * The template footer
871
 * @return void
872
 */
873
function template_footer()
874
{
875
	theme()->template_footer();
876
}
877
878
/**
879
 * Output the JavaScript files
880
 *
881
 * @depreciated since 2.0, only for old theme support
882
 * @return void
883
 */
884
function template_javascript()
885
{
886
	theme()->themeJs()->template_javascript();
887
}
888
889
/**
890
 * Output the CSS files
891
 *
892
 * @depreciated since 2.0, only for old theme support
893
 * @return void
894
 */
895
function template_css()
896
{
897
	theme()->themeCss()->template_css();
898
}
899
900
/**
901
 * Calls on template_show_error from index.template.php to show warnings
902
 * and security errors for admins
903
 * @return void
904
 */
905
function template_admin_warning_above()
906
{
907
	theme()->template_admin_warning_above();
908
}
909
910
/**
911
 * Convert IP address to IP range
912
 *
913
 *  - Internal function used to convert a user-readable format to a format suitable for the database.
914
 *
915
 * @param string $fullip The IP address to convert
916
 * @return array The IP range in the format [ ['low' => 'low_value_1', 'high' => 'high_value_1'], ... ]
917
 * If the input IP address is invalid or cannot be converted, an empty array is returned.
918
 */
919
function ip2range($fullip)
920
{
921
	// If its IPv6, validate it first.
922
	if (isValidIPv6($fullip))
923
	{
924
		$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

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

1029
			if (stripos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
1030
			{
1031
				$host = '';
1032
			}
1033
			// Invalid server option?
1034
			elseif ((stripos($test, 'invalid option') || stripos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
1035
			{
1036
				updateSettings(['host_to_dis' => 1]);
1037
			}
1038
			// Maybe it found something, after all?
1039
			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

1039
			elseif (preg_match('~\s(\S+?)\.\s~', /** @scrutinizer ignore-type */ $test, $match) === 1)
Loading history...
1040
			{
1041
				$host = $match[1];
1042
			}
1043
		}
1044
1045
		// This is nslookup; usually default on Windows, and possibly some Unix with bind-utils
1046
		if ((empty($host) || PHP_OS_FAMILY === 'Windows') && mt_rand(0, 1) === 1)
1047
		{
1048
			$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
1049
1050
			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

1050
			if (stripos(/** @scrutinizer ignore-type */ $test, 'Non-existent domain') !== false)
Loading history...
1051
			{
1052
				$host = '';
1053
			}
1054
			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

1054
			elseif (preg_match('~(?:Name:|Name =)\s+(\S+)~i', /** @scrutinizer ignore-type */ $test, $match) === 1)
Loading history...
1055
			{
1056
				$host = $match[1];
1057
			}
1058
		}
1059
	}
1060
1061
	// This is the last try :/.
1062
	if (!isset($host))
1063
	{
1064
		$host = @gethostbyaddr($ip);
1065
	}
1066
1067
	// It took a long time, so let's cache it!
1068
	if (microtime(true) - $t > 0.5)
1069
	{
1070
		$cache->put('hostlookup-' . $ip, $host, 600);
1071
	}
1072
1073
	return $host;
1074
}
1075
1076
/**
1077
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
1078
 *
1079
 * @param string $text The string to process
1080
 *     - If encrypt = true, this is the maximum number of bytes to use in integer hashes (for searching)
1081
 *     - If encrypt = false, this is the maximum number of letters in each word
1082
 * @param bool $encrypt = false Used for custom search indexes to return an int[] array representing the words
1083
 *
1084
 * @return array
1085
 */
1086
function text2words($text, $encrypt = false)
1087
{
1088
	// Step 0: prepare numbers so they are good for search & index 1000.45 -> 1000_45
1089
	$words = preg_replace('~([\d]+)[.-/]+(?=[\d])~u', '$1_', $text);
1090
1091
	// Step 1: Remove entities/things we don't consider words:
1092
	$words = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', strtr($words, ['<br />' => ' ']));
1093
1094
	// Step 2: Entities we left to letters, where applicable, lowercase.
1095
	$words = un_htmlspecialchars(Util::strtolower($words));
1096
1097
	// Step 3: Ready to split apart and index!
1098
	$words = explode(' ', $words);
1099
1100
	if ($encrypt)
1101
	{
1102
		$blocklist = getBlocklist();
1103
		$returned_ints = [];
1104
1105
		// Only index unique words
1106
		$words = array_unique($words);
1107
		foreach ($words as $word)
1108
		{
1109
			$word = trim($word, "-_'");
1110
			if ($word !== '' && !in_array($word, $blocklist) && Util::strlen($word) > 2)
1111
			{
1112
				// Get a hex representation of this word using a database indexing hash
1113
				// designed to be fast while maintaining a low collision rate
1114
				$encrypted = hash('murmur3a', $word);
1115
1116
				// Create an integer representation, the hash is an 8-char hex,
1117
				// so the largest int will be 4294967295 which fits in db int(10)
1118
				$returned_ints[$word] = hexdec($encrypted);
1119
			}
1120
		}
1121
1122
		return $returned_ints;
1123
	}
1124
1125
	// Trim characters before and after and add slashes for database insertion.
1126
	$returned_words = [];
1127
	foreach ($words as $word)
1128
	{
1129
		if (($word = trim($word, "-_'")) !== '')
1130
		{
1131
			$returned_words[] = substr($word, 0, 20);
1132
		}
1133
	}
1134
1135
	// Filter out all words that occur more than once.
1136
	return array_unique($returned_words);
1137
}
1138
1139
/**
1140
 * Get the blocklist from the search controller.
1141
 *
1142
 * @return array
1143
 */
1144
function getBlocklist()
1145
{
1146
	static $blocklist;
1147
1148
	if (!isset($blocklist))
1149
	{
1150
		$search = new Search();
1151
		$blocklist = $search->getBlockListedWords();
1152
		unset($search);
1153
	}
1154
1155
	return $blocklist;
1156
}
1157
1158
/**
1159
 * Sets up all the top menu buttons
1160
 *
1161
 * What it does:
1162
 *
1163
 * - Defines every master item in the menu, as well as any sub-items
1164
 * - Ensures the chosen action is set so the menu is highlighted
1165
 * - Saves them in the cache if it is available and on
1166
 * - Places the results in $context
1167
 */
1168
function setupMenuContext()
1169
{
1170
	(new MenuContext())->setupMenuContext();
1171
}
1172
1173
/**
1174
 * Process functions of an integration hook.
1175
 *
1176
 * What it does:
1177
 *
1178
 * - Calls all functions of the given hook.
1179
 * - Supports static class method calls.
1180
 *
1181
 * @param string $hook The name of the hook to call
1182
 * @param array $parameters = array() Parameters to pass to the hook
1183
 *
1184
 * @return array the results of the functions
1185
 */
1186
function call_integration_hook($hook, $parameters = [])
1187
{
1188
	return Hooks::instance()->hook($hook, $parameters);
1189
}
1190
1191
/**
1192
 * Includes files for hooks that only do that (i.e., integrate_pre_include)
1193
 *
1194
 * @param string $hook The name to include
1195
 */
1196
function call_integration_include_hook($hook)
1197
{
1198
	Hooks::instance()->include_hook($hook);
1199
}
1200
1201
/**
1202
 * Special hook call executed during obExit
1203
 */
1204
function call_integration_buffer()
1205
{
1206
	Hooks::instance()->buffer_hook();
1207
}
1208
1209
/**
1210
 * Add a function for integration hook.
1211
 *
1212
 * - Does nothing if the function is already added.
1213
 *
1214
 * @param string $hook The name of the hook to add
1215
 * @param string $function The function associated with the hook
1216
 * @param string $file The file that contains the function
1217
 * @param bool $permanent = true if true, updates the value in settings table
1218
 */
1219
function add_integration_function($hook, $function, $file = '', $permanent = true)
1220
{
1221
	Hooks::instance()->add($hook, $function, $file, $permanent);
1222
}
1223
1224
/**
1225
 * Remove an integration hook function.
1226
 *
1227
 * What it does:
1228
 *
1229
 * - Removes the given function from the given hook.
1230
 * - Does nothing if the function is not available.
1231
 *
1232
 * @param string $hook The name of the hook to remove
1233
 * @param string $function The name of the function
1234
 * @param string $file The file is located in
1235
 */
1236
function remove_integration_function($hook, $function, $file = '')
1237
{
1238
	Hooks::instance()->remove($hook, $function, $file);
1239
}
1240
1241
/**
1242
 * Decode numeric HTML entities to their UTF8 equivalent character.
1243
 *
1244
 * What it does:
1245
 *
1246
 * - Callback function for preg_replace_callback in subs-members
1247
 * - Uses capture group 2 in the supplied array
1248
 * - Does basic scan to ensure characters are inside a valid range
1249
 *
1250
 * @param array $matches matches from a preg_match_all
1251
 *
1252
 * @return string $string
1253
 */
1254
function replaceEntities__callback($matches)
1255
{
1256 26
	if (!isset($matches[2]))
1257
	{
1258
		return '';
1259 26
	}
1260
1261
	$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

1261
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
1262 26
1263
	// remove left to right / right to left overrides
1264 26
	if ($num === 0x202D || $num === 0x202E)
1265
	{
1266
		return '';
1267
	}
1268
1269
	// Quote, Ampersand, Apostrophe, Less/Greater Than get HTML replaced
1270
	if (in_array($num, [0x22, 0x26, 0x27, 0x3C, 0x3E]))
1271
	{
1272
		return '&#' . $num . ';';
1273
	}
1274
1275
	// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
1276
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
1277
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
1278
	{
1279
		return '';
1280
	}
1281
1282
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1283
	if ($num < 0x80)
1284
	{
1285
		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

1285
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
1286
	}
1287
1288
	// <0x800 (2048)
1289
	if ($num < 0x800)
1290
	{
1291
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1292
	}
1293 26
1294 26
	// < 0x10000 (65536)
1295
	if ($num < 0x10000)
1296 26
	{
1297
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1298 26
	}
1299
1300
	// <= 0x10FFFF (1114111)
1301
	return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1302
}
1303 26
1304
/**
1305
 * Converts HTML entities to utf8 equivalents
1306
 *
1307
 * What it does:
1308
 *
1309
 * - Callback function for preg_replace_callback
1310
 * - Uses capture group 1 in the supplied array
1311
 * - Does basic checks to keep characters inside a viewable range.
1312
 *
1313
 * @param array $matches array of matches as output from preg_match_all
1314
 *
1315
 * @return string $string
1316
 */
1317
function fixchar__callback($matches)
1318
{
1319
	if (!isset($matches[1]))
1320
	{
1321
		return '';
1322
	}
1323
1324
	$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

1324
	$num = $matches[1][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[1], 1)) : (int) $matches[1];
Loading history...
1325
1326
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
1327
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
1328
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
1329
	{
1330
		return '';
1331
	}
1332
1333
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1334
	if ($num < 0x80)
1335
	{
1336
		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

1336
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
1337 299
	}
1338
1339
	// <0x800 (2048)
1340
	if ($num < 0x800)
1341
	{
1342
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1343
	}
1344
1345
	// < 0x10000 (65536)
1346
	if ($num < 0x10000)
1347 231
	{
1348 231
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1349
	}
1350
1351
	// <= 0x10FFFF (1114111)
1352
	return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1353
}
1354
1355
/**
1356
 * Strips out invalid HTML entities, replaces others with HTML style &#123; codes
1357
 *
1358
 * What it does:
1359
 *
1360
 * - Callback function used of preg_replace_callback in various $ent_checks,
1361
 * - For example, strpos, strlen, substr, etc.
1362
 *
1363
 * @param array $matches array of matches for a preg_match_all
1364
 *
1365
 * @return string
1366
 */
1367
function entity_fix__callback($matches)
1368
{
1369
	if (!isset($matches[2]))
1370 4
	{
1371 4
		return '';
1372
	}
1373
1374
	$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

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

1485
	return array_merge(array_slice($input, 0, $position), $insert, array_slice($input, /** @scrutinizer ignore-type */ $position));
Loading history...
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

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

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

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

2170
	$expanded_ip = explode(':', /** @scrutinizer ignore-type */ expandIPv6($ip));
Loading history...
2171
2172
	$new_ip = [];
2173
	foreach ($expanded_ip as $int)
2174
	{
2175
		$new_ip[] = hexdec($int);
2176
	}
2177
2178
	// Save this in case of repeated use.
2179
	$expanded[$ip] = $new_ip;
2180
2181
	return $expanded[$ip];
2182
}
2183
2184
/**
2185
 * Expands an IPv6 address to its full form.
2186
 *
2187
 * @param string $addr ipv6 address string
2188
 * @param bool $strict_check checks length to expanded address for compliance
2189
 *
2190
 * @return bool|string expanded ipv6 address.
2191
 */
2192
function expandIPv6($addr, $strict_check = true)
2193
{
2194
	static $converted = [];
2195 3
2196
	// Check if we have done this already.
2197
	if (isset($converted[$addr]))
2198
	{
2199
		return $converted[$addr];
2200
	}
2201
2202
	// Check if there are segments missing, insert if necessary.
2203
	if (str_contains($addr, '::'))
2204
	{
2205
		$part = explode('::', $addr);
2206
		$part[0] = explode(':', $part[0]);
2207
		$part[1] = explode(':', $part[1]);
2208
		$missing = [];
2209
2210
		// Looks like this is an IPv4 address
2211
		if (isset($part[1][1]) && str_contains($part[1][1], '.'))
2212
		{
2213
			$ipoct = explode('.', $part[1][1]);
2214
			$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

2214
			$p1 = dechex(/** @scrutinizer ignore-type */ $ipoct[0]) . dechex($ipoct[1]);
Loading history...
2215
			$p2 = dechex($ipoct[2]) . dechex($ipoct[3]);
2216
2217
			$part[1] = [
2218
				$part[1][0],
2219
				$p1,
2220
				$p2
2221
			];
2222
		}
2223
2224
		$limit = count($part[0]) + count($part[1]);
2225
		for ($i = 0; $i < (8 - $limit); $i++)
2226
		{
2227
			$missing[] = '0000';
2228
		}
2229
2230
		$part = array_merge($part[0], $missing, $part[1]);
2231
	}
2232
	else
2233
	{
2234
		$part = explode(':', $addr);
2235
	}
2236
2237
	// Pad each segment until it has 4 digits.
2238
	foreach ($part as &$p)
2239
	{
2240
		while (strlen($p) < 4)
2241
		{
2242
			$p = '0' . $p;
2243
		}
2244
	}
2245
2246
	unset($p);
2247
2248
	// Join segments.
2249
	$result = implode(':', $part);
2250
2251
	// Save this in case of repeated use.
2252
	$converted[$addr] = $result;
2253
2254
	// Quick check to make sure the length is as expected.
2255
	if (!$strict_check || strlen($result) === 39)
2256
	{
2257
		return $result;
2258
	}
2259
2260
	return false;
2261
}
2262
2263
/**
2264
 * Extracts and normalizes the host of a URL to ASCII (Punycode) for reliable comparisons.
2265
 * Returns "" if a host can’t be determined.
2266
 */
2267
function iri_host_ascii($url)
2268
{
2269
	if ($url === '')
2270
	{
2271
		return '';
2272
	}
2273
2274
	// First attempt: normal parse
2275
	$host = parse_url($url, PHP_URL_HOST);
2276
2277
	// If parse_url failed and the URL contains Unicode, try to salvage the authority
2278
	if ($host === null || $host === false || $host === '')
2279
	{
2280
		if (preg_match('~^[a-z][a-z0-9+.-]*://([^/?#]+)~iu', $url, $m))
2281
		{
2282
			$host = $m[1];
2283
		}
2284
	}
2285
2286
	// Fallback for plain hostnames or IPs without http[s]:// scheme
2287
	if ($host === null || $host === false || $host === '')
2288
	{
2289
		$parsed = parse_url('scheme://' . $url);
2290
		if (isset($parsed['host']))
2291
		{
2292
			$host = $parsed['host'];
2293
		}
2294
	}
2295
2296
	if ($host === null || $host === false || $host === '')
2297
	{
2298
		return '';
2299
	}
2300
2301
	// Strip userinfo and port if present before IDNA
2302
	// e.g., user:pass@exämple.com:8080 -> exämple.com
2303
	if (str_contains($host, '@'))
2304
	{
2305
		$host = substr($host, strrpos($host, '@') + 1);
2306
	}
2307
2308
	if (str_contains($host, ':'))
2309
	{
2310
		$host = substr($host, 0, strpos($host, ':'));
2311
	}
2312
2313
	// Convert Unicode hostname to ASCII using UTS#46 (browser-compatible)
2314
	if (function_exists('idn_to_ascii'))
2315
	{
2316
		$ascii = idn_to_ascii($host, IDNA_DEFAULT, defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : 0);
2317
		if ($ascii !== false)
2318
		{
2319
			$host = $ascii;
2320
		}
2321
	}
2322
2323
	return strtolower($host);
2324
}
2325
2326
/**
2327
 * Removed in 2.0, always returns false.
2328
 *
2329
 * Logs the depreciation notice, returns false, sets context value such that
2330
 * old themes don't go sour ;)
2331
 *
2332
 * @param string $browser the browser we are checking for.
2333
 */
2334
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

2334
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...
2335
{
2336
	global $context;
2337
2338
	\ElkArte\Errors::instance()->log_deprecated('isBrowser()', 'Nothing');
2339
2340
	$context['browser_body_id'] = 'elkarte';
2341
2342
	return false;
2343
}
2344