response_prefix()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 0
Metric Value
cc 5
eloc 13
nc 3
nop 0
dl 0
loc 26
ccs 0
cts 8
cp 0
crap 30
rs 9.5222
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 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