Passed
Push — development ( b93807...dda237 )
by Emanuele
01:10 queued 23s
created

redirectexit()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 23
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 12.192

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 10
nc 3
nop 1
dl 0
loc 23
ccs 2
cts 10
cp 0.2
crap 12.192
rs 9.9332
c 1
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\Censor;
19
use ElkArte\Debug;
20
use ElkArte\GenericList;
21
use ElkArte\Hooks;
22
use ElkArte\Http\Headers;
23
use ElkArte\Notifications;
24
use ElkArte\Search\Search;
25
use ElkArte\UrlGenerator\UrlGenerator;
26
use ElkArte\User;
27
use ElkArte\Util;
28
29
/**
30
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
31
 *
32
 * What it does:
33
 *
34
 * - Updates both the settings table and $modSettings array.
35
 * - All of changeArray's indexes and values are assumed to have escaped apostrophes (')!
36
 * - If a variable is already set to what you want to change it to, that
37
 *   Variable will be skipped over; it would be unnecessary to reset.
38
 * - When update is true, UPDATEs will be used instead of REPLACE.
39
 * - When update is true, the value can be true or false to increment
40
 *  or decrement it, respectively.
41
 *
42
 * @param mixed[] $changeArray An associative array of what we're changing in 'setting' => 'value' format
43
 * @param bool $update Use an UPDATE query instead of a REPLACE query
44
 */
45
function updateSettings($changeArray, $update = false)
46 47
{
47
	global $modSettings;
48 47
49 47
	$db = database();
50
	$cache = Cache::instance();
51 47
52
	if (empty($changeArray) || !is_array($changeArray))
53
	{
54
		return;
55
	}
56
57 47
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
58
	if ($update)
59 28
	{
60
		foreach ($changeArray as $variable => $value)
61 28
		{
62
			$db->query('', '
63 28
				UPDATE {db_prefix}settings
64
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
65
				WHERE variable = {string:variable}',
66 28
				array(
67 28
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
68
					'variable' => $variable,
69
				)
70
			);
71 28
72
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
73
		}
74
75 28
		// Clean out the cache and make sure the cobwebs are gone too.
76
		$cache->remove('modSettings');
77 28
78
		return;
79
	}
80 35
81 35
	$replaceArray = array();
82
	foreach ($changeArray as $variable => $value)
83
	{
84 35
		// Don't bother if it's already like that ;).
85
		if (isset($modSettings[$variable]) && $modSettings[$variable] === $value)
86 14
		{
87
			continue;
88
		}
89
90 35
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
91
		if (!isset($modSettings[$variable]) && empty($value))
92
		{
93
			continue;
94
		}
95 35
96
		$replaceArray[] = array($variable, $value);
97 35
98
		$modSettings[$variable] = $value;
99
	}
100 35
101
	if (empty($replaceArray))
102 8
	{
103
		return;
104
	}
105 35
106 35
	$db->replace(
107 35
		'{db_prefix}settings',
108 15
		array('variable' => 'string-255', 'value' => 'string-65534'),
109 35
		$replaceArray,
110
		array('variable')
111
	);
112
113 35
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
114 35
	$cache->remove('modSettings');
115
}
116
117
/**
118
 * Deletes one setting from the settings table and takes care of $modSettings as well
119
 *
120
 * @param string|string[] $toRemove the setting or the settings to be removed
121
 */
122
function removeSettings($toRemove)
123 3
{
124
	global $modSettings;
125 3
126
	$db = database();
127 3
128
	if (empty($toRemove))
129
	{
130
		return;
131
	}
132 3
133
	if (!is_array($toRemove))
134 3
	{
135
		$toRemove = array($toRemove);
136
	}
137
138 3
	// Remove the setting from the db
139
	$db->query('', '
140
		DELETE FROM {db_prefix}settings
141
		WHERE variable IN ({array_string:setting_name})',
142 3
		array(
143
			'setting_name' => $toRemove,
144
		)
145
	);
146
147 3
	// Remove it from $modSettings now so it does not persist
148
	foreach ($toRemove as $setting)
149 3
	{
150
		if (isset($modSettings[$setting]))
151 2
		{
152
			unset($modSettings[$setting]);
153
		}
154
	}
155
156 3
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
157 3
	Cache::instance()->remove('modSettings');
158
}
159
160
/**
161
 * Constructs a page list.
162
 *
163
 * @depreciated since 2.0
164
 *
165
 */
166
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show = [])
167
{
168
	$pageindex = new \ElkArte\ConstructPageIndex($base_url, $start, $max_value, $num_per_page, $flexible_start, $show);
169
	return $pageindex->getPageIndex();
170
}
171
172
/**
173
 * Formats a number.
174
 *
175
 * What it does:
176
 *
177
 * - Uses the format of number_format to decide how to format the number.
178
 *   for example, it might display "1 234,50".
179
 * - Caches the formatting data from the setting for optimization.
180
 *
181
 * @param float $number The float value to apply comma formatting
182
 * @param int|bool $override_decimal_count = false or number of decimals
183
 *
184
 * @return string
185
 */
186
function comma_format($number, $override_decimal_count = false)
187
{
188
	global $txt;
189 18
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
190
191
	// Cache these values...
192 18
	if ($decimal_separator === null)
193 18
	{
194
		// Not set for whatever reason?
195 18
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
196
		{
197
			return $number;
198
		}
199 18
200
		// Cache these each load...
201
		$thousands_separator = $matches[1];
202 18
		$decimal_separator = $matches[2];
203
		$decimal_count = strlen($matches[3]);
204
	}
205
206
	// Format the string with our friend, number_format.
207 18
	return number_format($number, (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
0 ignored issues
show
Bug introduced by
It seems like (double)$number === $num...rride_decimal_count : 0 can also be of type true; however, parameter $decimals of number_format() 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

207
	return number_format($number, /** @scrutinizer ignore-type */ (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
Loading history...
208
}
209 6
210
/**
211
 * Formats a number to a multiple of thousands x, x k, x M, x G, x T
212
 *
213
 * @param float $number The value to format
214 12
 * @param int|bool $override_decimal_count = false or number of decimals
215
 *
216
 * @return string
217 18
 */
218
function thousands_format($number, $override_decimal_count = false)
219 18
{
220
	foreach (array('', ' k', ' M', ' G', ' T') as $kb)
221
	{
222 18
		if ($number < 1000)
223
		{
224
			break;
225
		}
226
227
		$number /= 1000;
228
	}
229
230
	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 220. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
231
}
232
233
/**
234
 * Formats a number to a computer byte size value xB, xKB, xMB, xGB
235
 *
236
 * @param int $number
237
 *
238
 * @return string
239
 */
240
function byte_format($number)
241
{
242
	global $txt;
243
244 18
	$kb = '';
245
	foreach (array('byte', 'kilobyte', 'megabyte', 'gigabyte') as $kb)
246
	{
247 18
		if ($number < 1024)
248
		{
249
			break;
250
		}
251
252
		$number /= 1024;
253 18
	}
254
255
	return comma_format($number) . ' ' . $txt[$kb];
256
}
257 18
258
/**
259
 * Format a time to make it look purdy.
260
 *
261
 * What it does:
262
 *
263 18
 * - Returns a pretty formatted version of time based on the user's format in User::$info->time_format.
264
 * - Applies all necessary time offsets to the timestamp, unless offset_type is set.
265
 * - If todayMod is set and show_today was not not specified or true, an
266
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
267
 * - Performs localization (more than just strftime would do alone.)
268
 *
269
 * @param int $log_time A unix timestamp
270
 * @param string|bool $show_today = true show "Today"/"Yesterday",
271
 *   false shows the date, a string can force a date format to use %b %d, %Y
272
 * @param string|bool $offset_type = false If false, uses both user time offset and forum offset.
273
 *   If 'forum', uses only the forum offset. Otherwise no offset is applied.
274
 *
275
 * @return string
276
 */
277
function standardTime($log_time, $show_today = true, $offset_type = false)
278
{
279
	global $txt, $modSettings;
280
	static $non_twelve_hour, $is_win = null;
281
282 18
	if ($is_win === null)
283
	{
284 18
		$is_win = detectServer()->is('windows');
285
	}
286
287
	// Offset the time.
288
	if (!$offset_type)
289
	{
290
		$time = $log_time + (User::$info->time_offset + $modSettings['time_offset']) * 3600;
291
	}
292 18
	// Just the forum offset?
293
	elseif ($offset_type === 'forum')
294 18
	{
295
		$time = $log_time + $modSettings['time_offset'] * 3600;
296
	}
297
	else
298
	{
299
		$time = $log_time;
300
	}
301
302 18
	// We can't have a negative date (on Windows, at least.)
303 18
	if ($log_time < 0)
304
	{
305 18
		$log_time = 0;
306
	}
307
308
	// Today and Yesterday?
309
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
310
	{
311
		// Get the current time.
312
		$nowtime = forum_time();
313 18
314
		$then = @getdate($time);
315
		$now = @getdate($nowtime);
316
317
		// Try to make something of a time format string...
318
		$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

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

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
750
}
751
752
/**
753
 * Helper function to convert memory string settings to bytes
754
 *
755
 * @param string $val The byte string, like 256M or 1G
756 16
 *
757
 * @return int The string converted to a proper integer in bytes
758
 */
759 16
function memoryReturnBytes($val)
760
{
761 16
	if (is_integer($val))
0 ignored issues
show
introduced by
The condition is_integer($val) is always false.
Loading history...
762
	{
763
		return $val;
764
	}
765
766
	// Separate the number from the designator
767
	$val = trim($val);
768
	$num = intval(substr($val, 0, strlen($val) - 1));
769
	$last = strtolower(substr($val, -1));
770
771
	// Convert to bytes
772
	switch ($last)
773
	{
774
		// fall through select g = 1024*1024*1024
775
		case 'g':
776
			$num *= 1024;
777
		// fall through select m = 1024*1024
778
		case 'm':
779
			$num *= 1024;
780
		// fall through select k = 1024
781
		case 'k':
782
			$num *= 1024;
783
	}
784
785
	return $num;
786
}
787
788
/**
789
 * This is the only template included in the sources.
790
 */
791
function template_rawdata()
792
{
793
	return theme()->template_rawdata();
0 ignored issues
show
Bug introduced by
Are you sure the usage of theme()->template_rawdata() targeting ElkArte\Themes\DefaultTh...eme::template_rawdata() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
794
}
795
796
/**
797
 * The header template
798
 */
799
function template_header()
800
{
801
	return theme()->template_header();
0 ignored issues
show
Bug introduced by
Are you sure the usage of theme()->template_header() targeting ElkArte\Themes\DefaultTh...heme::template_header() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
802
}
803
804
/**
805
 * Show the copyright.
806
 */
807
function theme_copyright()
808
{
809
	return theme()->theme_copyright();
0 ignored issues
show
Bug introduced by
Are you sure the usage of theme()->theme_copyright() targeting ElkArte\Themes\DefaultTh...heme::theme_copyright() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
810
}
811
812
/**
813
 * The template footer
814
 */
815
function template_footer()
816
{
817
	return theme()->template_footer();
0 ignored issues
show
Bug introduced by
Are you sure the usage of theme()->template_footer() targeting ElkArte\Themes\DefaultTh...heme::template_footer() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
818
}
819
820
/**
821
 * Output the Javascript files
822
 *
823
 * What it does:
824
 *
825
 * - tabbing in this function is to make the HTML source look proper
826
 * - outputs jQuery/jQueryUI from the proper source (local/CDN)
827
 * - if deferred is set function will output all JS (source & inline) set to load at page end
828
 * - if the admin option to combine files is set, will use Combiner.class
829
 *
830
 * @param bool $do_deferred = false
831
 */
832
function template_javascript($do_deferred = false)
833
{
834
	theme()->template_javascript($do_deferred);
835
836
	return;
837
}
838
839
/**
840
 * Output the CSS files
841
 *
842
 * What it does:
843
 *  - If the admin option to combine files is set, will use Combiner.class
844
 */
845
function template_css()
846
{
847
	theme()->template_css();
848
849
	return;
850
}
851
852
/**
853
 * Calls on template_show_error from index.template.php to show warnings
854
 * and security errors for admins
855
 */
856
function template_admin_warning_above()
857
{
858
	theme()->template_admin_warning_above();
859
860
	return;
861
}
862
863
/**
864
 * Convert a single IP to a ranged IP.
865
 *
866
 * - Internal function used to convert a user-readable format to a format suitable for the database.
867
 *
868
 * @param string $fullip A full dot notation IP address
869
 *
870
 * @return array|string 'unknown' if the ip in the input was '255.255.255.255'
871
 */
872
function ip2range($fullip)
873
{
874
	// If its IPv6, validate it first.
875
	if (isValidIPv6($fullip))
876
	{
877
		$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

877
		$ip_parts = explode(':', /** @scrutinizer ignore-type */ expandIPv6($fullip, false));
Loading history...
878
		$ip_array = array();
879
880
		if (count($ip_parts) !== 8)
881
		{
882
			return array();
883
		}
884
885
		for ($i = 0; $i < 8; $i++)
886
		{
887
			if ($ip_parts[$i] === '*')
888
			{
889
				$ip_array[$i] = array('low' => '0', 'high' => hexdec('ffff'));
890
			}
891
			elseif (preg_match('/^([0-9A-Fa-f]{1,4})\-([0-9A-Fa-f]{1,4})$/', $ip_parts[$i], $range) == 1)
892
			{
893
				$ip_array[$i] = array('low' => hexdec($range[1]), 'high' => hexdec($range[2]));
894
			}
895
			elseif (is_numeric(hexdec($ip_parts[$i])))
896
			{
897
				$ip_array[$i] = array('low' => hexdec($ip_parts[$i]), 'high' => hexdec($ip_parts[$i]));
898
			}
899
		}
900
901
		return $ip_array;
902
	}
903
904
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
905
	if ($fullip === 'unknown')
906
	{
907
		$fullip = '255.255.255.255';
908
	}
909
910
	$ip_parts = explode('.', $fullip);
911
	$ip_array = array();
912
913
	if (count($ip_parts) !== 4)
914
	{
915
		return array();
916
	}
917
918
	for ($i = 0; $i < 4; $i++)
919
	{
920
		if ($ip_parts[$i] === '*')
921
		{
922
			$ip_array[$i] = array('low' => '0', 'high' => '255');
923
		}
924
		elseif (preg_match('/^(\d{1,3})\-(\d{1,3})$/', $ip_parts[$i], $range) == 1)
925
		{
926
			$ip_array[$i] = array('low' => $range[1], 'high' => $range[2]);
927
		}
928
		elseif (is_numeric($ip_parts[$i]))
929
		{
930
			$ip_array[$i] = array('low' => $ip_parts[$i], 'high' => $ip_parts[$i]);
931
		}
932 4
	}
933
934
	// Makes it simpler to work with.
935
	$ip_array[4] = array('low' => 0, 'high' => 0);
936 4
	$ip_array[5] = array('low' => 0, 'high' => 0);
937
	$ip_array[6] = array('low' => 0, 'high' => 0);
938
	$ip_array[7] = array('low' => 0, 'high' => 0);
939
940
	return $ip_array;
941
}
942 4
943
/**
944
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
945 4
 *
946
 * @param string $ip A full dot notation IP address
947 4
 *
948
 * @return string
949
 * @throws \ElkArte\Exceptions\Exception
950
 */
951
function host_from_ip($ip)
952 4
{
953
	global $modSettings;
954
955
	$cache = Cache::instance();
956 4
957
	$host = '';
958
	if ($cache->getVar($host, 'hostlookup-' . $ip, 600) || empty($ip))
959
	{
960
		return $host;
961
	}
962
963
	$t = microtime(true);
964
965
	// Try the Linux host command, perhaps?
966
	if ((stripos(PHP_OS, 'win') === false || stripos(PHP_OS, 'darwin') !== false) && mt_rand(0, 1) === 1)
967
	{
968
		if (!isset($modSettings['host_to_dis']))
969
		{
970
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
971
		}
972
		else
973
		{
974
			$test = @shell_exec('host ' . @escapeshellarg($ip));
975
		}
976
977
		// Did host say it didn't find anything?
978
		if (strpos($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 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

978
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
979 10
		{
980
			$host = '';
981
		}
982
		// Invalid server option?
983
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
984
		{
985 10
			updateSettings(array('host_to_dis' => 1));
986 10
		}
987 10
		// Maybe it found something, after all?
988
		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

988
		elseif (preg_match('~\s([^\s]+?)\.\s~', /** @scrutinizer ignore-type */ $test, $match) == 1)
Loading history...
989
		{
990 5
			$host = $match[1];
991
		}
992
	}
993 10
994 10
	// This is nslookup; usually only Windows, but possibly some Unix?
995
	if (empty($host) && stripos(PHP_OS, 'win') !== false && stripos(PHP_OS, 'darwin') === false && mt_rand(0, 1) === 1)
996 10
	{
997 10
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
998
999
		if (strpos($test, 'Non-existent domain') !== false)
1000 10
		{
1001
			$host = '';
1002
		}
1003 10
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
1004
		{
1005
			$host = $match[1];
1006
		}
1007
	}
1008
1009
	// This is the last try :/.
1010
	if (!isset($host) || $host === false)
1011
	{
1012
		$host = @gethostbyaddr($ip);
1013
	}
1014
1015
	// It took a long time, so let's cache it!
1016
	if (microtime(true) - $t > 0.5)
1017
	{
1018
		$cache->put('hostlookup-' . $ip, $host, 600);
1019
	}
1020
1021
	return $host;
1022
}
1023
1024
/**
1025
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
1026
 *
1027
 * @param string $text The string to process
1028
 *     - if encrypt = true this is the maximum number of bytes to use in integer hashes (for searching)
1029
 *     - if encrypt = false this is the maximum number of letters in each word
1030
 * @param bool $encrypt = false Used for custom search indexes to return an int[] array representing the words
1031
 *
1032
 * @return array
1033
 */
1034
function text2words($text, $encrypt = false)
1035
{
1036
	// Step 0: prepare numbers so they are good for search & index 1000.45 -> 1000_45
1037
	$words = preg_replace('~([\d]+)[.-/]+(?=[\d])~u', '$1_', $text);
1038
1039
	// Step 1: Remove entities/things we don't consider words:
1040
	$words = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', strtr($words, array('<br />' => ' ')));
1041
1042
	// Step 2: Entities we left to letters, where applicable, lowercase.
1043
	$words = un_htmlspecialchars(ElkArte\Util::strtolower($words));
1044
1045
	// Step 3: Ready to split apart and index!
1046
	$words = explode(' ', $words);
1047
1048
	if ($encrypt)
1049
	{
1050
		$blocklist = getBlocklist();
1051
		$returned_ints = [];
1052
1053
		// Only index unique words
1054
		$words = array_unique($words);
1055
		foreach ($words as $word)
1056
		{
1057
			$word = trim($word, '-_\'');
1058
			if ($word !== '' && !in_array($word, $blocklist) && Util::strlen($word) > 2)
1059
			{
1060
				// Get a hex representation of this word using a database indexing hash
1061
				// designed to be fast while maintaining a very low collision rate
1062
				$encrypted = hash('FNV1A32', $word);
1063
1064
				// Create an integer representation, the hash is an 8 char hex
1065
				// so the largest int will be 4294967295 which fits in db int(10)
1066
				$returned_ints[$word] = hexdec($encrypted);
1067
			}
1068
		}
1069
1070
		return $returned_ints;
1071
	}
1072
	else
1073
	{
1074
		// Trim characters before and after and add slashes for database insertion.
1075
		$returned_words = array();
1076
		foreach ($words as $word)
1077
		{
1078
			if (($word = trim($word, '-_\'')) !== '')
1079
			{
1080
				$returned_words[] = substr($word, 0, 20);
1081
			}
1082
		}
1083
1084
		// Filter out all words that occur more than once.
1085
		return array_unique($returned_words);
1086
	}
1087
}
1088
1089
/**
1090
 * Get the block list from the search controller.
1091
 *
1092
 * @return array
1093
 */
1094
function getBlocklist()
1095
{
1096
	static $blocklist;
1097
1098
	if (!isset($blocklist))
1099
	{
1100
		$search = new Search();
1101
		$blocklist = $search->getBlockListedWords();
1102
		unset($search);
1103
	}
1104
1105
	return $blocklist;
1106
}
1107
1108
/**
1109
 * Sets up all of the top menu buttons
1110
 *
1111
 * What it does:
1112
 *
1113
 * - Defines every master item in the menu, as well as any sub-items
1114
 * - Ensures the chosen action is set so the menu is highlighted
1115
 * - Saves them in the cache if it is available and on
1116
 * - Places the results in $context
1117
 */
1118
function setupMenuContext()
1119
{
1120
	return theme()->setupMenuContext();
0 ignored issues
show
Bug introduced by
Are you sure the usage of theme()->setupMenuContext() targeting ElkArte\Themes\DefaultTh...eme::setupMenuContext() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
1121
}
1122
1123
/**
1124
 * Process functions of an integration hook.
1125
 *
1126
 * What it does:
1127
 *
1128
 * - Calls all functions of the given hook.
1129
 * - Supports static class method calls.
1130
 *
1131
 * @param string $hook The name of the hook to call
1132
 * @param mixed[] $parameters = array() Parameters to pass to the hook
1133
 *
1134
 * @return mixed[] the results of the functions
1135
 */
1136
function call_integration_hook($hook, $parameters = array())
1137
{
1138
	return Hooks::instance()->hook($hook, $parameters);
1139
}
1140
1141
/**
1142
 * Includes files for hooks that only do that (i.e. integrate_pre_include)
1143
 *
1144
 * @param string $hook The name to include
1145
 */
1146
function call_integration_include_hook($hook)
1147
{
1148
	Hooks::instance()->include_hook($hook);
1149
}
1150
1151
/**
1152
 * Special hook call executed during obExit
1153
 */
1154
function call_integration_buffer()
1155
{
1156
	Hooks::instance()->buffer_hook();
1157
}
1158
1159
/**
1160
 * Add a function for integration hook.
1161
 *
1162
 * - Does nothing if the function is already added.
1163
 *
1164
 * @param string $hook The name of the hook to add
1165
 * @param string $function The function associated with the hook
1166
 * @param string $file The file that contains the function
1167
 * @param bool $permanent = true if true, updates the value in settings table
1168
 */
1169
function add_integration_function($hook, $function, $file = '', $permanent = true)
1170
{
1171
	Hooks::instance()->add($hook, $function, $file, $permanent);
1172
}
1173
1174
/**
1175
 * Remove an integration hook function.
1176
 *
1177
 * What it does:
1178
 *
1179
 * - Removes the given function from the given hook.
1180
 * - Does nothing if the function is not available.
1181
 *
1182
 * @param string $hook The name of the hook to remove
1183
 * @param string $function The name of the function
1184
 * @param string $file The file its located in
1185
 */
1186
function remove_integration_function($hook, $function, $file = '')
1187
{
1188
	Hooks::instance()->remove($hook, $function, $file);
1189
}
1190
1191
/**
1192
 * Decode numeric html entities to their UTF8 equivalent character.
1193
 *
1194
 * What it does:
1195
 *
1196
 * - Callback function for preg_replace_callback in subs-members
1197
 * - Uses capture group 2 in the supplied array
1198
 * - Does basic scan to ensure characters are inside a valid range
1199
 *
1200
 * @param mixed[] $matches matches from a preg_match_all
1201
 *
1202
 * @return string $string
1203
 */
1204
function replaceEntities__callback($matches)
1205
{
1206
	if (!isset($matches[2]))
1207
	{
1208
		return '';
1209
	}
1210
1211
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
1212
1213
	// remove left to right / right to left overrides
1214
	if ($num === 0x202D || $num === 0x202E)
1215
	{
1216
		return '';
1217
	}
1218
1219
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
1220
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
1221
	{
1222
		return '&#' . $num . ';';
1223
	}
1224
1225
	// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
1226
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
1227
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
1228
	{
1229
		return '';
1230
	}
1231
1232
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1233
	if ($num < 0x80)
1234
	{
1235
		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

1235
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
1236
	}
1237
1238
	// <0x800 (2048)
1239
	if ($num < 0x800)
1240
	{
1241
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1242
	}
1243
1244
	// < 0x10000 (65536)
1245
	if ($num < 0x10000)
1246
	{
1247
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1248
	}
1249
1250
	// <= 0x10FFFF (1114111)
1251
	return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1252
}
1253
1254
/**
1255
 * Converts html entities to utf8 equivalents
1256 26
 *
1257
 * What it does:
1258
 *
1259 26
 * - Callback function for preg_replace_callback
1260
 * - Uses capture group 1 in the supplied array
1261
 * - Does basic checks to keep characters inside a viewable range.
1262 26
 *
1263
 * @param mixed[] $matches array of matches as output from preg_match_all
1264 26
 *
1265
 * @return string $string
1266
 */
1267
function fixchar__callback($matches)
1268
{
1269
	if (!isset($matches[1]))
1270
	{
1271
		return '';
1272
	}
1273
1274
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
1275
1276
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
1277
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
1278
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
1279
	{
1280
		return '';
1281
	}
1282
1283
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1284
	if ($num < 0x80)
1285
	{
1286
		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

1286
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
1287
	}
1288
1289
	// <0x800 (2048)
1290
	if ($num < 0x800)
1291
	{
1292
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1293 26
	}
1294 26
1295
	// < 0x10000 (65536)
1296 26
	if ($num < 0x10000)
1297
	{
1298 26
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1299
	}
1300
1301
	// <= 0x10FFFF (1114111)
1302
	return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1303 26
}
1304
1305
/**
1306
 * Strips out invalid html entities, replaces others with html style &#123; codes
1307
 *
1308
 * What it does:
1309
 *
1310
 * - Callback function used of preg_replace_callback in various $ent_checks,
1311
 * - For example strpos, strlen, substr etc
1312
 *
1313
 * @param mixed[] $matches array of matches for a preg_match_all
1314
 *
1315
 * @return string
1316
 */
1317
function entity_fix__callback($matches)
1318
{
1319
	if (!isset($matches[2]))
1320
	{
1321
		return '';
1322
	}
1323
1324
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
1325
1326
	// We don't allow control characters, characters out of range, byte markers, etc
1327
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
1328
	{
1329
		return '';
1330
	}
1331
1332
	return '&#' . $num . ';';
1333
}
1334
1335
/**
1336
 * Retrieve additional search engines, if there are any, as an array.
1337 299
 *
1338
 * @return mixed[] array of engines
1339
 */
1340
function prepareSearchEngines()
1341
{
1342
	global $modSettings;
1343
1344
	$engines = array();
1345
	if (!empty($modSettings['additional_search_engines']))
1346
	{
1347 231
		$search_engines = ElkArte\Util::unserialize($modSettings['additional_search_engines']);
1348 231
		foreach ($search_engines as $engine)
1349
		{
1350
			$engines[strtolower(preg_replace('~[^A-Za-z0-9 ]~', '', $engine['name']))] = $engine;
1351
		}
1352
	}
1353
1354
	return $engines;
1355
}
1356
1357
/**
1358
 * This function receives a request handle and attempts to retrieve the next result.
1359
 *
1360
 * What it does:
1361
 *
1362
 * - It is used by the controller callbacks from the template, such as
1363
 * posts in topic display page, posts search results page, or personal messages.
1364
 *
1365
 * @param resource $messages_request holds a query result
1366
 * @param bool $reset
1367
 *
1368
 * @return int|bool
1369
 * @throws \Exception
1370 4
 */
1371 4
function currentContext($messages_request, $reset = false)
1372
{
1373
	// Start from the beginning...
1374
	if ($reset)
1375
	{
1376
		return $messages_request->data_seek(0);
1377
	}
1378
1379
	// If the query has already returned false, get out of here
1380
	if ($messages_request->hasResults())
1381
	{
1382
		return false;
1383
	}
1384
1385
	// Attempt to get the next message.
1386
	$message = $messages_request->fetch_assoc();
1387 2
	if (!$message)
1388 2
	{
1389
		$messages_request->free_result();
1390
1391
		return false;
1392
	}
1393
1394
	return $message;
1395
}
1396
1397
/**
1398
 * Helper function to insert an array in to an existing array
1399
 *
1400
 * What it does:
1401
 *
1402
 * - Intended for addon use to allow such things as
1403
 * - Adding in a new menu item to an existing menu array
1404
 *
1405
 * @param mixed[] $input the array we will insert to
1406
 * @param string $key the key in the array that we are looking to find for the insert action
1407
 * @param mixed[] $insert the actual data to insert before or after the key
1408
 * @param string $where adding before or after
1409
 * @param bool $assoc if the array is a assoc array with named keys or a basic index array
1410
 * @param bool $strict search for identical elements, this means it will also check the types of the needle.
1411
 *
1412
 * @return array|mixed[]
1413
 */
1414
function elk_array_insert($input, $key, $insert, $where = 'before', $assoc = true, $strict = false)
1415
{
1416
	$position = $assoc ? array_search($key, array_keys($input), $strict) : array_search($key, $input, $strict);
1417
1418
	// If the key is not found, just insert it at the end
1419
	if ($position === false)
0 ignored issues
show
introduced by
The condition $position === false is always false.
Loading history...
1420
	{
1421
		return array_merge($input, $insert);
1422
	}
1423
1424
	if ($where === 'after')
1425
	{
1426
		$position++;
1427
	}
1428
1429
	// Insert as first
1430
	if (empty($position))
1431
	{
1432
		$input = array_merge($insert, $input);
1433
	}
1434
	else
1435
	{
1436
		$input = 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

1436
		$input = 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

1436
		$input = array_merge(array_slice($input, 0, /** @scrutinizer ignore-type */ $position), $insert, array_slice($input, $position));
Loading history...
1437
	}
1438
1439
	return $input;
1440
}
1441
1442
/**
1443
 * Run a scheduled task now
1444
 *
1445
 * What it does:
1446
 *
1447
 * - From time to time it may be necessary to fire a scheduled task ASAP
1448
 * - This function sets the scheduled task to be called before any other one
1449
 *
1450
 * @param string $task the name of a scheduled task
1451
 * @throws \ElkArte\Exceptions\Exception
1452
 */
1453
function scheduleTaskImmediate($task)
1454
{
1455
	global $modSettings;
1456
1457
	if (!isset($modSettings['scheduleTaskImmediate']))
1458
	{
1459
		$scheduleTaskImmediate = array();
1460
	}
1461
	else
1462
	{
1463
		$scheduleTaskImmediate = Util::unserialize($modSettings['scheduleTaskImmediate']);
1464
	}
1465
1466
	// If it has not been scheduled, the do so now
1467
	if (!isset($scheduleTaskImmediate[$task]))
1468
	{
1469
		$scheduleTaskImmediate[$task] = 0;
1470
		updateSettings(array('scheduleTaskImmediate' => serialize($scheduleTaskImmediate)));
1471
1472
		require_once(SUBSDIR . '/ScheduledTasks.subs.php');
1473
1474
		// Ensure the task is on
1475
		toggleTaskStatusByName($task, true);
1476
1477
		// Before trying to run it **NOW** :P
1478
		calculateNextTrigger($task, true);
1479
	}
1480
}
1481
1482
/**
1483
 * For diligent people: remove scheduleTaskImmediate when done, otherwise
1484
 * a maximum of 10 executions is allowed
1485
 *
1486
 * @param string $task the name of a scheduled task
1487
 * @param bool $calculateNextTrigger if recalculate the next task to execute
1488
 * @throws \ElkArte\Exceptions\Exception
1489
 */
1490
function removeScheduleTaskImmediate($task, $calculateNextTrigger = true)
1491
{
1492
	global $modSettings;
1493
1494
	// Not on, bail
1495
	if (!isset($modSettings['scheduleTaskImmediate']))
1496
	{
1497
		return;
1498
	}
1499
	else
1500
	{
1501
		$scheduleTaskImmediate = Util::unserialize($modSettings['scheduleTaskImmediate']);
1502
	}
1503
1504
	// Clear / remove the task if it was set
1505
	if (isset($scheduleTaskImmediate[$task]))
1506
	{
1507
		unset($scheduleTaskImmediate[$task]);
1508
		updateSettings(array('scheduleTaskImmediate' => serialize($scheduleTaskImmediate)));
1509
1510
		// Recalculate the next task to execute
1511
		if ($calculateNextTrigger)
1512
		{
1513
			require_once(SUBSDIR . '/ScheduledTasks.subs.php');
1514
			calculateNextTrigger($task);
1515
		}
1516
	}
1517
}
1518 8
1519
/**
1520
 * Helper function to replace commonly used urls in text strings
1521
 *
1522
 * @event integrate_basic_url_replacement add additional place holder replacements
1523 8
 * @param string $string the string to inject URLs into
1524
 *
1525
 * @return string the input string with the place-holders replaced with
1526 8
 *           the correct URLs
1527
 */
1528 4
function replaceBasicActionUrl($string)
1529
{
1530
	global $scripturl, $context, $boardurl;
1531 4
	static $find_replace = null;
1532
1533
	if ($find_replace === null)
1534
	{
1535
		$find_replace = array(
1536
			'{forum_name}' => $context['forum_name'],
1537
			'{forum_name_html_safe}' => $context['forum_name_html_safe'],
1538
			'{forum_name_html_unsafe}' => un_htmlspecialchars($context['forum_name_html_safe']),
1539
			'{script_url}' => $scripturl,
1540
			'{board_url}' => $boardurl,
1541
			'{login_url}' => getUrl('action', ['action' => 'login']),
1542
			'{register_url}' => getUrl('action', ['action' => 'register']),
1543
			'{activate_url}' => getUrl('action', ['action' => 'register', 'sa' => 'activate']),
1544
			'{help_url}' => getUrl('action', ['action' => 'help']),
1545
			'{admin_url}' => getUrl('admin', ['action' => 'admin']),
1546
			'{moderate_url}' => getUrl('moderate', ['action' => 'moderate']),
1547
			'{recent_url}' => getUrl('action', ['action' => 'recent']),
1548
			'{search_url}' => getUrl('action', ['action' => 'search']),
1549
			'{who_url}' => getUrl('action', ['action' => 'who']),
1550
			'{credits_url}' => getUrl('action', ['action' => 'who', 'sa' => 'credits']),
1551
			'{calendar_url}' => getUrl('action', ['action' => 'calendar']),
1552
			'{memberlist_url}' => getUrl('action', ['action' => 'memberlist']),
1553
			'{stats_url}' => getUrl('action', ['action' => 'stats']),
1554
		);
1555
		call_integration_hook('integrate_basic_url_replacement', array(&$find_replace));
1556
	}
1557
1558
	return str_replace(array_keys($find_replace), array_values($find_replace), $string);
1559
}
1560
1561
/**
1562
 * This function creates a new GenericList from all the passed options.
1563
 *
1564
 * What it does:
1565
 *
1566
 * - Calls integration hook integrate_list_"unique_list_id" to allow easy modifying
1567
 *
1568
 * @event integrate_list_$listID called before every createlist to allow access to its listoptions
1569
 * @param mixed[] $listOptions associative array of option => value
1570
 */
1571
function createList($listOptions)
1572
{
1573
	call_integration_hook('integrate_list_' . $listOptions['id'], array(&$listOptions));
1574
1575
	$list = new GenericList($listOptions);
1576
1577
	$list->buildList();
1578
}
1579
1580
/**
1581
 * This handy function retrieves a Request instance and passes it on.
1582
 *
1583
 * What it does:
1584
 *
1585
 * - To get hold of a Request, you can use this function or directly Request::instance().
1586
 * - This is for convenience, it simply delegates to Request::instance().
1587
 */
1588
function request()
1589
{
1590
	return ElkArte\Request::instance();
1591
}
1592
1593
/**
1594
 * Meant to replace any usage of $db_last_error.
1595
 *
1596
 * What it does:
1597
 *
1598
 * - Reads the file db_last_error.txt, if a time() is present returns it,
1599
 * otherwise returns 0.
1600
 */
1601
function db_last_error()
1602
{
1603
	$time = trim(file_get_contents(BOARDDIR . '/db_last_error.txt'));
1604
1605
	if (preg_match('~^\d{10}$~', $time) === 1)
1606
	{
1607
		return $time;
1608
	}
1609
1610
	return 0;
1611
}
1612
1613
/**
1614
 * This function has the only task to retrieve the correct prefix to be used
1615
 * in responses.
1616
 *
1617
 * @return string - The prefix in the default language of the forum
1618
 */
1619
function response_prefix()
1620
{
1621
	global $language, $txt;
1622
	static $response_prefix = null;
1623
1624
	$cache = Cache::instance();
1625
1626
	// Get a response prefix, but in the forum's default language.
1627
	if ($response_prefix === null && (!$cache->getVar($response_prefix, 'response_prefix') || !$response_prefix))
1628
	{
1629
		if ($language === User::$info->language)
1630
		{
1631
			$response_prefix = $txt['response_prefix'];
1632
		}
1633
		else
1634
		{
1635
			\ElkArte\Themes\ThemeLoader::loadLanguageFile('index', $language, false);
1636
			$response_prefix = $txt['response_prefix'];
1637
			\ElkArte\Themes\ThemeLoader::loadLanguageFile('index');
1638
		}
1639
1640
		$cache->put('response_prefix', $response_prefix, 600);
1641
	}
1642
1643
	return $response_prefix;
1644
}
1645
1646
/**
1647
 * A very simple function to determine if an email address is "valid" for Elkarte.
1648
 *
1649
 * - A valid email for ElkArte is something that resembles an email (filter_var) and
1650
 * is less than 255 characters (for database limits)
1651
 *
1652
 * @param string $value - The string to evaluate as valid email
1653
 *
1654
 * @return string|false - The email if valid, false if not a valid email
1655
 */
1656
function isValidEmail($value)
1657
{
1658
	$value = trim($value);
1659
	if (filter_var($value, FILTER_VALIDATE_EMAIL) && ElkArte\Util::strlen($value) < 255)
1660
	{
1661
		return $value;
1662
	}
1663
1664
	return false;
1665
}
1666
1667
/**
1668
 * Adds a protocol (http/s, ftp/mailto) to the beginning of an url if missing
1669
 *
1670
 * @param string $url - The url
1671
 * @param string[] $protocols - A list of protocols to check, the first is
1672
 *                 added if none is found (optional, default array('http://', 'https://'))
1673
 *
1674
 * @return string - The url with the protocol
1675
 */
1676
function addProtocol($url, $protocols = array())
1677
{
1678
	if (empty($protocols))
1679
	{
1680
		$pattern = '~^(http://|https://)~i';
1681
		$protocols = array('http://');
1682
	}
1683
	else
1684
	{
1685
		$pattern = '~^(' . implode('|', array_map(function ($val) {
1686
				return preg_quote($val, '~');
1687
			}, $protocols)) . ')~i';
1688
	}
1689
1690
	$found = false;
1691
	$url = preg_replace_callback($pattern, function ($match) use (&$found) {
1692
		$found = true;
1693
1694
		return strtolower($match[0]);
1695
	}, $url);
1696
1697
	if ($found)
1698
	{
1699
		return $url;
1700
	}
1701
1702
	return $protocols[0] . $url;
1703
}
1704
1705
/**
1706
 * Removes nested quotes from a text string.
1707
 *
1708
 * @param string $text - The body we want to remove nested quotes from
1709
 *
1710
 * @return string - The same body, just without nested quotes
1711
 */
1712
function removeNestedQuotes($text)
1713
{
1714
	global $modSettings;
1715
1716
	// Remove any nested quotes, if necessary.
1717
	if (!empty($modSettings['removeNestedQuotes']))
1718
	{
1719
		return preg_replace(array('~\n?\[quote.*?\].+?\[/quote\]\n?~is', '~^\n~', '~\[/quote\]~'), '', $text);
1720
	}
1721
1722
	return $text;
1723
}
1724
1725
/**
1726
 * Change a \t to a span that will show a tab
1727
 *
1728
 * @param string $string
1729 5
 *
1730 5
 * @return string
1731
 */
1732 5
function tabToHtmlTab($string)
1733
{
1734
	return str_replace("\t", "<span class=\"tab\">\t</span>", $string);
1735 3
}
1736 3
1737 3
/**
1738 3
 * Remove <br />
1739 3
 *
1740 3
 * @param string $string
1741 3
 *
1742 3
 * @return string
1743 3
 */
1744 3
function removeBr($string)
1745 3
{
1746 3
	return str_replace('<br />', '', $string);
1747 3
}
1748 3
1749 3
/**
1750 3
 * Are we using this browser?
1751 3
 *
1752 3
 * - Wrapper function for detectBrowser
1753
 *
1754 3
 * @param string $browser the browser we are checking for.
1755
 *
1756
 * @return bool
1757 5
 */
1758
function isBrowser($browser)
1759
{
1760
	global $context;
1761
1762
	// Don't know any browser!
1763
	if (empty($context['browser']))
1764
	{
1765
		detectBrowser();
1766
	}
1767
1768
	return !empty($context['browser'][$browser]) || !empty($context['browser']['is_' . $browser]);
1769
}
1770
1771
/**
1772 10
 * Replace all vulgar words with respective proper words. (substring or whole words..)
1773
 *
1774 10
 * What it does:
1775
 * - it censors the passed string.
1776 10
 * - if the admin setting allow_no_censored is on it does not censor unless force is also set.
1777 10
 * - if the admin setting allow_no_censored is off will censor words unless the user has set
1778
 * it to not censor in their profile and force is off
1779
 * - it caches the list of censored words to reduce parsing.
1780
 * - Returns the censored text
1781
 *
1782
 * @param string $text
1783
 * @param bool $force = false
1784
 *
1785
 * @return string
1786
 */
1787
function censor($text, $force = false)
1788
{
1789 251
	global $modSettings;
1790
	static $censor = null;
1791
1792
	if ($censor === null)
1793
	{
1794
		$censor = new Censor(explode("\n", $modSettings['censor_vulgar']), explode("\n", $modSettings['censor_proper']), $modSettings);
1795
	}
1796
1797
	return $censor->censor($text, $force);
1798
}
1799
1800
/**
1801
 * Helper function able to determine if the current member can see at least
1802
 * one button of a button strip.
1803
 *
1804
 * @param mixed[] $button_strip
1805
 *
1806
 * @return bool
1807
 */
1808
function can_see_button_strip($button_strip)
1809
{
1810
	global $context;
1811
1812
	foreach ($button_strip as $key => $value)
1813
	{
1814
		if (!isset($value['test']) || !empty($context[$value['test']]))
1815
		{
1816
			return true;
1817
		}
1818
	}
1819
1820 4
	return false;
1821 4
}
1822
1823 4
/**
1824
 * @return \ElkArte\Themes\DefaultTheme\Theme
1825
 */
1826 4
function theme()
1827
{
1828 2
	return $GLOBALS['context']['theme_instance'];
1829
}
1830 2
1831
/**
1832
 * Stops the execution with a 1x1 gif file
1833
 *
1834
 * @param bool $expired Sends an expired header.
1835
 */
1836
function dieGif($expired = false)
1837
{
1838
	// The following is an attempt at stopping the behavior identified in #2391
1839 2
	if (function_exists('fastcgi_finish_request'))
1840
	{
1841
		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...
1842 4
	}
1843
1844
	$headers = Headers::instance();
1845
	if ($expired)
1846
	{
1847
		$headers
1848
			->header('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')
1849
			->header('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
1850
	}
1851
1852
	$headers->contentType('image/gif')->sendHeaders();
1853
	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...
1854
}
1855
1856
/**
1857 2
 * Prepare ob_start with or without gzip compression
1858 2
 *
1859
 * @param bool $use_compression Starts compressed headers.
1860 2
 */
1861
function obStart($use_compression = false)
1862
{
1863
	// This is done to clear any output that was made before now.
1864
	while (ob_get_level() > 0)
1865
	{
1866
		@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

1866
		/** @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...
1867
	}
1868
1869
	if ($use_compression)
1870
	{
1871
		ob_start('ob_gzhandler');
1872
	}
1873
	else
1874
	{
1875
		ob_start();
1876
		Headers::instance()->header('Content-Encoding', 'none');
1877 14
	}
1878
}
1879 14
1880 14
/**
1881
 * Returns an URL based on the parameters passed and the selected generator
1882
 *
1883
 * @param string $type The type of the URL (depending on the type, the
1884
 *                     generator can act differently
1885
 * @param mixed[] $params All the parameters of the URL
1886
 *
1887
 * @return string An URL
1888
 */
1889 14
function getUrl($type, $params)
1890
{
1891 14
	static $generator = null;
1892
1893 14
	if ($generator === null)
1894 14
	{
1895
		$generator = initUrlGenerator();
1896 14
	}
1897
1898 14
	return $generator->get($type, $params);
1899
}
1900
1901 2
/**
1902
 * Returns the query part of an URL based on the parameters passed and the selected generator
1903
 *
1904
 * @param string $type The type of the URL (depending on the type, the
1905
 *                     generator can act differently
1906
 * @param mixed[] $params All the parameters of the URL
1907
 *
1908
 * @return string The query part of an URL
1909
 */
1910
function getUrlQuery($type, $params)
1911
{
1912
	static $generator = null;
1913
1914
	if ($generator === null)
1915
	{
1916
		$generator = initUrlGenerator();
1917
	}
1918
1919
	return $generator->getQuery($type, $params);
1920
}
1921
1922
/**
1923
 * Initialize the URL generator
1924
 *
1925
 * @return object The URL generator object
1926
 */
1927
function initUrlGenerator()
1928
{
1929
	global $scripturl, $context, $url_format;
1930
1931
	$generator = new UrlGenerator([
1932
		'generator' => ucfirst($url_format ?? 'standard'),
1933 4
		'scripturl' => $scripturl,
1934
		'replacements' => [
1935
			'{session_data}' => isset($context['session_var']) ? $context['session_var'] . '=' . $context['session_id'] : ''
1936
		]
1937
	]);
1938
1939
	$generator->register('Topic');
1940
	$generator->register('Board');
1941
	$generator->register('Profile');
1942
1943
	return $generator;
1944
}
1945
1946
/**
1947
 * This function only checks if a certain feature (in core features)
1948
 * is enabled or not.
1949
 *
1950
 * @param string $feature The abbreviated code of a core feature
1951
 * @return bool true/false for enabled/disabled
1952
 */
1953
function featureEnabled($feature)
1954
{
1955
	global $modSettings, $context;
1956
	static $features = null;
1957
1958
	if ($features === null)
1959 229
	{
1960
		// This allows us to change the way things look for the admin.
1961
		$features = explode(',', isset($modSettings['admin_features']) ?
1962 229
			$modSettings['admin_features'] : 'cd,cp,k,w,rg,ml,pm');
1963
1964
		// @deprecated since 2.0 - Next line is just for backward compatibility to remove before release
1965
		$context['admin_features'] = $features;
1966
	}
1967 229
1968
	return in_array($feature, $features);
1969
}
1970
1971
/**
1972
 * Clean up the XML to make sure it doesn't contain invalid characters.
1973
 *
1974
 * What it does:
1975
 *
1976
 * - Removes invalid XML characters to assure the input string being
1977
 * parsed properly.
1978
 *
1979
 * @param string $string The string to clean
1980
 *
1981
 * @return string The clean string
1982
 */
1983
function cleanXml($string)
1984
{
1985
	// http://www.w3.org/TR/2000/REC-xml-20001006#NT-Char
1986
	return preg_replace('~[\x00-\x08\x0B\x0C\x0E-\x19\x{FFFE}\x{FFFF}]~u', '', $string);
1987
}
1988 24
1989 24
/**
1990
 * Validates a IPv6 address. returns true if it is ipv6.
1991 24
 *
1992
 * @param string $ip ip address to be validated
1993 2
 *
1994
 * @return bool true|false
1995
 */
1996 24
function isValidIPv6($ip)
1997
{
1998
	return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
1999
}
2000
2001
/**
2002
 * Converts IPv6s to numbers.  This makes ban checks much easier.
2003
 *
2004
 * @param string $ip ip address to be converted
2005
 *
2006
 * @return int[] array
2007
 */
2008
function convertIPv6toInts($ip)
2009
{
2010
	static $expanded = array();
2011
2012
	// Check if we have done this already.
2013
	if (isset($expanded[$ip]))
2014
	{
2015
		return $expanded[$ip];
2016
	}
2017
2018
	// Expand the IP out.
2019
	$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

2019
	$expanded_ip = explode(':', /** @scrutinizer ignore-type */ expandIPv6($ip));
Loading history...
2020
2021
	$new_ip = array();
2022
	foreach ($expanded_ip as $int)
2023
	{
2024
		$new_ip[] = hexdec($int);
2025
	}
2026
2027 337
	// Save this in case of repeated use.
2028
	$expanded[$ip] = $new_ip;
2029
2030
	return $expanded[$ip];
2031
}
2032
2033
/**
2034
 * Expands a IPv6 address to its full form.
2035
 *
2036
 * @param string $addr ipv6 address string
2037
 * @param bool $strict_check checks length to expanded address for compliance
2038
 *
2039
 * @return bool|string expanded ipv6 address.
2040
 */
2041
function expandIPv6($addr, $strict_check = true)
2042
{
2043
	static $converted = array();
2044
2045
	// Check if we have done this already.
2046
	if (isset($converted[$addr]))
2047
	{
2048
		return $converted[$addr];
2049
	}
2050
2051
	// Check if there are segments missing, insert if necessary.
2052
	if (strpos($addr, '::') !== false)
2053
	{
2054
		$part = explode('::', $addr);
2055
		$part[0] = explode(':', $part[0]);
2056
		$part[1] = explode(':', $part[1]);
2057
		$missing = array();
2058
2059
		// Looks like this is an IPv4 address
2060
		if (isset($part[1][1]) && strpos($part[1][1], '.') !== false)
2061
		{
2062
			$ipoct = explode('.', $part[1][1]);
2063
			$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

2063
			$p1 = dechex(/** @scrutinizer ignore-type */ $ipoct[0]) . dechex($ipoct[1]);
Loading history...
2064
			$p2 = dechex($ipoct[2]) . dechex($ipoct[3]);
2065
2066
			$part[1] = array(
2067
				$part[1][0],
2068
				$p1,
2069
				$p2
2070
			);
2071
		}
2072
2073
		$limit = count($part[0]) + count($part[1]);
2074
		for ($i = 0; $i < (8 - $limit); $i++)
2075
		{
2076
			array_push($missing, '0000');
2077
		}
2078
2079
		$part = array_merge($part[0], $missing, $part[1]);
2080
	}
2081
	else
2082
	{
2083
		$part = explode(':', $addr);
2084
	}
2085
2086
	// Pad each segment until it has 4 digits.
2087
	foreach ($part as &$p)
2088 41
	{
2089
		while (strlen($p) < 4)
2090 41
		{
2091
			$p = '0' . $p;
2092 1
		}
2093
	}
2094
2095 41
	unset($p);
2096
2097
	// Join segments.
2098
	$result = implode(':', $part);
2099
2100
	// Save this in case of repeated use.
2101
	$converted[$addr] = $result;
2102
2103
	// Quick check to make sure the length is as expected.
2104
	if (!$strict_check || strlen($result) == 39)
2105
	{
2106
		return $result;
2107
	}
2108
2109
	return false;
2110
}
2111