Passed
Push — development ( f9d3d6...da8715 )
by Spuds
01:09 queued 25s
created

handleMaintance()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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

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

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

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

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

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

1017
			if (stripos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
1018
			{
1019
				$host = '';
1020
			}
1021
			// Invalid server option?
1022
			elseif ((stripos($test, 'invalid option') || stripos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
1023
			{
1024
				updateSettings(array('host_to_dis' => 1));
1025
			}
1026
			// Maybe it found something, after all?
1027
			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

1027
			elseif (preg_match('~\s([^\s]+?)\.\s~', /** @scrutinizer ignore-type */ $test, $match) == 1)
Loading history...
1028
			{
1029
				$host = $match[1];
1030
			}
1031
		}
1032
1033
		// This is nslookup; usually default on Windows, and possibly some Unix with bind-utils
1034
		if ((empty($host) || PHP_OS_FAMILY === 'Windows') && mt_rand(0, 1) === 1)
1035
		{
1036
			$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
1037
1038
			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

1038
			if (stripos(/** @scrutinizer ignore-type */ $test, 'Non-existent domain') !== false)
Loading history...
1039
			{
1040
				$host = '';
1041
			}
1042
			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

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

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

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

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

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

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

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

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

1312
	$num = $matches[1][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[1], 1)) : (int) $matches[1];
Loading history...
1313
1314
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
1315
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
1316
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
1317
	{
1318
		return '';
1319
	}
1320
1321
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1322
	if ($num < 0x80)
1323
	{
1324
		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

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

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

1473
	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

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

1799
	return trim(/** @scrutinizer ignore-type */ $text);
Loading history...
1800
}
1801
1802
/**
1803
 * Change a \t to a span that will show a tab
1804
 *
1805
 * @param string $string
1806
 *
1807
 * @return string
1808
 */
1809
function tabToHtmlTab($string)
1810
{
1811
	return str_replace("\t", "<span class=\"tab\">\t</span>", $string);
1812
}
1813
1814
/**
1815
 * Remove <br />
1816
 *
1817
 * @param string $string
1818
 *
1819
 * @return string
1820 4
 */
1821 4
function removeBr($string)
1822
{
1823 4
	return str_replace('<br />', '', $string);
1824
}
1825
1826 4
/**
1827
 * Replace all vulgar words with respective proper words. (substring or whole words..)
1828 2
 *
1829
 * What it does:
1830 2
 *  - it censors the passed string.
1831
 *  - if the admin setting allow_no_censored is on it does not censor unless force is also set.
1832
 *  - if the admin setting allow_no_censored is off will censor words unless the user has set
1833
 * it to not censor in their profile and force is off
1834
 *  - it caches the list of censored words to reduce parsing.
1835
 *  - Returns the censored text
1836
 *
1837
 * @param string $text
1838
 * @param bool $force = false
1839 2
 *
1840
 * @return string
1841
 */
1842 4
function censor($text, $force = false)
1843
{
1844
	global $modSettings;
1845
	static $censor = null;
1846
1847
	if ($censor === null)
1848
	{
1849
		$censor = new Censor(explode("\n", $modSettings['censor_vulgar']), explode("\n", $modSettings['censor_proper']), $modSettings);
1850
	}
1851
1852
	return $censor->censor($text, $force);
1853
}
1854
1855
/**
1856
 * Helper function able to determine if the current member can see at least
1857 2
 * one button of a button strip.
1858 2
 *
1859
 * @param array $button_strip
1860 2
 *
1861
 * @return bool
1862
 */
1863
function can_see_button_strip($button_strip)
1864
{
1865
	global $context;
1866
1867
	foreach ($button_strip as $value)
1868
	{
1869
		if (!isset($value['test']) || !empty($context[$value['test']]))
1870
		{
1871
			return true;
1872
		}
1873
	}
1874
1875
	return false;
1876
}
1877 14
1878
/**
1879 14
 * Get the current theme instance.
1880 14
 *
1881
 * @return \ElkArte\Themes\DefaultTheme\Theme The current theme instance.
1882
 */
1883
function theme()
1884
{
1885
	return $GLOBALS['context']['theme_instance'];
1886
}
1887
1888
/**
1889 14
 * Set the JSON template for sending JSON response.
1890
 *
1891 14
 * This method prepares the template layers, loads the 'Json' template,
1892
 * and sets the sub_template to 'send_json' in the global $context array.
1893 14
 * The JSON data is initialized to null.
1894 14
 *
1895
 * @return void
1896 14
 */
1897
function setJsonTemplate()
1898 14
{
1899
	global $context;
1900
1901 2
	$template_layers = $GLOBALS['context']['theme_instance']->getLayers();
1902
	$template_layers->removeAll();
1903
	$GLOBALS['context']['theme_instance']->getTemplates()->load('Json');
1904
	$context['sub_template'] = 'send_json';
1905
1906
	$context['json_data'] = null;
1907
}
1908
1909
function setPWACacheStale($refresh = false)
1910
{
1911
	global $modSettings;
1912
1913
	// We need a PWA cache stale to keep things moving, changing this will trigger a PWA cache flush
1914
	if (empty($modSettings['elk_pwa_cache_stale']) || $refresh)
1915
	{
1916
		$tokenizer = new TokenHash();
1917
		$elk_pwa_cache_stale = $tokenizer->generate_hash(8);
1918
		updateSettings(['elk_pwa_cache_stale' => $elk_pwa_cache_stale]);
1919
	}
1920
}
1921
1922
/**
1923
 * Send a 1x1 GIF response and terminate the script execution
1924
 *
1925
 * @param bool $expired Flag to determine if header Expires should be sent
1926
 *
1927
 * @return void
1928
 */
1929
function dieGif($expired = false)
1930
{
1931
	// The following is an attempt at stopping the behavior identified in #2391
1932
	if (function_exists('fastcgi_finish_request'))
1933 4
	{
1934
		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...
1935
	}
1936
1937
	$headers = Headers::instance();
1938
	if ($expired)
1939
	{
1940
		$headers
1941
			->header('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')
1942
			->header('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
1943
	}
1944
1945
	$headers->contentType('image/gif')->sendHeaders();
1946
	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...
1947
}
1948
1949
/**
1950
 * Prepare ob_start with or without gzip compression
1951
 *
1952
 * @param bool $use_compression Starts compressed headers.
1953
 */
1954
function obStart($use_compression = false)
1955
{
1956
	// This is done to clear any output that was made before now.
1957
	while (ob_get_level() > 0)
1958
	{
1959 229
		@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

1959
		/** @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...
1960
	}
1961
1962 229
	if ($use_compression)
1963
	{
1964
		ob_start('ob_gzhandler');
1965
	}
1966
	else
1967 229
	{
1968
		ob_start();
1969
		Headers::instance()->header('Content-Encoding', 'none');
1970
	}
1971
}
1972
1973
/**
1974
 * Returns a URL based on the parameters passed and the selected generator
1975
 *
1976
 * @param string $type The type of the URL (depending on the type, the
1977
 *                     generator can act differently
1978
 * @param array $params All the parameters of the URL
1979
 *
1980
 * @return string An URL
1981
 */
1982
function getUrl($type, $params)
1983
{
1984
	static $generator = null;
1985
1986
	if ($generator === null)
1987
	{
1988 24
		$generator = initUrlGenerator();
1989 24
	}
1990
1991 24
	return $generator->get($type, $params);
1992
}
1993 2
1994
/**
1995
 * Returns the query part of a URL based on the parameters passed and the selected generator
1996 24
 *
1997
 * @param string $type The type of the URL (depending on the type, the
1998
 *                     generator can act differently
1999
 * @param array $params All the parameters of the URL
2000
 *
2001
 * @return string The query part of an URL
2002
 */
2003
function getUrlQuery($type, $params)
2004
{
2005
	static $generator = null;
2006
2007
	if ($generator === null)
2008
	{
2009
		$generator = initUrlGenerator();
2010
	}
2011
2012
	return $generator->getQuery($type, $params);
2013
}
2014
2015
/**
2016
 * Initialize the URL generator
2017
 *
2018
 * @return object The URL generator object
2019
 */
2020
function initUrlGenerator()
2021
{
2022
	global $scripturl, $context, $url_format;
2023
2024
	$generator = new UrlGenerator([
2025
		'generator' => ucfirst($url_format ?? 'standard'),
2026
		'scripturl' => $scripturl,
2027 337
		'replacements' => [
2028
			'{session_data}' => isset($context['session_var']) ? $context['session_var'] . '=' . $context['session_id'] : ''
2029
		]
2030
	]);
2031
2032
	$generator->register('Topic');
2033
	$generator->register('Board');
2034
	$generator->register('Profile');
2035
2036
	return $generator;
2037
}
2038
2039
/**
2040
 * This function only checks if a certain feature (in core features)
2041
 * is enabled or not.
2042
 *
2043
 * @param string $feature The abbreviated code of a core feature
2044
 * @return bool true/false for enabled/disabled
2045
 */
2046
function featureEnabled($feature)
2047
{
2048
	global $modSettings, $context;
2049
	static $features = null;
2050
2051
	if ($features === null)
2052
	{
2053
		// This allows us to change the way things look for the admin.
2054
		$features = explode(',', $modSettings['admin_features'] ?? 'cd,cp,k,w,rg,ml,pm');
2055
2056
		// @deprecated since 2.0 - Next line is just for backward compatibility to remove before release
2057
		$context['admin_features'] = $features;
2058
	}
2059
2060
	return in_array($feature, $features, true);
2061
}
2062
2063
/**
2064
 * Clean up the XML to make sure it doesn't contain invalid characters.
2065
 *
2066
 * What it does:
2067
 *
2068
 * - Removes invalid XML characters to assure the input string being parsed properly.
2069
 *
2070
 * @param string $string The string to clean
2071
 *
2072
 * @return string The clean string
2073
 */
2074
function cleanXml($string)
2075
{
2076
	// https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Char
2077
	$string = preg_replace('~[\x00-\x08\x0B\x0C\x0E-\x1F\x{FFFE}\x{FFFF}]~u', '', $string);
2078
2079
	// Discouraged
2080
	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);
2081
}
2082
2083
/**
2084
 * Validates a IPv6 address. returns true if it is ipv6.
2085
 *
2086
 * @param string $ip ip address to be validated
2087
 *
2088 41
 * @return bool true|false
2089
 */
2090 41
function isValidIPv6($ip)
2091
{
2092 1
	return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
2093
}
2094
2095 41
/**
2096
 * Converts IPv6s to numbers.  These make ban checks much easier.
2097
 *
2098
 * @param string $ip ip address to be converted
2099
 *
2100
 * @return int[] array
2101
 */
2102
function convertIPv6toInts($ip)
2103
{
2104
	static $expanded = array();
2105
2106
	// Check if we have done this already.
2107
	if (isset($expanded[$ip]))
2108
	{
2109
		return $expanded[$ip];
2110
	}
2111
2112
	// Expand the IP out.
2113
	$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

2113
	$expanded_ip = explode(':', /** @scrutinizer ignore-type */ expandIPv6($ip));
Loading history...
2114
2115
	$new_ip = array();
2116
	foreach ($expanded_ip as $int)
2117
	{
2118
		$new_ip[] = hexdec($int);
2119
	}
2120
2121
	// Save this in case of repeated use.
2122
	$expanded[$ip] = $new_ip;
2123
2124
	return $expanded[$ip];
2125
}
2126 2
2127
/**
2128 2
 * Expands a IPv6 address to its full form.
2129 2
 *
2130 2
 * @param string $addr ipv6 address string
2131
 * @param bool $strict_check checks length to expanded address for compliance
2132 2
 *
2133
 * @return bool|string expanded ipv6 address.
2134
 */
2135
function expandIPv6($addr, $strict_check = true)
2136 2
{
2137 2
	static $converted = array();
2138 2
2139
	// Check if we have done this already.
2140 2
	if (isset($converted[$addr]))
2141
	{
2142
		return $converted[$addr];
2143
	}
2144
2145
	// Check if there are segments missing, insert if necessary.
2146
	if (strpos($addr, '::') !== false)
2147
	{
2148
		$part = explode('::', $addr);
2149
		$part[0] = explode(':', $part[0]);
2150
		$part[1] = explode(':', $part[1]);
2151
		$missing = array();
2152 17
2153 17
		// Looks like this is an IPv4 address
2154
		if (isset($part[1][1]) && strpos($part[1][1], '.') !== false)
2155 17
		{
2156
			$ipoct = explode('.', $part[1][1]);
2157
			$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

2157
			$p1 = dechex(/** @scrutinizer ignore-type */ $ipoct[0]) . dechex($ipoct[1]);
Loading history...
2158 1
			$p2 = dechex($ipoct[2]) . dechex($ipoct[3]);
2159 1
2160
			$part[1] = array(
2161
				$part[1][0],
2162 1
				$p1,
2163
				$p2
2164
			);
2165 17
		}
2166
2167
		$limit = count($part[0]) + count($part[1]);
2168
		for ($i = 0; $i < (8 - $limit); $i++)
2169
		{
2170
			$missing[] = '0000';
2171
		}
2172
2173
		$part = array_merge($part[0], $missing, $part[1]);
2174
	}
2175
	else
2176
	{
2177
		$part = explode(':', $addr);
2178
	}
2179
2180
	// Pad each segment until it has 4 digits.
2181
	foreach ($part as &$p)
2182
	{
2183
		while (strlen($p) < 4)
2184
		{
2185
			$p = '0' . $p;
2186
		}
2187
	}
2188
2189
	unset($p);
2190
2191
	// Join segments.
2192
	$result = implode(':', $part);
2193
2194
	// Save this in case of repeated use.
2195 3
	$converted[$addr] = $result;
2196
2197
	// Quick check to make sure the length is as expected.
2198
	if (!$strict_check || strlen($result) == 39)
2199
	{
2200
		return $result;
2201
	}
2202
2203
	return false;
2204
}
2205
2206
/**
2207
 * Removed in 2.0, always returns false.
2208
 *
2209
 * Logs the depreciation notice, returns false, sets context value such that
2210
 * old themes don't go sour ;)
2211
 *
2212
 * @param string $browser the browser we are checking for.
2213
 */
2214
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

2214
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...
2215
{
2216
	global $context;
2217
2218
	\ElkArte\Errors::instance()->log_deprecated('isBrowser()', 'Nothing');
2219
2220
	$context['browser_body_id'] = 'elkarte';
2221
2222
	return false;
2223
}
2224