Issues (1686)

sources/Subs.php (1 issue)

1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to, well, everything.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
use ElkArte\Cache\Cache;
18
use ElkArte\Debug;
19
use ElkArte\Helper\Censor;
20
use ElkArte\Helper\ConstructPageIndex;
21
use ElkArte\Helper\GenericList;
22
use ElkArte\Helper\TokenHash;
23
use ElkArte\Helper\Util;
24
use ElkArte\Hooks;
25
use ElkArte\Http\Headers;
26
use ElkArte\Languages\Loader;
27
use ElkArte\Notifications\Notifications;
28
use ElkArte\Request;
29
use ElkArte\Search\Search;
30
use ElkArte\UrlGenerator\UrlGenerator;
31
use ElkArte\User;
32
33
/**
34
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
35
 *
36
 * What it does:
37
 *
38
 * - Updates both the settings table and $modSettings array.
39
 * - All of changeArray's indexes and values are assumed to have escaped apostrophes (')!
40
 * - If a variable is already set to what you want to change it to, that
41
 *   Variable will be skipped over; it would be unnecessary to reset.
42
 * - When update is true, UPDATEs will be used instead of REPLACE.
43
 * - When update is true, the value can be true or false to increment
44
 *  or decrement it, respectively.
45
 *
46 47
 * @param array $changeArray An associative array of what we're changing in 'setting' => 'value' format
47
 * @param bool $update Use an UPDATE query instead of a REPLACE query
48 47
 */
49 47
function updateSettings($changeArray, $update = false)
50
{
51 47
	global $modSettings;
52
53
	$db = database();
54
	$cache = Cache::instance();
55
56
	if (empty($changeArray) || !is_array($changeArray))
57 47
	{
58
		return;
59 28
	}
60
61 28
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
62
	if ($update)
63 28
	{
64
		foreach ($changeArray as $variable => $value)
65
		{
66 28
			$db->query('', '
67 28
				UPDATE {db_prefix}settings
68
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
69
				WHERE variable = {string:variable}',
70
				array(
71 28
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
72
					'variable' => $variable,
73
				)
74
			);
75 28
76
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
77 28
		}
78
79
		// Clean out the cache and make sure the cobwebs are gone too.
80 35
		$cache->remove('modSettings');
81 35
82
		return;
83
	}
84 35
85
	$replaceArray = array();
86 14
	foreach ($changeArray as $variable => $value)
87
	{
88
		// Don't bother if it's already like that ;).
89
		if (isset($modSettings[$variable]) && $modSettings[$variable] === $value)
90 35
		{
91
			continue;
92
		}
93
94
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
95 35
		if (!isset($modSettings[$variable]) && empty($value))
96
		{
97 35
			continue;
98
		}
99
100 35
		$replaceArray[] = array($variable, $value);
101
102 8
		$modSettings[$variable] = $value;
103
	}
104
105 35
	if (empty($replaceArray))
106 35
	{
107 35
		return;
108 15
	}
109 35
110
	$db->replace(
111
		'{db_prefix}settings',
112
		array('variable' => 'string-255', 'value' => 'string-65534'),
113 35
		$replaceArray,
114 35
		array('variable')
115
	);
116
117
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
118
	$cache->remove('modSettings');
119
}
120
121
/**
122
 * Deletes one setting from the settings table and takes care of $modSettings as well
123 3
 *
124
 * @param string|string[] $toRemove the setting or the settings to be removed
125 3
 */
126
function removeSettings($toRemove)
127 3
{
128
	global $modSettings;
129
130
	$db = database();
131
132 3
	if (empty($toRemove))
133
	{
134 3
		return;
135
	}
136
137
	if (!is_array($toRemove))
138 3
	{
139
		$toRemove = array($toRemove);
140
	}
141
142 3
	// Remove the setting from the db
143
	$db->query('', '
144
		DELETE FROM {db_prefix}settings
145
		WHERE variable IN ({array_string:setting_name})',
146
		array(
147 3
			'setting_name' => $toRemove,
148
		)
149 3
	);
150
151 2
	// Remove it from $modSettings now so it does not persist
152
	foreach ($toRemove as $setting)
153
	{
154
		if (isset($modSettings[$setting]))
155
		{
156 3
			unset($modSettings[$setting]);
157 3
		}
158
	}
159
160
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
161
	Cache::instance()->remove('modSettings');
162
}
163
164
/**
165
 * Constructs a page list.
166
 *
167
 * @depreciated since 2.0
168
 *
169
 */
170
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show = [])
171
{
172
	$pageindex = new ConstructPageIndex($base_url, $start, $max_value, $num_per_page, $flexible_start, $show);
173
	return $pageindex->getPageIndex();
174
}
175
176
/**
177
 * Formats a number.
178
 *
179
 * What it does:
180
 *
181
 * - Uses the format of number_format to decide how to format the number.
182
 *   for example, it might display "1 234,50".
183
 * - Caches the formatting data from the setting for optimization.
184
 *
185
 * @param float $number The float value to apply comma formatting
186
 * @param int|bool $override_decimal_count = false or number of decimals
187
 *
188
 * @return string
189 18
 */
190
function comma_format($number, $override_decimal_count = false)
191
{
192 18
	global $txt;
193 18
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
194
195 18
	// Cache these values...
196
	if ($decimal_separator === null)
197
	{
198
		// Not set for whatever reason?
199 18
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
200
		{
201
			return $number;
202 18
		}
203
204
		// Cache these each load...
205
		$thousands_separator = $matches[1];
206
		$decimal_separator = $matches[2];
207 18
		$decimal_count = strlen($matches[3]);
208
	}
209 6
210
	// Format the string with our friend, number_format.
211
	$decimals = ((float) $number === $number) ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0;
212
	return number_format((float) $number, (int) $decimals, $decimal_separator, $thousands_separator);
213
}
214 12
215
/**
216
 * Formats a number to a multiple of thousands x, x k, x M, x G, x T
217 18
 *
218
 * @param float $number The value to format
219 18
 * @param int|bool $override_decimal_count = false or number of decimals
220
 *
221
 * @return string
222 18
 */
223
function thousands_format($number, $override_decimal_count = false)
224
{
225
	foreach (['', ' k', ' M', ' G', ' T'] as $kb)
226
	{
227
		if ($number < 1000)
228
		{
229
			break;
230
		}
231
232
		$number /= 1000;
233
	}
234
235
	return comma_format($number, $override_decimal_count) . $kb;
236
}
237
238
/**
239
 * Formats a number to a computer byte size value xB, xKB, xMB, xGB
240
 *
241
 * @param int $number
242
 *
243
 * @return string
244 18
 */
245
function byte_format($number)
246
{
247 18
	global $txt;
248
249
	foreach (array('byte', 'kilobyte', 'megabyte', 'gigabyte') as $kb)
250
	{
251
		if ($number < 1024)
252
		{
253 18
			break;
254
		}
255
256
		$number /= 1024;
257 18
	}
258
259
	return comma_format($number) . ' ' . $txt[$kb];
260
}
261
262
/**
263 18
 * Format a time to make it look purdy.
264
 *
265
 * What it does:
266
 *
267
 * - Returns a pretty formatted version of time based on the user's format in User::$info->time_format.
268
 * - Applies all necessary time offsets to the timestamp, unless offset_type is set.
269
 * - If todayMod is set and show_today was not specified or true, an
270
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
271
 * - Performs localization (more than just strftime would do alone.)
272
 *
273
 * @param int $log_time A unix timestamp
274
 * @param string|bool $show_today = true show "Today"/"Yesterday",
275
 *   false shows the date, a string can force a date format to use %b %d, %Y
276
 * @param string|bool $offset_type = false If false, uses both user time offset and forum offset.
277
 *   If 'forum', uses only the forum offset. Otherwise no offset is applied.
278
 *
279
 * @return string
280
 */
281
function standardTime($log_time, $show_today = true, $offset_type = false)
282 18
{
283
	global $txt, $modSettings;
284 18
	static $non_twelve_hour, $is_win = null;
285
286
	if ($is_win === null)
287
	{
288
		$is_win = detectServer()->is('windows');
289
	}
290
291
	// Offset the time.
292 18
	if (!$offset_type)
293
	{
294 18
		$time = $log_time + (User::$info->time_offset + $modSettings['time_offset']) * 3600;
295
	}
296
	// Just the forum offset?
297
	elseif ($offset_type === 'forum')
298
	{
299
		$time = $log_time + $modSettings['time_offset'] * 3600;
300
	}
301
	else
302 18
	{
303 18
		$time = $log_time;
304
	}
305 18
306
	// We can't have a negative date (on Windows, at least.)
307
	if ($log_time < 0)
308
	{
309
		$log_time = 0;
310
	}
311
312
	// Today and Yesterday?
313 18
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
314
	{
315
		// Get the current time.
316
		$nowtime = forum_time();
317
318
		$then = @getdate($time);
319
		$now = @getdate($nowtime);
320
321
		// Try to make something of a time format string...
322
		$s = strpos(User::$info->time_format, '%S') === false ? '' : ':%S';
323
		if (strpos(User::$info->time_format, '%H') === false && strpos(User::$info->time_format, '%T') === false)
324
		{
325
			$h = strpos(User::$info->time_format, '%l') === false ? '%I' : '%l';
326
			$today_fmt = $h . ':%M' . $s . ' %p';
327
		}
328
		else
329
		{
330
			$today_fmt = '%H:%M' . $s;
331
		}
332 18
333
		// Same day of the year, same year.... Today!
334
		if ($then['yday'] === $now['yday'] && $then['year'] === $now['year'])
335
		{
336
			return sprintf($txt['today'], standardTime($log_time, $today_fmt, $offset_type));
337
		}
338 18
339
		// 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...
340
		if ((int) $modSettings['todayMod'] === 2
341
			&& (($then['yday'] === $now['yday'] - 1 && $then['year'] === $now['year'])
342
				|| (($now['yday'] === 0 && $then['year'] === $now['year'] - 1) && $then['mon'] === 12 && $then['mday'] === 31)))
343
		{
344
			return sprintf($txt['yesterday'], standardTime($log_time, $today_fmt, $offset_type));
345 18
		}
346
	}
347
348
	$str = is_bool($show_today) ? User::$info->time_format : $show_today;
349
350
	// Windows requires a slightly different language code identifier (LCID).
351
	// https://msdn.microsoft.com/en-us/library/cc233982.aspx
352
	if ($is_win)
353
	{
354
		$txt['lang_locale'] = str_replace('_', '-', $txt['lang_locale']);
355
	}
356
357 18
	if (setlocale(LC_TIME, $txt['lang_locale']))
358
	{
359
		if (!isset($non_twelve_hour))
360
		{
361
			$non_twelve_hour = trim(Util::strftime('%p')) === '';
362
		}
363
364
		if ($non_twelve_hour && strpos($str, '%p') !== false)
365
		{
366
			$str = str_replace('%p', (Util::strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
367
		}
368
369
		foreach (['%a', '%A', '%b', '%B'] as $token)
370
		{
371
			if (strpos($str, $token) !== false)
372
			{
373
				$str = str_replace($token, empty($txt['lang_capitalize_dates']) ? Util::strftime($token, $time) : Util::ucwords(Util::strftime($token, $time)), $str);
374
			}
375
		}
376 10
	}
377 10
	else
378
	{
379
		// Do-it-yourself time localization.  Fun.
380 10
		foreach (['%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months'] as $token => $text_label)
381
		{
382
			if (strpos($str, $token) !== false)
383 2
			{
384
				$str = str_replace($token, $txt[$text_label][(int) Util::strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
385
			}
386
		}
387
388
		if (strpos($str, '%p') !== false)
389 2
		{
390 2
			$str = str_replace('%p', (Util::strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
391 2
		}
392
	}
393
394
	// Windows doesn't support %e; on some versions, strftime fails altogether if used, so let's prevent that.
395 10
	if ($is_win && strpos($str, '%e') !== false)
396
	{
397
		$str = str_replace('%e', ltrim(Util::strftime('%d', $time), '0'), $str);
398
	}
399
400
	// Format any other characters..
401
	return Util::strftime($str, $time);
402
}
403
404
/**
405
 * Used to render a timestamp to html5 <time> tag format.
406
 *
407
 * @param int $timestamp A unix timestamp
408
 *
409
 * @return string
410
 */
411
function htmlTime($timestamp)
412
{
413
	global $txt, $context;
414
415
	if (empty($timestamp))
416
	{
417
		return '';
418
	}
419
420
	$forumtime = forum_time(false, $timestamp);
421
	$timestamp = forum_time(true, $timestamp);
422
	$time = date('Y-m-d H:i', $timestamp);
423
	$stdtime = standardTime($timestamp, true, true);
424
425
	// @todo maybe htmlspecialchars on the title attribute?
426
	return '<time title="' . (empty($context['using_relative_time']) ? $txt['last_post'] : $stdtime) . '" datetime="' . $time . '" data-timestamp="' . $timestamp . '" data-forumtime="' . $forumtime . '">' . $stdtime . '</time>';
427
}
428
429
/**
430 2
 * Convert a given timestamp to UTC time in the format of Atom date format.
431
 *
432 2
 * This method takes a unix timestamp as input and converts it to UTC time in the format of
433 2
 * Atom date format (YYYY-MM-DDTHH:MM:SS+00:00).
434
 *
435 2
 * It considers the user's time offset, system's time offset, and the default timezone setting
436
 * from the modifications/settings administration panel.
437 2
 *
438
 * @param int $timestamp The timestamp to convert to UTC time.
439
 * @param int $userAdjust The timestamp is not to be adjusted for user offset
440 2
 * @return string The UTC time in the format of Atom date format.
441
 */
442
function utcTime($timestamp, $userAdjust = false)
443 2
{
444
	global $user_info, $modSettings;
445
446
	// Back out user time
447
	if ($userAdjust === true && !empty($user_info['time_offset']))
448
	{
449
		$timestamp -= ($modSettings['time_offset'] + $user_info['time_offset']) * 3600;
450
	}
451
452
	// Using the system timezone offset, format the date
453
	try
454
	{
455
		$tz = empty($modSettings['default_timezone']) ? 'UTC' : $modSettings['default_timezone'];
456
		$date = new DateTime('@' . $timestamp, new DateTimeZone($tz));
457
	}
458
	catch (Exception)
459
	{
460
		return standardTime($timestamp);
461
	}
462
463
	// Something like 2012-12-21T11:11:00+00:00
464
	return $date->format(DateTimeInterface::ATOM);
465
}
466
467 22
/**
468 22
 * Gets the current time with offset.
469
 *
470 22
 * What it does:
471
 *
472 2
 * - Always applies the offset in the time_offset setting.
473
 *
474
 * @param bool $use_user_offset = true if use_user_offset is true, applies the user's offset as well
475
 * @param int|null $timestamp = null A unix timestamp (null to use current time)
476 22
 *
477
 * @return int seconds since the unix epoch
478 16
 */
479
function forum_time($use_user_offset = true, $timestamp = null)
480
{
481 18
	global $modSettings;
482
483
	if ($timestamp === null)
484
	{
485
		$timestamp = time();
486
	}
487 18
	elseif ($timestamp === 0)
488
	{
489
		return 0;
490
	}
491 22
492
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? User::$info->time_offset : 0)) * 3600;
493
}
494
495
/**
496
 * Removes special entities from strings.  Compatibility...
497 22
 *
498
 * - Faster than html_entity_decode
499
 * - Removes the base entities ( &amp; &quot; &#039; &lt; and &gt;. ) from text with htmlspecialchars_decode
500 22
 * - Additionally converts &nbsp with str_replace
501
 *
502 22
 * @param string $string The string to apply htmlspecialchars_decode
503 22
 *
504
 * @return string string without entities
505
 */
506 22
function un_htmlspecialchars($string)
507 22
{
508
	if (empty($string))
509 22
	{
510 22
		return $string;
511
	}
512
513
	$string = htmlspecialchars_decode($string, ENT_QUOTES);
514
515
	return str_replace('&nbsp;', ' ', $string);
516
}
517
518 22
/**
519
 * Lexicographic permutation function.
520 16
 *
521
 * This is a special type of permutation which involves the order of the set. The next
522
 * lexicographic permutation of '32541' is '34125'. Numerically, it is simply the smallest
523
 * set larger than the current one.
524 8
 *
525
 * The benefit of this over a recursive solution is that the whole list does NOT need
526
 * to be held in memory. So it's actually possible to run 30! permutations without
527
 * causing a memory overflow.
528
 *
529
 * Source: O'Reilly PHP Cookbook
530 22
 *
531
 * @param array $p The array keys to apply permutation
532
 * @param int $size The size of our permutation array
533
 *
534 22
 * @return array|bool the next permutation of the passed array $p
535
 */
536
function pc_next_permutation($p, $size)
537
{
538
	// Slide down the array looking for where we're smaller than the next guy
539 22
	for ($i = $size - 1; isset($p[$i]) && $p[$i] >= $p[$i + 1]; --$i)
540
	{
541
		// Required to set $i
542
	}
543
544
	// If this doesn't occur, we've finished our permutations
545
	// the array is reversed: (1, 2, 3, 4) => (4, 3, 2, 1)
546
	if ($i === -1)
547
	{
548
		return false;
549
	}
550
551
	// Slide down the array looking for a bigger number than what we found before
552
	for ($j = $size; $p[$j] <= $p[$i]; --$j)
553
	{
554
		// Required to set $j
555
	}
556
557
	// Swap them
558
	$tmp = $p[$i];
559
	$p[$i] = $p[$j];
560
	$p[$j] = $tmp;
561 22
562
	// Now reverse the elements in between by swapping the ends
563 22
	for (++$i, $j = $size; $i < $j; ++$i, --$j)
564
	{
565 11
		$tmp = $p[$i];
566
		$p[$i] = $p[$j];
567
		$p[$j] = $tmp;
568
	}
569 22
570
	return $p;
571 16
}
572
573
/**
574
 * Ends execution and redirects the user to a new location
575
 *
576 22
 * What it does:
577
 *
578
 * - Makes sure the browser doesn't come back and repost the form data.
579
 * - Should be used whenever anything is posted.
580
 * - Diverts final execution to obExit() which means a end to processing and sending of final output
581
 *
582 22
 * @event integrate_redirect called before headers are sent
583
 * @param string $setLocation = '' The URL to redirect to
584
 */
585
function redirectexit($setLocation = '')
586
{
587
	global $db_show_debug;
588
589
	// Note to developers.  The testbed will add the following, allowing phpunit test returns
590
	//if (defined("PHPUNITBOOTSTRAP") && defined("STDIN")){return $setLocation;}
591
592
	// Send headers, call integration, do maintance
593
	Headers::instance()
594 18
		->removeHeader('all')
595
		->redirect($setLocation)
596 18
		->send();
597
598 2
	// Debugging.
599
	if ($db_show_debug === true)
600
	{
601 18
		$_SESSION['debug_redirect'] = Debug::instance()->get_db();
602 18
	}
603 18
604
	obExit(false);
605
}
606 18
607
/**
608
 * Ends execution.
609
 *
610
 * What it does:
611
 *
612
 * - Takes care of template loading and remembering the previous URL.
613
 * - Calls ob_start() with ob_sessrewrite to fix URLs if necessary.
614
 *
615
 * @event integrate_invalid_old_url allows adding to "from" urls we don't save
616
 * @event integrate_exit inform portal, etc. that we're integrated with to exit
617
 * @param bool|null $header = null Output the header
618
 * @param bool|null $do_footer = null Output the footer
619
 * @param bool $from_index = false If we're coming from index.php
620
 * @param bool $from_fatal_error = false If we are exiting due to a fatal error
621
 * @throws \ElkArte\Exceptions\Exception
622
 */
623 25
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
624
{
625 25
	global $context, $txt, $db_show_debug;
626
627 25
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
628
629 22
	// Attempt to prevent a recursive loop.
630
	++$level;
631
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
632
	{
633
		exit;
634 25
	}
635
636
	if ($from_fatal_error)
637
	{
638
		$has_fatal_error = true;
639
	}
640
641
	$do_header = $header ?? !$header_done;
642
	$do_footer = $do_footer ?? $do_header;
643
644
	// Has the template/header been done yet?
645
	if ($do_header)
646
	{
647
		handleMaintenance();
648
649
		// Was the page title set last minute? Also update the HTML safe one.
650 58
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
651
		{
652 58
			$context['page_title_html_safe'] = Util::htmlspecialchars(un_htmlspecialchars($context['page_title'])) . (empty($context['current_page']) ? '' : ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1));
653
		}
654
655
		// Start up the session URL fixer.
656
		ob_start('ob_sessrewrite');
657
658
		call_integration_buffer();
659
660
		// Display the screen in the logical order.
661
		template_header();
662
		$header_done = true;
663
	}
664
665
	if ($do_footer)
666
	{
667
		// Show the footer.
668
		theme()->getTemplates()->loadSubTemplate($context['sub_template'] ?? 'main');
669
670
		// Just so we don't get caught in an endless loop of errors from the footer...
671
		if (!$footer_done)
672
		{
673
			$footer_done = true;
674
			template_footer();
675
676 4
			// Add $db_show_debug = true; to Settings.php if you want to show the debugging information.
677
			// (since this is just debugging... it's okay that it's after </html>.)
678
			if (($db_show_debug === true)
679
				&& !isset($_REQUEST['api'])
680
				&& ((!isset($_GET['action']) || $_GET['action'] !== 'viewquery') && !isset($_GET['api'])))
681
			{
682 4
				Debug::instance()->display();
683
			}
684 4
		}
685
	}
686
687
	// Need user agent
688 4
	$req = Request::instance();
689
690
	setOldUrl();
691
692
	// For session check verification.... don't switch browsers...
693 4
	$_SESSION['USER_AGENT'] = $req->user_agent();
694 4
695 4
	// Hand off the output to the portal, etc. we're integrated with.
696
	call_integration_hook('integrate_exit', [$do_footer]);
697
698 4
	// Note to developers.  The testbed will add the following, allowing phpunit test returns
699
	//if (defined("PHPUNITBOOTSTRAP") && defined("STDIN")){return;}
700 4
701 4
	// Don't exit if we're coming from index.php; that will pass through normally.
702 4
	if (!$from_index)
703
	{
704
		exit;
705 4
	}
706
}
707
708
/**
709
 * Takes care of a few dynamic maintenance items
710
 */
711
function handleMaintenance()
712
{
713
	global $context;
714
715
	// Clear out the stat cache.
716
	trackStats();
717
718
	// Send off any notifications accumulated
719
	Notifications::instance()->send();
720
721
	// Queue any mail that needs to be sent
722
	if (!empty($context['flush_mail']))
723
	{
724
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
725
		AddMailQueue(true);
726 16
	}
727
}
728
729 16
/**
730
 * @param string $index
731
 */
732
function setOldUrl($index = 'old_url')
733
{
734
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
735 16
	$invalid_old_url = array(
736
		'action=dlattach',
737 16
		'action=jsoption',
738
		';api=xml',
739 16
	);
740
	call_integration_hook('integrate_invalid_old_url', array(&$invalid_old_url));
741 16
	$make_old = true;
742
	foreach ($invalid_old_url as $url)
743
	{
744
		if (strpos($_SERVER['REQUEST_URL'], $url) !== false)
745 16
		{
746
			$make_old = false;
747 16
			break;
748
		}
749
	}
750
751
	if ($make_old)
752
	{
753
		$_SESSION[$index] = $_SERVER['REQUEST_URL'];
754
	}
755
}
756 16
757
/**
758
 * Sets the class of the current topic based on is_very_hot, veryhot, hot, etc
759 16
 *
760
 * @param array $topic_context array of topic information
761 16
 */
762
function determineTopicClass(&$topic_context)
763
{
764
	$topic_context['class'] = empty($topic_context['is_poll']) ? 'i-normal' : 'i-poll';
765
766
	// Set topic class depending on locked status and number of replies.
767
	if ($topic_context['is_very_hot'])
768
	{
769
		$topic_context['class'] = 'i-hot colorize-red';
770
	}
771
	elseif ($topic_context['is_hot'])
772
	{
773
		$topic_context['class'] = 'i-hot colorize-yellow';
774
	}
775
776
	if ($topic_context['is_sticky'])
777
	{
778
		$topic_context['class'] = 'i-sticky';
779
	}
780
781
	if ($topic_context['is_locked'])
782
	{
783
		$topic_context['class'] = 'i-locked';
784
	}
785
}
786
787
/**
788
 * Sets up the basic theme context stuff.
789
 *
790
 * @param bool $forceload defaults to false
791
 */
792
function setupThemeContext($forceload = false)
793
{
794
	theme()->setupThemeContext($forceload);
795
}
796
797
/**
798
 * Helper function to convert memory string settings to bytes
799
 *
800
 * @param string|bool $val The byte string, like 256M or 1G
801
 *
802
 * @return int The string converted to a proper integer in bytes
803
 */
804
function memoryReturnBytes($val)
805
{
806
	// Treat blank values as 0
807
	$val = is_bool($val) || empty($val) ? 0 : trim($val);
808
809
	// Separate the number from the designator, if any
810
	preg_match('~(\d+)(.*)~', $val, $val);
811
	$num = (int) $val[1];
812
	$last = strtolower(substr($val[2] ?? '', 0, 1));
813
814
	// Convert to bytes
815
	switch ($last)
816
	{
817
		// fall through select g = 1024*1024*1024
818
		case 'g':
819
			$num *= 1024;
820
		// fall through select m = 1024*1024
821
		case 'm':
822
			$num *= 1024;
823
		// fall through select k = 1024
824
		case 'k':
825
			$num *= 1024;
826
	}
827
828
	return $num;
829
}
830
831
/**
832
 * This is the only template included in the sources.
833
 * @return void
834
 */
835
function template_rawdata()
836
{
837
	theme()->template_rawdata();
838
}
839
840
/**
841
 * The header template
842
 * @return void
843
 */
844
function template_header()
845
{
846
	theme()->template_header();
847
}
848
849
/**
850
 * Show the copyright.
851
 * @return void
852
 */
853
function theme_copyright()
854
{
855
	theme()->theme_copyright();
856
}
857
858
/**
859
 * The template footer
860
 * @return void
861
 */
862
function template_footer()
863
{
864
	theme()->template_footer();
865
}
866
867
/**
868
 * Output the Javascript files
869
 *
870
 * @depreciated since 2.0, only for old theme support
871
 * @return void
872
 */
873
function template_javascript()
874
{
875
	theme()->themeJs()->template_javascript();
876
}
877
878
/**
879
 * Output the CSS files
880
 *
881
 * @depreciated since 2.0, only for old theme suppot
882
 * @return void
883
 */
884
function template_css()
885
{
886
	theme()->themecss->template_css();
887
}
888
889
/**
890
 * Calls on template_show_error from index.template.php to show warnings
891
 * and security errors for admins
892
 * @return void
893
 */
894
function template_admin_warning_above()
895
{
896
	theme()->template_admin_warning_above();
897
}
898
899
/**
900
 * Convert IP address to IP range
901
 *
902
 *  - Internal function used to convert a user-readable format to a format suitable for the database.
903
 *
904
 * @param string $fullip The IP address to convert
905
 * @return array The IP range in the format [ ['low' => 'low_value_1', 'high' => 'high_value_1'], ... ]
906
 * If the input IP address is invalid or cannot be converted, an empty array is returned.
907
 */
908
function ip2range($fullip)
909
{
910
	// If its IPv6, validate it first.
911
	if (isValidIPv6($fullip))
912
	{
913
		$ip_parts = explode(':', expandIPv6($fullip, false));
914
		$ip_array = [];
915
916
		if (count($ip_parts) !== 8)
917
		{
918
			return [];
919
		}
920
921
		for ($i = 0; $i < 8; $i++)
922
		{
923
			if ($ip_parts[$i] === '*')
924
			{
925
				$ip_array[$i] = ['low' => '0', 'high' => hexdec('ffff')];
926
			}
927
			elseif (preg_match('/^([0-9A-Fa-f]{1,4})-([0-9A-Fa-f]{1,4})$/', $ip_parts[$i], $range) === 1)
928
			{
929
				$ip_array[$i] = ['low' => hexdec($range[1]), 'high' => hexdec($range[2])];
930
			}
931
			elseif (is_numeric(hexdec($ip_parts[$i])))
932 4
			{
933
				$ip_array[$i] = ['low' => hexdec($ip_parts[$i]), 'high' => hexdec($ip_parts[$i])];
934
			}
935
		}
936 4
937
		return $ip_array;
938
	}
939
940
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
941
	if ($fullip === 'unknown')
942 4
	{
943
		$fullip = '255.255.255.255';
944
	}
945 4
946
	$ip_parts = explode('.', $fullip);
947 4
	$ip_array = [];
948
949
	if (count($ip_parts) !== 4)
950
	{
951
		return [];
952 4
	}
953
954
	for ($i = 0; $i < 4; $i++)
955
	{
956 4
		if ($ip_parts[$i] === '*')
957
		{
958
			$ip_array[$i] = ['low' => '0', 'high' => '255'];
959
		}
960
		elseif (preg_match('/^(\d{1,3})-(\d{1,3})$/', $ip_parts[$i], $range) === 1)
961
		{
962
			$ip_array[$i] = ['low' => $range[1], 'high' => $range[2]];
963
		}
964
		elseif (is_numeric($ip_parts[$i]))
965
		{
966
			$ip_array[$i] = ['low' => $ip_parts[$i], 'high' => $ip_parts[$i]];
967
		}
968
	}
969
970
	// Makes it simpler to work with.
971
	$ip_array[4] = ['low' => 0, 'high' => 0];
972
	$ip_array[5] = ['low' => 0, 'high' => 0];
973
	$ip_array[6] = ['low' => 0, 'high' => 0];
974
	$ip_array[7] = ['low' => 0, 'high' => 0];
975
976
	return $ip_array;
977
}
978
979 10
/**
980
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
981
 *
982
 * @param string $ip A full dot notation IP address
983
 *
984
 * @return string
985 10
 */
986 10
function host_from_ip($ip)
987 10
{
988
	global $modSettings;
989
990 5
	$cache = Cache::instance();
991
992
	$host = '';
993 10
	if (empty($ip) || $cache->getVar($host, 'hostlookup-' . $ip, 600))
994 10
	{
995
		return $host;
996 10
	}
997 10
998
	$t = microtime(true);
999
1000 10
	// Check if shell_exec is on the list of disabled functions.
1001
	if (function_exists('shell_exec'))
1002
	{
1003 10
		// Try the Linux host command, perhaps?
1004
		if (PHP_OS_FAMILY !== 'Windows' && mt_rand(0, 1) === 1)
1005
		{
1006
			if (!isset($modSettings['host_to_dis']))
1007
			{
1008
				$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
1009
			}
1010
			else
1011
			{
1012
				$test = @shell_exec('host ' . @escapeshellarg($ip));
1013
			}
1014
1015
			$test = $test ?? '';
1016
1017
			// Did host say it didn't find anything?
1018
			if (stripos($test, 'not found') !== false)
1019
			{
1020
				$host = '';
1021
			}
1022
			// Invalid server option?
1023
			elseif ((stripos($test, 'invalid option') || stripos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
1024
			{
1025
				updateSettings(array('host_to_dis' => 1));
1026
			}
1027
			// Maybe it found something, after all?
1028
			elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
1029
			{
1030
				$host = $match[1];
1031
			}
1032
		}
1033
1034
		// This is nslookup; usually default on Windows, and possibly some Unix with bind-utils
1035
		if ((empty($host) || PHP_OS_FAMILY === 'Windows') && mt_rand(0, 1) === 1)
1036
		{
1037
			$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
1038
1039
			if (stripos($test, 'Non-existent domain') !== false)
1040
			{
1041
				$host = '';
1042
			}
1043
			elseif (preg_match('~(?:Name:|Name =)\s+([^\s]+)~i', $test, $match) === 1)
1044
			{
1045
				$host = $match[1];
1046
			}
1047
		}
1048
	}
1049
1050
	// This is the last try :/.
1051
	if (!isset($host))
1052
	{
1053
		$host = @gethostbyaddr($ip);
1054
	}
1055
1056
	// It took a long time, so let's cache it!
1057
	if (microtime(true) - $t > 0.5)
1058
	{
1059
		$cache->put('hostlookup-' . $ip, $host, 600);
1060
	}
1061
1062
	return $host;
1063
}
1064
1065
/**
1066
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
1067
 *
1068
 * @param string $text The string to process
1069
 *     - if encrypt = true this is the maximum number of bytes to use in integer hashes (for searching)
1070
 *     - if encrypt = false this is the maximum number of letters in each word
1071
 * @param bool $encrypt = false Used for custom search indexes to return an int[] array representing the words
1072
 *
1073
 * @return array
1074
 */
1075
function text2words($text, $encrypt = false)
1076
{
1077
	// Step 0: prepare numbers so they are good for search & index 1000.45 -> 1000_45
1078
	$words = preg_replace('~([\d]+)[.-/]+(?=[\d])~u', '$1_', $text);
1079
1080
	// Step 1: Remove entities/things we don't consider words:
1081
	$words = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', strtr($words, array('<br />' => ' ')));
1082
1083
	// Step 2: Entities we left to letters, where applicable, lowercase.
1084
	$words = un_htmlspecialchars(Util::strtolower($words));
1085
1086
	// Step 3: Ready to split apart and index!
1087
	$words = explode(' ', $words);
1088
1089
	if ($encrypt)
1090
	{
1091
		$blocklist = getBlocklist();
1092
		$returned_ints = [];
1093
1094
		// Only index unique words
1095
		$words = array_unique($words);
1096
		foreach ($words as $word)
1097
		{
1098
			$word = trim($word, "-_'");
1099
			if ($word !== '' && !in_array($word, $blocklist) && Util::strlen($word) > 2)
1100
			{
1101
				// Get a hex representation of this word using a database indexing hash
1102
				// designed to be fast while maintaining a very low collision rate
1103
				$encrypted = hash('FNV1A32', $word);
1104
1105
				// Create an integer representation, the hash is an 8 char hex
1106
				// so the largest int will be 4294967295 which fits in db int(10)
1107
				$returned_ints[$word] = hexdec($encrypted);
1108
			}
1109
		}
1110
1111
		return $returned_ints;
1112
	}
1113
1114
	// Trim characters before and after and add slashes for database insertion.
1115
	$returned_words = [];
1116
	foreach ($words as $word)
1117
	{
1118
		if (($word = trim($word, "-_'")) !== '')
1119
		{
1120
			$returned_words[] = substr($word, 0, 20);
1121
		}
1122
	}
1123
1124
	// Filter out all words that occur more than once.
1125
	return array_unique($returned_words);
1126
}
1127
1128
/**
1129
 * Get the block list from the search controller.
1130
 *
1131
 * @return array
1132
 */
1133
function getBlocklist()
1134
{
1135
	static $blocklist;
1136
1137
	if (!isset($blocklist))
1138
	{
1139
		$search = new Search();
1140
		$blocklist = $search->getBlockListedWords();
1141
		unset($search);
1142
	}
1143
1144
	return $blocklist;
1145
}
1146
1147
/**
1148
 * Sets up all of the top menu buttons
1149
 *
1150
 * What it does:
1151
 *
1152
 * - Defines every master item in the menu, as well as any sub-items
1153
 * - Ensures the chosen action is set so the menu is highlighted
1154
 * - Saves them in the cache if it is available and on
1155
 * - Places the results in $context
1156
 */
1157
function setupMenuContext()
1158
{
1159
	return theme()->setupMenuContext();
0 ignored issues
show
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

1159
	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...
1160
}
1161
1162
/**
1163
 * Process functions of an integration hook.
1164
 *
1165
 * What it does:
1166
 *
1167
 * - Calls all functions of the given hook.
1168
 * - Supports static class method calls.
1169
 *
1170
 * @param string $hook The name of the hook to call
1171
 * @param array $parameters = array() Parameters to pass to the hook
1172
 *
1173
 * @return array the results of the functions
1174
 */
1175
function call_integration_hook($hook, $parameters = array())
1176
{
1177
	return Hooks::instance()->hook($hook, $parameters);
1178
}
1179
1180
/**
1181
 * Includes files for hooks that only do that (i.e. integrate_pre_include)
1182
 *
1183
 * @param string $hook The name to include
1184
 */
1185
function call_integration_include_hook($hook)
1186
{
1187
	Hooks::instance()->include_hook($hook);
1188
}
1189
1190
/**
1191
 * Special hook call executed during obExit
1192
 */
1193
function call_integration_buffer()
1194
{
1195
	Hooks::instance()->buffer_hook();
1196
}
1197
1198
/**
1199
 * Add a function for integration hook.
1200
 *
1201
 * - Does nothing if the function is already added.
1202
 *
1203
 * @param string $hook The name of the hook to add
1204
 * @param string $function The function associated with the hook
1205
 * @param string $file The file that contains the function
1206
 * @param bool $permanent = true if true, updates the value in settings table
1207
 */
1208
function add_integration_function($hook, $function, $file = '', $permanent = true)
1209
{
1210
	Hooks::instance()->add($hook, $function, $file, $permanent);
1211
}
1212
1213
/**
1214
 * Remove an integration hook function.
1215
 *
1216
 * What it does:
1217
 *
1218
 * - Removes the given function from the given hook.
1219
 * - Does nothing if the function is not available.
1220
 *
1221
 * @param string $hook The name of the hook to remove
1222
 * @param string $function The name of the function
1223
 * @param string $file The file its located in
1224
 */
1225
function remove_integration_function($hook, $function, $file = '')
1226
{
1227
	Hooks::instance()->remove($hook, $function, $file);
1228
}
1229
1230
/**
1231
 * Decode numeric html entities to their UTF8 equivalent character.
1232
 *
1233
 * What it does:
1234
 *
1235
 * - Callback function for preg_replace_callback in subs-members
1236
 * - Uses capture group 2 in the supplied array
1237
 * - Does basic scan to ensure characters are inside a valid range
1238
 *
1239
 * @param array $matches matches from a preg_match_all
1240
 *
1241
 * @return string $string
1242
 */
1243
function replaceEntities__callback($matches)
1244
{
1245
	if (!isset($matches[2]))
1246
	{
1247
		return '';
1248
	}
1249
1250
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
1251
1252
	// remove left to right / right to left overrides
1253
	if ($num === 0x202D || $num === 0x202E)
1254
	{
1255
		return '';
1256 26
	}
1257
1258
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
1259 26
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
1260
	{
1261
		return '&#' . $num . ';';
1262 26
	}
1263
1264 26
	// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
1265
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
1266
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
1267
	{
1268
		return '';
1269
	}
1270
1271
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1272
	if ($num < 0x80)
1273
	{
1274
		return chr($num);
1275
	}
1276
1277
	// <0x800 (2048)
1278
	if ($num < 0x800)
1279
	{
1280
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1281
	}
1282
1283
	// < 0x10000 (65536)
1284
	if ($num < 0x10000)
1285
	{
1286
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1287
	}
1288
1289
	// <= 0x10FFFF (1114111)
1290
	return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1291
}
1292
1293 26
/**
1294 26
 * Converts html entities to utf8 equivalents
1295
 *
1296 26
 * What it does:
1297
 *
1298 26
 * - Callback function for preg_replace_callback
1299
 * - Uses capture group 1 in the supplied array
1300
 * - Does basic checks to keep characters inside a viewable range.
1301
 *
1302
 * @param array $matches array of matches as output from preg_match_all
1303 26
 *
1304
 * @return string $string
1305
 */
1306
function fixchar__callback($matches)
1307
{
1308
	if (!isset($matches[1]))
1309
	{
1310
		return '';
1311
	}
1312
1313
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
1314
1315
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
1316
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
1317
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
1318
	{
1319
		return '';
1320
	}
1321
1322
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1323
	if ($num < 0x80)
1324
	{
1325
		return chr($num);
1326
	}
1327
1328
	// <0x800 (2048)
1329
	if ($num < 0x800)
1330
	{
1331
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1332
	}
1333
1334
	// < 0x10000 (65536)
1335
	if ($num < 0x10000)
1336
	{
1337 299
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1338
	}
1339
1340
	// <= 0x10FFFF (1114111)
1341
	return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1342
}
1343
1344
/**
1345
 * Strips out invalid html entities, replaces others with html style &#123; codes
1346
 *
1347 231
 * What it does:
1348 231
 *
1349
 * - Callback function used of preg_replace_callback in various $ent_checks,
1350
 * - For example strpos, strlen, substr etc
1351
 *
1352
 * @param array $matches array of matches for a preg_match_all
1353
 *
1354
 * @return string
1355
 */
1356
function entity_fix__callback($matches)
1357
{
1358
	if (!isset($matches[2]))
1359
	{
1360
		return '';
1361
	}
1362
1363
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
1364
1365
	// We don't allow control characters, characters out of range, byte markers, etc
1366
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
1367
	{
1368
		return '';
1369
	}
1370 4
1371 4
	return '&#' . $num . ';';
1372
}
1373
1374
/**
1375
 * Retrieve additional search engines, if there are any, as an array.
1376
 *
1377
 * @return array array of engines
1378
 */
1379
function prepareSearchEngines()
1380
{
1381
	global $modSettings;
1382
1383
	$engines = [];
1384
	if (!empty($modSettings['additional_search_engines']))
1385
	{
1386
		$search_engines = Util::unserialize($modSettings['additional_search_engines']);
1387 2
		foreach ($search_engines as $engine)
1388 2
		{
1389
			$engines[strtolower(preg_replace('~[^A-Za-z0-9 ]~', '', $engine['name']))] = $engine;
1390
		}
1391
	}
1392
1393
	return $engines;
1394
}
1395
1396
/**
1397
 * This function receives a request handle and attempts to retrieve the next result.
1398
 *
1399
 * What it does:
1400
 *
1401
 * - It is used by the controller callbacks from the template, such as
1402
 * posts in topic display page, posts search results page, or personal messages.
1403
 *
1404
 * @param resource $messages_request holds a query result
1405
 * @param bool $reset
1406
 *
1407
 * @return int|bool
1408
 * @throws Exception
1409
 */
1410
function currentContext($messages_request, $reset = false)
1411
{
1412
	// Start from the beginning...
1413
	if ($reset)
1414
	{
1415
		return $messages_request->data_seek(0);
1416
	}
1417
1418
	// If the query has already returned false, get out of here
1419
	if ($messages_request->hasResults())
1420
	{
1421
		return false;
1422
	}
1423
1424
	// Attempt to get the next message.
1425
	$message = $messages_request->fetch_assoc();
1426
	if (!$message)
1427
	{
1428
		$messages_request->free_result();
1429
1430
		return false;
1431
	}
1432
1433
	return $message;
1434
}
1435
1436
/**
1437
 * Helper function to insert an array in to an existing array
1438
 *
1439
 * What it does:
1440
 *
1441
 * - Intended for addon use to allow such things as
1442
 * - Adding in a new menu item to an existing menu array
1443
 *
1444
 * @param array $input the array we will insert to
1445
 * @param string $key the key in the array that we are looking to find for the insert action
1446
 * @param array $insert the actual data to insert before or after the key
1447
 * @param string $where adding before or after
1448
 * @param bool $assoc if the array is a assoc array with named keys or a basic index array
1449
 * @param bool $strict search for identical elements, this means it will also check the types of the needle.
1450
 *
1451
 * @return array
1452
 */
1453
function elk_array_insert($input, $key, $insert, $where = 'before', $assoc = true, $strict = false)
1454
{
1455
	$position = $assoc ? array_search($key, array_keys($input), $strict) : array_search($key, $input, $strict);
1456
1457
	// If the key is not found, just insert it at the end
1458
	if ($position === false)
1459
	{
1460
		return array_merge($input, $insert);
1461
	}
1462
1463
	if ($where === 'after')
1464
	{
1465
		$position++;
1466
	}
1467
1468
	// Insert as first
1469
	if (empty($position))
1470
	{
1471
		return array_merge($insert, $input);
1472
	}
1473
1474
	return array_merge(array_slice($input, 0, $position), $insert, array_slice($input, $position));
1475
}
1476
1477
/**
1478
 * Run a scheduled task now
1479
 *
1480
 * What it does:
1481
 *
1482
 * - From time to time it may be necessary to fire a scheduled task ASAP
1483
 * - This function sets the scheduled task to be called before any other one
1484
 *
1485
 * @param string $task the name of a scheduled task
1486
 */
1487
function scheduleTaskImmediate($task)
1488
{
1489
	global $modSettings;
1490
1491
	if (!isset($modSettings['scheduleTaskImmediate']))
1492
	{
1493
		$scheduleTaskImmediate = array();
1494
	}
1495
	else
1496
	{
1497
		$scheduleTaskImmediate = Util::unserialize($modSettings['scheduleTaskImmediate']);
1498
	}
1499
1500
	// If it has not been scheduled, the do so now
1501
	if (!isset($scheduleTaskImmediate[$task]))
1502
	{
1503
		$scheduleTaskImmediate[$task] = 0;
1504
		updateSettings(array('scheduleTaskImmediate' => serialize($scheduleTaskImmediate)));
1505
1506
		require_once(SUBSDIR . '/ScheduledTasks.subs.php');
1507
1508
		// Ensure the task is on
1509
		toggleTaskStatusByName($task, true);
1510
1511
		// Before trying to run it **NOW** :P
1512
		calculateNextTrigger($task, true);
1513
	}
1514
}
1515
1516
/**
1517
 * For diligent people: remove scheduleTaskImmediate when done, otherwise
1518 8
 * a maximum of 10 executions is allowed
1519
 *
1520
 * @param string $task the name of a scheduled task
1521
 * @param bool $calculateNextTrigger if recalculate the next task to execute
1522
 */
1523 8
function removeScheduleTaskImmediate($task, $calculateNextTrigger = true)
1524
{
1525
	global $modSettings;
1526 8
1527
	// Not on, bail
1528 4
	if (!isset($modSettings['scheduleTaskImmediate']))
1529
	{
1530
		return;
1531 4
	}
1532
1533
	$scheduleTaskImmediate = Util::unserialize($modSettings['scheduleTaskImmediate']);
1534
1535
	// Clear / remove the task if it was set
1536
	if (isset($scheduleTaskImmediate[$task]))
1537
	{
1538
		unset($scheduleTaskImmediate[$task]);
1539
		updateSettings(array('scheduleTaskImmediate' => serialize($scheduleTaskImmediate)));
1540
1541
		// Recalculate the next task to execute
1542
		if ($calculateNextTrigger)
1543
		{
1544
			require_once(SUBSDIR . '/ScheduledTasks.subs.php');
1545
			calculateNextTrigger($task);
1546
		}
1547
	}
1548
}
1549
1550
/**
1551
 * Helper function to replace commonly used urls in text strings
1552
 *
1553
 * @event integrate_basic_url_replacement add additional place holder replacements
1554
 * @param string $string the string to inject URLs into
1555
 *
1556
 * @return string the input string with the place-holders replaced with
1557
 *           the correct URLs
1558
 */
1559
function replaceBasicActionUrl($string)
1560
{
1561
	global $scripturl, $context, $boardurl;
1562
	static $find_replace = null;
1563
1564
	if ($find_replace === null)
1565
	{
1566
		$find_replace = array(
1567
			'{forum_name}' => $context['forum_name'],
1568
			'{forum_name_html_safe}' => $context['forum_name_html_safe'],
1569
			'{forum_name_html_unsafe}' => un_htmlspecialchars($context['forum_name_html_safe']),
1570
			'{script_url}' => $scripturl,
1571
			'{board_url}' => $boardurl,
1572
			'{login_url}' => getUrl('action', ['action' => 'login']),
1573
			'{register_url}' => getUrl('action', ['action' => 'register']),
1574
			'{activate_url}' => getUrl('action', ['action' => 'register', 'sa' => 'activate']),
1575
			'{help_url}' => getUrl('action', ['action' => 'help']),
1576
			'{admin_url}' => getUrl('admin', ['action' => 'admin']),
1577
			'{moderate_url}' => getUrl('moderate', ['action' => 'moderate']),
1578
			'{recent_url}' => getUrl('action', ['action' => 'recent']),
1579
			'{search_url}' => getUrl('action', ['action' => 'search']),
1580
			'{who_url}' => getUrl('action', ['action' => 'who']),
1581
			'{credits_url}' => getUrl('action', ['action' => 'about', 'sa' => 'credits']),
1582
			'{calendar_url}' => getUrl('action', ['action' => 'calendar']),
1583
			'{memberlist_url}' => getUrl('action', ['action' => 'memberlist']),
1584
			'{stats_url}' => getUrl('action', ['action' => 'stats']),
1585
		);
1586
		call_integration_hook('integrate_basic_url_replacement', array(&$find_replace));
1587
	}
1588
1589
	return str_replace(array_keys($find_replace), array_values($find_replace), $string);
1590
}
1591
1592
/**
1593
 * This function creates a new GenericList from all the passed options.
1594
 *
1595
 * What it does:
1596
 *
1597
 * - Calls integration hook integrate_list_"unique_list_id" to allow easy modifying
1598
 *
1599
 * @event integrate_list_$listID called before every createlist to allow access to its listoptions
1600
 * @param array $listOptions associative array of option => value
1601
 */
1602
function createList($listOptions)
1603
{
1604
	call_integration_hook('integrate_list_' . $listOptions['id'], array(&$listOptions));
1605
1606
	$list = new GenericList($listOptions);
1607
1608
	$list->buildList();
1609
}
1610
1611
/**
1612
 * This handy function retrieves a Request instance and passes it on.
1613
 *
1614
 * What it does:
1615
 *
1616
 * - To get hold of a Request, you can use this function or directly Request::instance().
1617
 * - This is for convenience, it simply delegates to Request::instance().
1618
 */
1619
function request()
1620
{
1621
	return Request::instance();
1622
}
1623
1624
/**
1625
 * Meant to replace any usage of $db_last_error.
1626
 *
1627
 * What it does:
1628
 *
1629
 * - Reads the file db_last_error.txt, if a time() is present returns it,
1630
 * otherwise returns 0.
1631
 */
1632
function db_last_error()
1633
{
1634
	$time = trim(file_get_contents(BOARDDIR . '/db_last_error.txt'));
1635
1636
	if (preg_match('~^\d{10}$~', $time) === 1)
1637
	{
1638
		return $time;
1639
	}
1640
1641
	return 0;
1642
}
1643
1644
/**
1645
 * This function has the only task to retrieve the correct prefix to be used
1646
 * in responses.
1647
 *
1648
 * @return string - The prefix in the default language of the forum
1649
 */
1650
function response_prefix()
1651
{
1652
	global $language, $txt;
1653
	static $response_prefix = null;
1654
1655
	$cache = Cache::instance();
1656
1657
	// Get a response prefix, but in the forum's default language.
1658
	if ($response_prefix === null && (!$cache->getVar($response_prefix, 'response_prefix') || !$response_prefix))
1659
	{
1660
		if ($language === User::$info->language)
1661
		{
1662
			$response_prefix = $txt['response_prefix'];
1663
		}
1664
		else
1665
		{
1666
			$mtxt = [];
1667
			$lang_loader = new Loader($language, $mtxt, database());
1668
			$lang_loader->load('index');
1669
			$response_prefix = $mtxt['response_prefix'];
1670
		}
1671
1672
		$cache->put('response_prefix', $response_prefix, 600);
1673
	}
1674
1675
	return $response_prefix;
1676
}
1677
1678
/**
1679
 * A very simple function to determine if an email address is "valid" for Elkarte.
1680
 *
1681
 * - A valid email for ElkArte is something that resembles an email (filter_var) and
1682
 * is less than 255 characters (for database limits)
1683
 *
1684
 * @param string $value - The string to evaluate as valid email
1685
 *
1686
 * @return string|false - The email if valid, false if not a valid email
1687
 */
1688
function isValidEmail($value)
1689
{
1690
	$value = trim($value);
1691
	if (!filter_var($value, FILTER_VALIDATE_EMAIL))
1692
	{
1693
		return false;
1694
	}
1695
1696
	if (Util::strlen($value) >= 255)
1697
	{
1698
		return false;
1699
	}
1700
1701
	return $value;
1702
}
1703
1704
/**
1705
 * Adds a protocol (http/s, ftp/mailto) to the beginning of an url if missing
1706
 *
1707
 * @param string $url - The url
1708
 * @param string[] $protocols - A list of protocols to check, the first is
1709
 *                 added if none is found (optional, default array('http://', 'https://'))
1710
 *
1711
 * @return string - The url with the protocol
1712
 */
1713
function addProtocol($url, $protocols = array())
1714
{
1715
	if (empty($protocols))
1716
	{
1717
		$pattern = '~^(http://|https://)~i';
1718
		$protocols = array('http://');
1719
	}
1720
	else
1721
	{
1722
		$pattern = '~^(' . implode('|', array_map(static fn($val) => preg_quote($val, '~'), $protocols)) . ')~i';
1723
	}
1724
1725
	$found = false;
1726
	$url = preg_replace_callback($pattern, static function ($match) use (&$found) {
1727
		$found = true;
1728
1729 5
		return strtolower($match[0]);
1730 5
	}, $url);
1731
1732 5
	if ($found)
1733
	{
1734
		return $url;
1735 3
	}
1736 3
1737 3
	return $protocols[0] . $url;
1738 3
}
1739 3
1740 3
/**
1741 3
 * Validate if a URL is allowed to be a "dofollow"
1742 3
 *
1743 3
 * @param string $checkUrl The URL to be checked
1744 3
 * @return bool Returns true if the URL is allowed, false otherwise
1745 3
 */
1746 3
function validateURLAllowList($checkUrl)
1747 3
{
1748 3
	global $modSettings, $boardurl;
1749 3
	static $allowList = null;
1750 3
1751 3
	if ($allowList === null)
1752 3
	{
1753
		$allowList = empty($modSettings['nofollow_allowlist']) ? [] : json_decode($modSettings['nofollow_allowlist']);
1754 3
1755
		// Always allow your own site
1756
		$parse = parse_url($boardurl);
1757 5
		$allowList[] = $parse['host'];
1758
1759
		$allowList = array_unique($allowList);
1760
	}
1761
1762
	$parsed = parse_url($checkUrl);
1763
	if (empty($parsed['host']))
1764
	{
1765
		return false;
1766
	}
1767
1768
	foreach ($allowList as $validDomain)
1769
	{
1770
		if (substr($parsed['host'], -strlen($validDomain)) === $validDomain)
1771
		{
1772 10
			return true;
1773
		}
1774 10
	}
1775
1776 10
	return false;
1777 10
}
1778
1779
/**
1780
 * Removes all, or those over a limit, of nested quotes from a text string.
1781
 *
1782
 * @param string $text - The body we want to remove nested quotes from
1783
 *
1784
 * @return string - The same body, just without nested quotes
1785
 */
1786
function removeNestedQuotes($text)
1787
{
1788
	global $modSettings;
1789 251
1790
	if (!isset($modSettings['removeNestedQuotes']))
1791
	{
1792
		return $text;
1793
	}
1794
1795
	// How many levels will we allow?
1796
	$max_depth = (int) $modSettings['removeNestedQuotes'];
1797
1798
	// Remove quotes over our limit, then we need to find them all
1799
	preg_match_all('~(\[/?quote(.*?)?])~i', $text, $matches, PREG_OFFSET_CAPTURE);
1800
	$depth = 0;
1801
	$remove = [];
1802
	$start_pos = 0;
1803
1804
	// Mark ones that are in excess of the limit.  $match[0] will be the found tag
1805
	// such as [quote=some author] or [/quote], $match[1] is the starting position of that tag.
1806
	foreach ($matches[0] as $match)
1807
	{
1808
		// Closing quote
1809
		if ($match[0][1] === '/')
1810
		{
1811
			--$depth;
1812
1813
			// To many, mark it for removal
1814
			if ($depth === $max_depth)
1815
			{
1816
				// This quote position in the string, note [/quote] = 8
1817
				$end_pos = $match[1] + 8;
1818
				$length = $end_pos - $start_pos;
1819
				$remove[] = [$start_pos, $length];
1820 4
			}
1821 4
1822
			continue;
1823 4
		}
1824
1825
		// Another quote level inward
1826 4
		++$depth;
1827
		if ($depth === $max_depth + 1)
1828 2
		{
1829
			$start_pos = $match[1];
1830 2
		}
1831
	}
1832
1833
	// Time to cull the herd
1834
	foreach (array_reverse($remove) as [$start_pos, $length])
1835
	{
1836
		$text = substr_replace($text, '', $start_pos, $length);
1837
	}
1838
1839 2
	return trim($text);
1840
}
1841
1842 4
/**
1843
 * Change a \t to a span that will show a tab
1844
 *
1845
 * @param string $string
1846
 *
1847
 * @return string
1848
 */
1849
function tabToHtmlTab($string)
1850
{
1851
	return str_replace("\t", "<span class=\"tab\">\t</span>", $string);
1852
}
1853
1854
/**
1855
 * Remove <br />
1856
 *
1857 2
 * @param string $string
1858 2
 *
1859
 * @return string
1860 2
 */
1861
function removeBr($string)
1862
{
1863
	return str_replace('<br />', '', $string);
1864
}
1865
1866
/**
1867
 * Replace all vulgar words with respective proper words. (substring or whole words..)
1868
 *
1869
 * What it does:
1870
 *  - it censors the passed string.
1871
 *  - if the admin setting allow_no_censored is on it does not censor unless force is also set.
1872
 *  - if the admin setting allow_no_censored is off will censor words unless the user has set
1873
 * it to not censor in their profile and force is off
1874
 *  - it caches the list of censored words to reduce parsing.
1875
 *  - Returns the censored text
1876
 *
1877 14
 * @param string $text
1878
 * @param bool $force = false
1879 14
 *
1880 14
 * @return string
1881
 */
1882
function censor($text, $force = false)
1883
{
1884
	global $modSettings;
1885
	static $censor = null;
1886
1887
	if ($censor === null)
1888
	{
1889 14
		$censor = new Censor(explode("\n", $modSettings['censor_vulgar']), explode("\n", $modSettings['censor_proper']), $modSettings);
1890
	}
1891 14
1892
	return $censor->censor($text, $force);
1893 14
}
1894 14
1895
/**
1896 14
 * Helper function able to determine if the current member can see at least
1897
 * one button of a button strip.
1898 14
 *
1899
 * @param array $button_strip
1900
 *
1901 2
 * @return bool
1902
 */
1903
function can_see_button_strip($button_strip)
1904
{
1905
	global $context;
1906
1907
	foreach ($button_strip as $value)
1908
	{
1909
		if (!isset($value['test']) || !empty($context[$value['test']]))
1910
		{
1911
			return true;
1912
		}
1913
	}
1914
1915
	return false;
1916
}
1917
1918
/**
1919
 * Get the current theme instance.
1920
 *
1921
 * @return \ElkArte\Themes\DefaultTheme\Theme The current theme instance.
1922
 */
1923
function theme()
1924
{
1925
	return $GLOBALS['context']['theme_instance'];
1926
}
1927
1928
/**
1929
 * Set the JSON template for sending JSON response.
1930
 *
1931
 * This method prepares the template layers, loads the 'Json' template,
1932
 * and sets the sub_template to 'send_json' in the global $context array.
1933 4
 * The JSON data is initialized to null.
1934
 *
1935
 * @return void
1936
 */
1937
function setJsonTemplate()
1938
{
1939
	global $context;
1940
1941
	$template_layers = $GLOBALS['context']['theme_instance']->getLayers();
1942
	$template_layers->removeAll();
1943
	$GLOBALS['context']['theme_instance']->getTemplates()->load('Json');
1944
	$context['sub_template'] = 'send_json';
1945
1946
	$context['json_data'] = null;
1947
}
1948
1949
function setPWACacheStale($refresh = false)
1950
{
1951
	global $modSettings;
1952
1953
	// We need a PWA cache stale to keep things moving, changing this will trigger a PWA cache flush
1954
	if (empty($modSettings['elk_pwa_cache_stale']) || $refresh)
1955
	{
1956
		$tokenizer = new TokenHash();
1957
		$elk_pwa_cache_stale = $tokenizer->generate_hash(8);
1958
		updateSettings(['elk_pwa_cache_stale' => $elk_pwa_cache_stale]);
1959 229
	}
1960
}
1961
1962 229
/**
1963
 * Send a 1x1 GIF response and terminate the script execution
1964
 *
1965
 * @param bool $expired Flag to determine if header Expires should be sent
1966
 *
1967 229
 * @return void
1968
 */
1969
function dieGif($expired = false)
1970
{
1971
	// The following is an attempt at stopping the behavior identified in #2391
1972
	if (function_exists('fastcgi_finish_request'))
1973
	{
1974
		die();
1975
	}
1976
1977
	$headers = Headers::instance();
1978
	if ($expired)
1979
	{
1980
		$headers
1981
			->header('Expires', 'Mon, 26 Jul 1997 05:00:00 GMT')
1982
			->header('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT');
1983
	}
1984
1985
	$headers->contentType('image/gif')->sendHeaders();
1986
	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");
1987
}
1988 24
1989 24
/**
1990
 * Prepare ob_start with or without gzip compression
1991 24
 *
1992
 * @param bool $use_compression Starts compressed headers.
1993 2
 */
1994
function obStart($use_compression = false)
1995
{
1996 24
	// This is done to clear any output that was made before now.
1997
	while (ob_get_level() > 0)
1998
	{
1999
		@ob_end_clean();
2000
	}
2001
2002
	if ($use_compression)
2003
	{
2004
		ob_start('ob_gzhandler');
2005
	}
2006
	else
2007
	{
2008
		ob_start();
2009
		Headers::instance()->header('Content-Encoding', 'none');
2010
	}
2011
}
2012
2013
/**
2014
 * Returns a URL based on the parameters passed and the selected generator
2015
 *
2016
 * @param string $type The type of the URL (depending on the type, the
2017
 *                     generator can act differently
2018
 * @param array $params All the parameters of the URL
2019
 *
2020
 * @return string An URL
2021
 */
2022
function getUrl($type, $params)
2023
{
2024
	static $generator = null;
2025
2026
	if ($generator === null)
2027 337
	{
2028
		$generator = initUrlGenerator();
2029
	}
2030
2031
	return $generator->get($type, $params);
2032
}
2033
2034
/**
2035
 * Returns the query part of a URL based on the parameters passed and the selected generator
2036
 *
2037
 * @param string $type The type of the URL (depending on the type, the
2038
 *                     generator can act differently
2039
 * @param array $params All the parameters of the URL
2040
 *
2041
 * @return string The query part of an URL
2042
 */
2043
function getUrlQuery($type, $params)
2044
{
2045
	static $generator = null;
2046
2047
	if ($generator === null)
2048
	{
2049
		$generator = initUrlGenerator();
2050
	}
2051
2052
	return $generator->getQuery($type, $params);
2053
}
2054
2055
/**
2056
 * Initialize the URL generator
2057
 *
2058
 * @return object The URL generator object
2059
 */
2060
function initUrlGenerator()
2061
{
2062
	global $scripturl, $context, $url_format;
2063
2064
	$generator = new UrlGenerator([
2065
		'generator' => ucfirst($url_format ?? 'standard'),
2066
		'scripturl' => $scripturl,
2067
		'replacements' => [
2068
			'{session_data}' => isset($context['session_var']) ? $context['session_var'] . '=' . $context['session_id'] : ''
2069
		]
2070
	]);
2071
2072
	$generator->register('Topic');
2073
	$generator->register('Board');
2074
	$generator->register('Profile');
2075
2076
	return $generator;
2077
}
2078
2079
/**
2080
 * This function only checks if a certain feature (in core features)
2081
 * is enabled or not.
2082
 *
2083
 * @param string $feature The abbreviated code of a core feature
2084
 * @return bool true/false for enabled/disabled
2085
 */
2086
function featureEnabled($feature)
2087
{
2088 41
	global $modSettings, $context;
2089
	static $features = null;
2090 41
2091
	if ($features === null)
2092 1
	{
2093
		// This allows us to change the way things look for the admin.
2094
		$features = explode(',', $modSettings['admin_features'] ?? 'cd,cp,k,w,rg,ml,pm');
2095 41
2096
		// @deprecated since 2.0 - Next line is just for backward compatibility to remove before release
2097
		$context['admin_features'] = $features;
2098
	}
2099
2100
	return in_array($feature, $features, true);
2101
}
2102
2103
/**
2104
 * Clean up the XML to make sure it doesn't contain invalid characters.
2105
 *
2106
 * What it does:
2107
 *
2108
 * - Removes invalid XML characters to assure the input string being parsed properly.
2109
 *
2110
 * @param string $string The string to clean
2111
 *
2112
 * @return string The clean string
2113
 */
2114
function cleanXml($string)
2115
{
2116
	// https://www.w3.org/TR/2008/REC-xml-20081126/#NT-Char
2117
	$string = preg_replace('~[\x00-\x08\x0B\x0C\x0E-\x1F\x{FFFE}\x{FFFF}]~u', '', $string);
2118
2119
	// Discouraged
2120
	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);
2121
}
2122
2123
/**
2124
 * Validates a IPv6 address. returns true if it is ipv6.
2125
 *
2126 2
 * @param string $ip ip address to be validated
2127
 *
2128 2
 * @return bool true|false
2129 2
 */
2130 2
function isValidIPv6($ip)
2131
{
2132 2
	return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
2133
}
2134
2135
/**
2136 2
 * Converts IPv6s to numbers.  These make ban checks much easier.
2137 2
 *
2138 2
 * @param string $ip ip address to be converted
2139
 *
2140 2
 * @return int[] array
2141
 */
2142
function convertIPv6toInts($ip)
2143
{
2144
	static $expanded = array();
2145
2146
	// Check if we have done this already.
2147
	if (isset($expanded[$ip]))
2148
	{
2149
		return $expanded[$ip];
2150
	}
2151
2152 17
	// Expand the IP out.
2153 17
	$expanded_ip = explode(':', expandIPv6($ip));
2154
2155 17
	$new_ip = array();
2156
	foreach ($expanded_ip as $int)
2157
	{
2158 1
		$new_ip[] = hexdec($int);
2159 1
	}
2160
2161
	// Save this in case of repeated use.
2162 1
	$expanded[$ip] = $new_ip;
2163
2164
	return $expanded[$ip];
2165 17
}
2166
2167
/**
2168
 * Expands a IPv6 address to its full form.
2169
 *
2170
 * @param string $addr ipv6 address string
2171
 * @param bool $strict_check checks length to expanded address for compliance
2172
 *
2173
 * @return bool|string expanded ipv6 address.
2174
 */
2175
function expandIPv6($addr, $strict_check = true)
2176
{
2177
	static $converted = array();
2178
2179
	// Check if we have done this already.
2180
	if (isset($converted[$addr]))
2181
	{
2182
		return $converted[$addr];
2183
	}
2184
2185
	// Check if there are segments missing, insert if necessary.
2186
	if (strpos($addr, '::') !== false)
2187
	{
2188
		$part = explode('::', $addr);
2189
		$part[0] = explode(':', $part[0]);
2190
		$part[1] = explode(':', $part[1]);
2191
		$missing = array();
2192
2193
		// Looks like this is an IPv4 address
2194
		if (isset($part[1][1]) && strpos($part[1][1], '.') !== false)
2195 3
		{
2196
			$ipoct = explode('.', $part[1][1]);
2197
			$p1 = dechex($ipoct[0]) . dechex($ipoct[1]);
2198
			$p2 = dechex($ipoct[2]) . dechex($ipoct[3]);
2199
2200
			$part[1] = array(
2201
				$part[1][0],
2202
				$p1,
2203
				$p2
2204
			);
2205
		}
2206
2207
		$limit = count($part[0]) + count($part[1]);
2208
		for ($i = 0; $i < (8 - $limit); $i++)
2209
		{
2210
			$missing[] = '0000';
2211
		}
2212
2213
		$part = array_merge($part[0], $missing, $part[1]);
2214
	}
2215
	else
2216
	{
2217
		$part = explode(':', $addr);
2218
	}
2219
2220
	// Pad each segment until it has 4 digits.
2221
	foreach ($part as &$p)
2222
	{
2223
		while (strlen($p) < 4)
2224
		{
2225
			$p = '0' . $p;
2226
		}
2227
	}
2228
2229
	unset($p);
2230
2231
	// Join segments.
2232
	$result = implode(':', $part);
2233
2234
	// Save this in case of repeated use.
2235
	$converted[$addr] = $result;
2236
2237
	// Quick check to make sure the length is as expected.
2238
	if (!$strict_check || strlen($result) == 39)
2239
	{
2240
		return $result;
2241
	}
2242
2243
	return false;
2244
}
2245
2246
/**
2247
 * Removed in 2.0, always returns false.
2248
 *
2249
 * Logs the depreciation notice, returns false, sets context value such that
2250
 * old themes don't go sour ;)
2251
 *
2252
 * @param string $browser the browser we are checking for.
2253
 */
2254
function isBrowser($browser)
2255
{
2256
	global $context;
2257
2258
	\ElkArte\Errors::instance()->log_deprecated('isBrowser()', 'Nothing');
2259
2260
	$context['browser_body_id'] = 'elkarte';
2261
2262
	return false;
2263
}
2264