Completed
Pull Request — development (#3246)
by Emanuele
16:38
created

Subs.php ➔ getUrl()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 2
dl 0
loc 10
ccs 3
cts 4
cp 0.75
crap 2.0625
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to, well, everything.
5
 *
6
 * @name      ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
9
 *
10
 * This file contains code covered by:
11
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
12
 * license:		BSD, See included LICENSE.TXT for terms and conditions.
13
 *
14
 * @version 2.0 dev
15
 *
16
 */
17
18
/**
19
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
20
 *
21
 * What it does:
22
 *
23
 * - Updates both the settings table and $modSettings array.
24
 * - All of changeArray's indexes and values are assumed to have escaped apostrophes (')!
25
 * - If a variable is already set to what you want to change it to, that
26
 *   Variable will be skipped over; it would be unnecessary to reset.
27
 * - When update is true, UPDATEs will be used instead of REPLACE.
28
 * - When update is true, the value can be true or false to increment
29
 *  or decrement it, respectively.
30
 *
31
 * @param mixed[] $changeArray An associative array of what we're changing in 'setting' => 'value' format
32
 * @param bool $update Use an UPDATE query instead of a REPLACE query
33
 */
34
function updateSettings($changeArray, $update = false)
35
{
36 25
	global $modSettings;
37
38 25
	$db = database();
39 25
	$cache = Cache::instance();
40
41 25
	if (empty($changeArray) || !is_array($changeArray))
42
		return;
43
44
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
45 25
	if ($update)
46
	{
47 13
		foreach ($changeArray as $variable => $value)
48
		{
49 13
			$db->query('', '
50
				UPDATE {db_prefix}settings
51 13
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
52
				WHERE variable = {string:variable}',
53
				array(
54 13
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
55 13
					'variable' => $variable,
56
				)
57
			);
58
59 13
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
60
		}
61
62
		// Clean out the cache and make sure the cobwebs are gone too.
63 13
		$cache->remove('modSettings');
64
65 13
		return;
66
	}
67
68 21
	$replaceArray = array();
69 21
	foreach ($changeArray as $variable => $value)
70
	{
71
		// Don't bother if it's already like that ;).
72 21
		if (isset($modSettings[$variable]) && $modSettings[$variable] == $value)
73 15
			continue;
74
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
75 17
		elseif (!isset($modSettings[$variable]) && empty($value))
76
			continue;
77
78 17
		$replaceArray[] = array($variable, $value);
79
80 17
		$modSettings[$variable] = $value;
81
	}
82
83 21
	if (empty($replaceArray))
84 9
		return;
85
86 17
	$db->insert('replace',
87 17
		'{db_prefix}settings',
88 17
		array('variable' => 'string-255', 'value' => 'string-65534'),
89 17
		$replaceArray,
90 17
		array('variable')
91
	);
92
93
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
94 17
	$cache->remove('modSettings');
95 17
}
96
97
/**
98
 * Deletes one setting from the settings table and takes care of $modSettings as well
99
 *
100
 * @param string|string[] $toRemove the setting or the settings to be removed
101
 */
102
function removeSettings($toRemove)
103
{
104 2
	global $modSettings;
105
106 2
	$db = database();
107
108 2
	if (empty($toRemove))
109
		return;
110
111 2
	if (!is_array($toRemove))
112 1
		$toRemove = array($toRemove);
113
114
	// Remove the setting from the db
115 2
	$db->query('', '
116
		DELETE FROM {db_prefix}settings
117
		WHERE variable IN ({array_string:setting_name})',
118
		array(
119 2
			'setting_name' => $toRemove,
120
		)
121
	);
122
123
	// Remove it from $modSettings now so it does not persist
124 2
	foreach ($toRemove as $setting)
125 2
		if (isset($modSettings[$setting]))
126 2
			unset($modSettings[$setting]);
127
128
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
129 2
	Cache::instance()->remove('modSettings');
130 2
}
131
132
/**
133
 * Constructs a page list.
134
 *
135
 * What it does:
136
 *
137
 * - Builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15.
138
 * - Flexible_start causes it to use "url.page" instead of "url;start=page".
139
 * - Very importantly, cleans up the start value passed, and forces it to
140
 *   be a multiple of num_per_page.
141
 * - Checks that start is not more than max_value.
142
 * - Base_url should be the URL without any start parameter on it.
143
 * - Uses the compactTopicPagesEnable and compactTopicPagesContiguous
144
 *   settings to decide how to display the menu.
145
 *
146
 * @example is available near the function definition.
147
 * @example $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages,
148
 *     $maxindex, true);
149
 *
150
 * @param string $base_url The base URL to be used for each link.
151
 * @param int &$start The start position, by reference. If this is not a multiple
152
 * of the number of items per page, it is sanitized to be so and the value will persist upon the function's return.
153
 * @param int $max_value The total number of items you are paginating for.
154
 * @param int $num_per_page The number of items to be displayed on a given page.
155
 * @param bool $flexible_start = false Use "url.page" instead of "url;start=page"
156
 * @param mixed[] $show associative array of option => boolean paris
157
 *
158
 * @return string
159
 */
160
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show = array())
161
{
162 2
	global $modSettings, $context, $txt, $settings, $scripturl;
163
164
	// Save whether $start was less than 0 or not.
165 2
	$start = (int) $start;
166 2
	$start_invalid = $start < 0;
167
	$show_defaults = array(
168 2
		'prev_next' => true,
169
		'all' => false,
170
	);
171
172 2
	$show = array_merge($show_defaults, $show);
173
174
	// Make sure $start is a proper variable - not less than 0.
175 2
	if ($start_invalid)
176
		$start = 0;
177
	// Not greater than the upper bound.
178 2
	elseif ($start >= $max_value)
179
		$start = max(0, (int) $max_value - (((int) $max_value % (int) $num_per_page) == 0 ? $num_per_page : ((int) $max_value % (int) $num_per_page)));
180
	// And it has to be a multiple of $num_per_page!
181
	else
182 2
		$start = max(0, (int) $start - ((int) $start % (int) $num_per_page));
183
184 2
	$context['current_page'] = $start / $num_per_page;
185
186 2
	$base_link = str_replace('{base_link}', ($flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'), $settings['page_index_template']['base_link']);
187
188
	// Compact pages is off or on?
189 2
	if (empty($modSettings['compactTopicPagesEnable']))
190
	{
191
		// Show the left arrow.
192
		$pageindex = $start == 0 || !$show['prev_next'] ? ' ' : sprintf($base_link, $start - $num_per_page, str_replace('{prev_txt}', $txt['prev'], $settings['page_index_template']['previous_page']));
193
194
		// Show all the pages.
195
		$display_page = 1;
196
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
197
			$pageindex .= $start == $counter && !$start_invalid && empty($show['all_selected']) ? sprintf($settings['page_index_template']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
198
199
		// Show the right arrow.
200
		$display_page = ($start + $num_per_page) > $max_value ? $max_value : ($start + $num_per_page);
201
		if ($start != $counter - $max_value && !$start_invalid && $show['prev_next'] && empty($show['all_selected']))
202
			$pageindex .= $display_page > $counter - $num_per_page ? ' ' : sprintf($base_link, $display_page, str_replace('{next_txt}', $txt['next'], $settings['page_index_template']['next_page']));
203
	}
204
	else
205
	{
206
		// If they didn't enter an odd value, pretend they did.
207 2
		$PageContiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
208
209
		// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
210 2
		if (!empty($start) && $show['prev_next'])
211
			$pageindex = sprintf($base_link, $start - $num_per_page, str_replace('{prev_txt}', $txt['prev'], $settings['page_index_template']['previous_page']));
212
		else
213 2
			$pageindex = '';
214
215
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
216 2
		if ($start > $num_per_page * $PageContiguous)
217
			$pageindex .= sprintf($base_link, 0, '1');
218
219
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
220 2
		if ($start > $num_per_page * ($PageContiguous + 1))
221
		{
222
			$pageindex .= str_replace(
223
				'{custom}',
224
				'data-baseurl="' . htmlspecialchars(
225
					JavaScriptEscape(
226
						strtr(
227
							$flexible_start ? $base_url : strtr($base_url, ['%' => '%%']) . ';start=%1$d',
228
							[$scripturl => '']
229
						)
230
					),
231
					ENT_COMPAT,
232
					'UTF-8'
233
				) . '" data-perpage="' . $num_per_page . '" data-firstpage="' . $num_per_page . '" data-lastpage="' . ($start - $num_per_page * $PageContiguous) . '"',
234
				$settings['page_index_template']['expand_pages']
235
			);
236
		}
237
238
		// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
239 2
		for ($nCont = $PageContiguous; $nCont >= 1; $nCont--)
240 2 View Code Duplication
			if ($start >= $num_per_page * $nCont)
241
			{
242
				$tmpStart = $start - $num_per_page * $nCont;
243
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
244
			}
245
246
		// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
247 2
		if (!$start_invalid && empty($show['all_selected']))
248 2
			$pageindex .= sprintf($settings['page_index_template']['current_page'], ($start / $num_per_page + 1));
249
		else
250
			$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
251
252
		// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
253 2
		$tmpMaxPages = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
254 2
		for ($nCont = 1; $nCont <= $PageContiguous; $nCont++)
255 2 View Code Duplication
			if ($start + $num_per_page * $nCont <= $tmpMaxPages)
256
			{
257
				$tmpStart = $start + $num_per_page * $nCont;
258
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
259
			}
260
261
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
262 2
		if ($start + $num_per_page * ($PageContiguous + 1) < $tmpMaxPages)
263
		{
264
			$pageindex .= str_replace(
265
				'{custom}',
266
				'data-baseurl="' . htmlspecialchars(
267
					JavaScriptEscape(
268
						strtr(
269
							$flexible_start ? $base_url : strtr($base_url, ['%' => '%%']) . ';start=%1$d',
270
							[$scripturl => '']
271
						)
272
					),
273
					ENT_COMPAT,
274
					'UTF-8'
275
				) . '" data-perpage="' . $num_per_page . '" data-firstpage="' . ($start + $num_per_page * ($PageContiguous + 1)) . '" data-lastpage="' . $tmpMaxPages . '"',
276
				$settings['page_index_template']['expand_pages']
277
			);
278
		}
279
280
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
281 2
		if ($start + $num_per_page * $PageContiguous < $tmpMaxPages)
282
			$pageindex .= sprintf($base_link, $tmpMaxPages, $tmpMaxPages / $num_per_page + 1);
283
284
		// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
285 2
		if ($start != $tmpMaxPages && $show['prev_next'] && empty($show['all_selected']))
286
			$pageindex .= sprintf($base_link, $start + $num_per_page, str_replace('{next_txt}', $txt['next'], $settings['page_index_template']['next_page']));
287
	}
288
289
	// The "all" button
290 2
	if ($show['all'])
291
	{
292
		if (!empty($show['all_selected']))
293
			$pageindex .= sprintf($settings['page_index_template']['current_page'], $txt['all']);
294
		else
295
			$pageindex .= sprintf(str_replace('%1$d', '%1$s', $base_link), '0;all', str_replace('{all_txt}', $txt['all'], $settings['page_index_template']['all']));
296
	}
297
298 2
	return $pageindex;
299
}
300
301
/**
302
 * Formats a number.
303
 *
304
 * What it does:
305
 *
306
 * - Uses the format of number_format to decide how to format the number.
307
 *   for example, it might display "1 234,50".
308
 * - Caches the formatting data from the setting for optimization.
309
 *
310
 * @param float $number The float value to apply comma formatting
311
 * @param integer|bool $override_decimal_count = false or number of decimals
312
 *
313
 * @return string
314
 */
315
function comma_format($number, $override_decimal_count = false)
316
{
317 2
	global $txt;
318 2
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
319
320
	// Cache these values...
321 2
	if ($decimal_separator === null)
322
	{
323
		// Not set for whatever reason?
324 1
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
325
			return $number;
326
327
		// Cache these each load...
328 1
		$thousands_separator = $matches[1];
329 1
		$decimal_separator = $matches[2];
330 1
		$decimal_count = strlen($matches[3]);
331
	}
332
333
	// Format the string with our friend, number_format.
334 2
	return number_format($number, (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
335
}
336
337
/**
338
 * Formats a number to a multiple of thousands x, x k, x M, x G, x T
339
 *
340
 * @param float $number The value to format
341
 * @param integer|bool $override_decimal_count = false or number of decimals
342
 *
343
 * @return string
344
 */
345
function thousands_format($number, $override_decimal_count = false)
346
{
347
	foreach (array('', ' k', ' M', ' G', ' T') as $kb)
348
	{
349
		if ($number < 1000)
350
		{
351
			break;
352
		}
353
354
		$number /= 1000;
355
	}
356
357
	return comma_format($number, $override_decimal_count) . $kb;
0 ignored issues
show
Bug introduced by
The variable $kb seems to be defined by a foreach iteration on line 347. Are you sure the iterator is never empty, otherwise this variable is not defined?

It seems like you are relying on a variable being defined by an iteration:

foreach ($a as $b) {
}

// $b is defined here only if $a has elements, for example if $a is array()
// then $b would not be defined here. To avoid that, we recommend to set a
// default value for $b.


// Better
$b = 0; // or whatever default makes sense in your context
foreach ($a as $b) {
}

// $b is now guaranteed to be defined here.
Loading history...
358
}
359
360
/**
361
 * Formats a number to a computer byte size value xB, xKB, xMB, xGB
362
 *
363
 * @param int $number
364
 *
365
 * @return string
366
 */
367
function byte_format($number)
368
{
369
	global $txt;
370
371
	$kb = '';
372
	foreach (array('byte', 'kilobyte', 'megabyte', 'gigabyte') as $kb)
373
	{
374
		if ($number < 1024)
375
		{
376
			break;
377
		}
378
379
		$number /= 1024;
380
	}
381
382
	return comma_format($number) . ' ' . $txt[$kb];
383
}
384
385
/**
386
 * Format a time to make it look purdy.
387
 *
388
 * What it does:
389
 *
390
 * - Returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
391
 * - Applies all necessary time offsets to the timestamp, unless offset_type is set.
392
 * - If todayMod is set and show_today was not not specified or true, an
393
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
394
 * - Performs localization (more than just strftime would do alone.)
395
 *
396
 * @param int $log_time A unix timestamp
397
 * @param string|bool $show_today = true show "Today"/"Yesterday",
398
 *   false shows the date, a string can force a date format to use %b %d, %Y
399
 * @param string|bool $offset_type = false If false, uses both user time offset and forum offset.
400
 *   If 'forum', uses only the forum offset. Otherwise no offset is applied.
401
 *
402
 * @return string
403
 */
404
function standardTime($log_time, $show_today = true, $offset_type = false)
405
{
406 6
	global $user_info, $txt, $modSettings;
407 6
	static $non_twelve_hour, $is_win = null;
408
409 6
	if ($is_win === null)
410
	{
411 1
		$is_win = detectServer()->is('windows');
412
	}
413
414
	// Offset the time.
415 6
	if (!$offset_type)
416 3
		$time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
417
	// Just the forum offset?
418 5
	elseif ($offset_type === 'forum')
419
		$time = $log_time + $modSettings['time_offset'] * 3600;
420
	else
421 5
		$time = $log_time;
422
423
	// We can't have a negative date (on Windows, at least.)
424 6
	if ($log_time < 0)
425
		$log_time = 0;
426
427
	// Today and Yesterday?
428 6
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
429
	{
430
		// Get the current time.
431 6
		$nowtime = forum_time();
432
433 6
		$then = @getdate($time);
434 6
		$now = @getdate($nowtime);
435
436
		// Try to make something of a time format string...
437 6
		$s = strpos($user_info['time_format'], '%S') === false ? '' : ':%S';
438 6
		if (strpos($user_info['time_format'], '%H') === false && strpos($user_info['time_format'], '%T') === false)
439
		{
440 6
			$h = strpos($user_info['time_format'], '%l') === false ? '%I' : '%l';
441 6
			$today_fmt = $h . ':%M' . $s . ' %p';
442
		}
443
		else
444
			$today_fmt = '%H:%M' . $s;
445
446
		// Same day of the year, same year.... Today!
447 6
		if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
448 3
			return sprintf($txt['today'], standardTime($log_time, $today_fmt, $offset_type));
449
450
		// 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...
451 3
		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))
452
			return sprintf($txt['yesterday'], standardTime($log_time, $today_fmt, $offset_type));
453
	}
454
455 6
	$str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
456
457
	// Windows requires a slightly different language code identifier (LCID).
458
	// https://msdn.microsoft.com/en-us/library/cc233982.aspx
459 6
	if ($is_win)
460
	{
461
		$txt['lang_locale'] = strtr($txt['lang_locale'], '_', '-');
462
	}
463
464 6
	if (setlocale(LC_TIME, $txt['lang_locale']))
465
	{
466
		if (!isset($non_twelve_hour))
467
			$non_twelve_hour = trim(strftime('%p')) === '';
468 View Code Duplication
		if ($non_twelve_hour && strpos($str, '%p') !== false)
469
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
470
471
		foreach (array('%a', '%A', '%b', '%B') as $token)
472
			if (strpos($str, $token) !== false)
473
				$str = str_replace($token, !empty($txt['lang_capitalize_dates']) ? Util::ucwords(strftime($token, $time)) : strftime($token, $time), $str);
474
	}
475
	else
476
	{
477
		// Do-it-yourself time localization.  Fun.
478 6
		foreach (array('%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months') as $token => $text_label)
479 6
			if (strpos($str, $token) !== false)
480 6
				$str = str_replace($token, $txt[$text_label][(int) strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
481
482 6 View Code Duplication
		if (strpos($str, '%p') !== false)
483 3
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
484
	}
485
486
	// Windows doesn't support %e; on some versions, strftime fails altogether if used, so let's prevent that.
487 6
	if ($is_win && strpos($str, '%e') !== false)
488
		$str = str_replace('%e', ltrim(strftime('%d', $time), '0'), $str);
489
490
	// Format any other characters..
491 6
	return strftime($str, $time);
492
}
493
494
/**
495
 * Used to render a timestamp to html5 <time> tag format.
496
 *
497
 * @param int $timestamp A unix timestamp
498
 *
499
 * @return string
500
 */
501
function htmlTime($timestamp)
502
{
503 5
	global $txt, $context;
504
505 5
	if (empty($timestamp))
506
		return '';
507
508 5
	$timestamp = forum_time(true, $timestamp);
509 5
	$time = date('Y-m-d H:i', $timestamp);
510 5
	$stdtime = standardTime($timestamp, true, true);
511
512
	// @todo maybe htmlspecialchars on the title attribute?
513 5
	return '<time title="' . (!empty($context['using_relative_time']) ? $stdtime : $txt['last_post']) . '" datetime="' . $time . '" data-timestamp="' . $timestamp . '">' . $stdtime . '</time>';
514
}
515
516
/**
517
 * Gets the current time with offset.
518
 *
519
 * What it does:
520
 *
521
 * - Always applies the offset in the time_offset setting.
522
 *
523
 * @param bool $use_user_offset = true if use_user_offset is true, applies the user's offset as well
524
 * @param int|null $timestamp = null A unix timestamp (null to use current time)
525
 *
526
 * @return int seconds since the unix epoch
527
 */
528
function forum_time($use_user_offset = true, $timestamp = null)
529
{
530 7
	global $user_info, $modSettings;
531
532 7
	if ($timestamp === null)
533 7
		$timestamp = time();
534 6
	elseif ($timestamp == 0)
535
		return 0;
536
537 7
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
538
}
539
540
/**
541
 * Removes special entities from strings.  Compatibility...
542
 *
543
 * - Faster than html_entity_decode
544
 * - Removes the base entities ( &amp; &quot; &#039; &lt; and &gt;. ) from text with htmlspecialchars_decode
545
 * - Additionally converts &nbsp with str_replace
546
 *
547
 * @param string $string The string to apply htmlspecialchars_decode
548
 *
549
 * @return string string without entities
550
 */
551
function un_htmlspecialchars($string)
552
{
553 22
	$string = htmlspecialchars_decode($string, ENT_QUOTES);
554 22
	$string = str_replace('&nbsp;', ' ', $string);
555
556 22
	return $string;
557
}
558
559
/**
560
 * Lexicographic permutation function.
561
 *
562
 * This is a special type of permutation which involves the order of the set. The next
563
 * lexicographic permutation of '32541' is '34125'. Numerically, it is simply the smallest
564
 * set larger than the current one.
565
 *
566
 * The benefit of this over a recursive solution is that the whole list does NOT need
567
 * to be held in memory. So it's actually possible to run 30! permutations without
568
 * causing a memory overflow.
569
 *
570
 * Source: O'Reilly PHP Cookbook
571
 *
572
 * @param mixed[] $p The array keys to apply permutation
573
 * @param int $size The size of our permutation array
574
 *
575
 * @return mixed[] the next permutation of the passed array $p
576
 */
577
function pc_next_permutation($p, $size)
578
{
579
	// Slide down the array looking for where we're smaller than the next guy
580 2
	for ($i = $size - 1; isset($p[$i]) && $p[$i] >= $p[$i + 1]; --$i)
581
	{
582
	}
583
584
	// If this doesn't occur, we've finished our permutations
585
	// the array is reversed: (1, 2, 3, 4) => (4, 3, 2, 1)
586 2
	if ($i === -1)
587
	{
588 2
		return false;
589
	}
590
591
	// Slide down the array looking for a bigger number than what we found before
592 2
	for ($j = $size; $p[$j] <= $p[$i]; --$j)
593
	{
594
	}
595
596
	// Swap them
597 2
	$tmp = $p[$i];
598 2
	$p[$i] = $p[$j];
599 2
	$p[$j] = $tmp;
600
601
	// Now reverse the elements in between by swapping the ends
602 2
	for (++$i, $j = $size; $i < $j; ++$i, --$j)
603
	{
604 2
		$tmp = $p[$i];
605 2
		$p[$i] = $p[$j];
606 2
		$p[$j] = $tmp;
607
	}
608
609 2
	return $p;
610
}
611
612
/**
613
 * Highlight any code.
614
 *
615
 * What it does:
616
 *
617
 * - Uses PHP's highlight_string() to highlight PHP syntax
618
 * - does special handling to keep the tabs in the code available.
619
 * - used to parse PHP code from inside [code] and [php] tags.
620
 *
621
 * @param string $code The string containing php code
622
 *
623
 * @return string the code with highlighted HTML.
624
 */
625
function highlight_php_code($code)
626
{
627
	// Remove special characters.
628
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", "\t" => '___TAB();', '&#91;' => '[')));
629
630
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
631
632
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
633
	$buffer = preg_replace('~___TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
634
635
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
636
}
637
638
/**
639
 * Ends execution and redirects the user to a new location
640
 *
641
 * What it does:
642
 *
643
 * - Makes sure the browser doesn't come back and repost the form data.
644
 * - Should be used whenever anything is posted.
645
 * - Calls AddMailQueue to process any mail queue items its can
646
 * - Calls call_integration_hook integrate_redirect before headers are sent
647
 * - Diverts final execution to obExit() which means a end to processing and sending of final output
648
 *
649
 * @event integrate_redirect called before headers are sent
650
 * @param string $setLocation = '' The URL to redirect to
651
 * @param bool $refresh = false, enable to send a refresh header, default is a location header
652
 * @throws Elk_Exception
653
 */
654
function redirectexit($setLocation = '', $refresh = false)
655
{
656
	global $scripturl, $context, $modSettings, $db_show_debug;
657
658
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
659
	if (!empty($context['flush_mail']))
660
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
661
		AddMailQueue(true);
662
663
	Notifications::instance()->send();
664
665
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
666
667
	if ($add)
668
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
669
670
	// Put the session ID in.
671 View Code Duplication
	if (empty($_COOKIE) && defined('SID') && SID != '')
672
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
673
	// Keep that debug in their for template debugging!
674
	elseif (isset($_GET['debug']))
675
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
676
677
	if (!empty($modSettings['queryless_urls']) && detectServer()->supportRewrite())
678
	{
679
		if (defined('SID') && SID != '')
680
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~', 'redirectexit_callback', $setLocation);
681
		else
682
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~', 'redirectexit_callback', $setLocation);
683
	}
684
685
	// Maybe integrations want to change where we are heading?
686
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh));
687
688
	// We send a Refresh header only in special cases because Location looks better. (and is quicker...)
689
	if ($refresh)
690
		header('Refresh: 0; URL=' . strtr($setLocation, array(' ' => '%20')));
0 ignored issues
show
Security Response Splitting introduced by
'Refresh: 0; URL=' . str...n, array(' ' => '%20')) can contain request data and is used in response header context(s) leading to a potential security vulnerability.

8 paths for user data to reach this point

  1. Path: Fetching key HTTP_REFERER from $_SERVER, and $_SERVER['HTTP_REFERER'] is passed to redirectexit() in sources/admin/Admin.controller.php on line 984
  1. Fetching key HTTP_REFERER from $_SERVER, and $_SERVER['HTTP_REFERER'] is passed to redirectexit()
    in sources/admin/Admin.controller.php on line 984
  2. $setLocation is passed through strtr()
    in sources/Subs.php on line 690
  2. Path: Read from $_REQUEST, and 'action=pm;f=' . $_REQUEST['f'] . ';start=' . $this->_req->start . (isset($_REQUEST['l']) ? ';l=' . $this->_req->get('l', 'intval') : '') . (isset($_REQUEST['pm']) ? '#' . $this->_req->get('pm', 'intval') : '') is passed to redirectexit() in sources/controllers/Karma.controller.php on line 181
  1. Read from $_REQUEST, and 'action=pm;f=' . $_REQUEST['f'] . ';start=' . $this->_req->start . (isset($_REQUEST['l']) ? ';l=' . $this->_req->get('l', 'intval') : '') . (isset($_REQUEST['pm']) ? '#' . $this->_req->get('pm', 'intval') : '') is passed to redirectexit()
    in sources/controllers/Karma.controller.php on line 181
  2. $setLocation is passed through strtr()
    in sources/Subs.php on line 690
  3. Path: Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit() in sources/controllers/Post.controller.php on line 1135
  1. Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit()
    in sources/controllers/Post.controller.php on line 1135
  2. $setLocation is passed through strtr()
    in sources/Subs.php on line 690
  4. Path: Read from $_REQUEST, and $_REQUEST['search'] is passed through explode(), and explode(' ', $_REQUEST['search']) is passed through implode(), and implode($engine['separator'], explode(' ', $_REQUEST['search'])) is escaped by urlencode() for all (url-encoded) context(s), and $engine['url'] . urlencode(implode($engine['separator'], explode(' ', $_REQUEST['search']))) is passed to redirectexit() in sources/controllers/Search.controller.php on line 61
  1. Read from $_REQUEST, and $_REQUEST['search'] is passed through explode(), and explode(' ', $_REQUEST['search']) is passed through implode(), and implode($engine['separator'], explode(' ', $_REQUEST['search'])) is escaped by urlencode() for all (url-encoded) context(s), and $engine['url'] . urlencode(implode($engine['separator'], explode(' ', $_REQUEST['search']))) is passed to redirectexit()
    in sources/controllers/Search.controller.php on line 61
  2. $setLocation is passed through strtr()
    in sources/Subs.php on line 690
  5. Path: Read from $_REQUEST, and $_REQUEST['search'] is escaped by urlencode() for all (url-encoded) context(s), and 'action=memberlist;sa=search;fields=name,email;search=' . urlencode($_REQUEST['search']) is passed to redirectexit() in sources/controllers/Search.controller.php on line 68
  1. Read from $_REQUEST, and $_REQUEST['search'] is escaped by urlencode() for all (url-encoded) context(s), and 'action=memberlist;sa=search;fields=name,email;search=' . urlencode($_REQUEST['search']) is passed to redirectexit()
    in sources/controllers/Search.controller.php on line 68
  2. $setLocation is passed through strtr()
    in sources/Subs.php on line 690
  6. Path: Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit() in sources/Load.php on line 475
  1. Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit()
    in sources/Load.php on line 475
  2. $setLocation is passed through strtr()
    in sources/Subs.php on line 690
  7. Path: Read from $_GET, and $_GET is passed through key(), and 'wwwRedirect;' . key($_GET) . '=' . current($_GET) is passed to redirectexit() in sources/subs/Themes/ThemeLoader.php on line 541
  1. Read from $_GET, and $_GET is passed through key(), and 'wwwRedirect;' . key($_GET) . '=' . current($_GET) is passed to redirectexit()
    in sources/subs/Themes/ThemeLoader.php on line 541
  2. $setLocation is passed through strtr()
    in sources/Subs.php on line 690
  8. Path: Read from $_POST, and $_POST is passed to Data_Validator::is_valid() in sources/controllers/Post.controller.php on line 883
  1. Read from $_POST, and $_POST is passed to Data_Validator::is_valid()
    in sources/controllers/Post.controller.php on line 883
  2. $data is passed to Data_Validator::validate()
    in sources/subs/DataValidator.class.php on line 153
  3. Data_Validator::$_data is assigned
    in sources/subs/DataValidator.class.php on line 266
  4. Tainted property Data_Validator::$_data is read
    in sources/subs/DataValidator.class.php on line 303
  5. Data_Validator::validation_data() returns tainted data
    in sources/subs/HttpReq.class.php on line 377
  6. HttpReq::cleanValue() returns tainted data, and HttpReq::$_param is assigned
    in sources/subs/HttpReq.class.php on line 223
  7. Tainted property HttpReq::$_param is read
    in sources/subs/HttpReq.class.php on line 192
  8. HttpReq::__get() returns tainted data, and 'topic=' . $topic . '.' . $this->_req->start . '#msg' . $this->_req->get('m', 'intval') is passed to redirectexit()
    in sources/controllers/Karma.controller.php on line 178
  9. $setLocation is passed through strtr()
    in sources/Subs.php on line 690

Response Splitting Attacks

Allowing an attacker to set a response header, opens your application to response splitting attacks; effectively allowing an attacker to send any response, he would like.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
691
	else
692
		header('Location: ' . str_replace(' ', '%20', $setLocation));
0 ignored issues
show
Security Response Splitting introduced by
'Location: ' . str_repla...', '%20', $setLocation) can contain request data and is used in response header context(s) leading to a potential security vulnerability.

8 paths for user data to reach this point

  1. Path: Fetching key HTTP_REFERER from $_SERVER, and $_SERVER['HTTP_REFERER'] is passed to redirectexit() in sources/admin/Admin.controller.php on line 984
  1. Fetching key HTTP_REFERER from $_SERVER, and $_SERVER['HTTP_REFERER'] is passed to redirectexit()
    in sources/admin/Admin.controller.php on line 984
  2. $setLocation is passed through str_replace()
    in sources/Subs.php on line 692
  2. Path: Read from $_REQUEST, and 'action=pm;f=' . $_REQUEST['f'] . ';start=' . $this->_req->start . (isset($_REQUEST['l']) ? ';l=' . $this->_req->get('l', 'intval') : '') . (isset($_REQUEST['pm']) ? '#' . $this->_req->get('pm', 'intval') : '') is passed to redirectexit() in sources/controllers/Karma.controller.php on line 181
  1. Read from $_REQUEST, and 'action=pm;f=' . $_REQUEST['f'] . ';start=' . $this->_req->start . (isset($_REQUEST['l']) ? ';l=' . $this->_req->get('l', 'intval') : '') . (isset($_REQUEST['pm']) ? '#' . $this->_req->get('pm', 'intval') : '') is passed to redirectexit()
    in sources/controllers/Karma.controller.php on line 181
  2. $setLocation is passed through str_replace()
    in sources/Subs.php on line 692
  3. Path: Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit() in sources/controllers/Post.controller.php on line 1135
  1. Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit()
    in sources/controllers/Post.controller.php on line 1135
  2. $setLocation is passed through str_replace()
    in sources/Subs.php on line 692
  4. Path: Read from $_REQUEST, and $_REQUEST['search'] is passed through explode(), and explode(' ', $_REQUEST['search']) is passed through implode(), and implode($engine['separator'], explode(' ', $_REQUEST['search'])) is escaped by urlencode() for all (url-encoded) context(s), and $engine['url'] . urlencode(implode($engine['separator'], explode(' ', $_REQUEST['search']))) is passed to redirectexit() in sources/controllers/Search.controller.php on line 61
  1. Read from $_REQUEST, and $_REQUEST['search'] is passed through explode(), and explode(' ', $_REQUEST['search']) is passed through implode(), and implode($engine['separator'], explode(' ', $_REQUEST['search'])) is escaped by urlencode() for all (url-encoded) context(s), and $engine['url'] . urlencode(implode($engine['separator'], explode(' ', $_REQUEST['search']))) is passed to redirectexit()
    in sources/controllers/Search.controller.php on line 61
  2. $setLocation is passed through str_replace()
    in sources/Subs.php on line 692
  5. Path: Read from $_REQUEST, and $_REQUEST['search'] is escaped by urlencode() for all (url-encoded) context(s), and 'action=memberlist;sa=search;fields=name,email;search=' . urlencode($_REQUEST['search']) is passed to redirectexit() in sources/controllers/Search.controller.php on line 68
  1. Read from $_REQUEST, and $_REQUEST['search'] is escaped by urlencode() for all (url-encoded) context(s), and 'action=memberlist;sa=search;fields=name,email;search=' . urlencode($_REQUEST['search']) is passed to redirectexit()
    in sources/controllers/Search.controller.php on line 68
  2. $setLocation is passed through str_replace()
    in sources/Subs.php on line 692
  6. Path: Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit() in sources/Load.php on line 475
  1. Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit()
    in sources/Load.php on line 475
  2. $setLocation is passed through str_replace()
    in sources/Subs.php on line 692
  7. Path: Read from $_GET, and $_GET is passed through key(), and 'wwwRedirect;' . key($_GET) . '=' . current($_GET) is passed to redirectexit() in sources/subs/Themes/ThemeLoader.php on line 541
  1. Read from $_GET, and $_GET is passed through key(), and 'wwwRedirect;' . key($_GET) . '=' . current($_GET) is passed to redirectexit()
    in sources/subs/Themes/ThemeLoader.php on line 541
  2. $setLocation is passed through str_replace()
    in sources/Subs.php on line 692
  8. Path: Read from $_POST, and $_POST is passed to Data_Validator::is_valid() in sources/controllers/Post.controller.php on line 883
  1. Read from $_POST, and $_POST is passed to Data_Validator::is_valid()
    in sources/controllers/Post.controller.php on line 883
  2. $data is passed to Data_Validator::validate()
    in sources/subs/DataValidator.class.php on line 153
  3. Data_Validator::$_data is assigned
    in sources/subs/DataValidator.class.php on line 266
  4. Tainted property Data_Validator::$_data is read
    in sources/subs/DataValidator.class.php on line 303
  5. Data_Validator::validation_data() returns tainted data
    in sources/subs/HttpReq.class.php on line 377
  6. HttpReq::cleanValue() returns tainted data, and HttpReq::$_param is assigned
    in sources/subs/HttpReq.class.php on line 223
  7. Tainted property HttpReq::$_param is read
    in sources/subs/HttpReq.class.php on line 192
  8. HttpReq::__get() returns tainted data, and 'topic=' . $topic . '.' . $this->_req->start . '#msg' . $this->_req->get('m', 'intval') is passed to redirectexit()
    in sources/controllers/Karma.controller.php on line 178
  9. $setLocation is passed through str_replace()
    in sources/Subs.php on line 692

Response Splitting Attacks

Allowing an attacker to set a response header, opens your application to response splitting attacks; effectively allowing an attacker to send any response, he would like.

General Strategies to prevent injection

In general, it is advisable to prevent any user-data to reach this point. This can be done by white-listing certain values:

if ( ! in_array($value, array('this-is-allowed', 'and-this-too'), true)) {
    throw new \InvalidArgumentException('This input is not allowed.');
}

For numeric data, we recommend to explicitly cast the data:

$sanitized = (integer) $tainted;
Loading history...
693
694
	// Debugging.
695
	if ($db_show_debug === true)
696
	{
697
		$_SESSION['debug_redirect'] = Debug::instance()->get_db();
698
	}
699
700
	obExit(false);
701
}
702
703
/**
704
 * URL fixer for redirect exit
705
 *
706
 * What it does:
707
 *
708
 * - Similar to the callback function used in ob_sessrewrite
709
 * - Evoked by enabling queryless_urls for systems that support that function
710
 *
711
 * @param mixed[] $matches results from the calling preg
712
 *
713
 * @return string
714
 */
715
function redirectexit_callback($matches)
716
{
717
	global $scripturl;
718
719
	if (defined('SID') && SID != '')
720
		return $scripturl . '/' . strtr($matches[1], '&;=', '//,') . '.html?' . SID . (isset($matches[2]) ? $matches[2] : '');
721 View Code Duplication
	else
722
		return $scripturl . '/' . strtr($matches[1], '&;=', '//,') . '.html' . (isset($matches[2]) ? $matches[2] : '');
723
}
724
725
/**
726
 * Ends execution.
727
 *
728
 * What it does:
729
 *
730
 * - Takes care of template loading and remembering the previous URL.
731
 * - Calls ob_start() with ob_sessrewrite to fix URLs if necessary.
732
 *
733
 * @event integrate_invalid_old_url allows adding to "from" urls we don't save
734
 * @event integrate_exit inform portal, etc. that we're integrated with to exit
735
 * @param bool|null $header = null Output the header
736
 * @param bool|null $do_footer = null Output the footer
737
 * @param bool $from_index = false If we're coming from index.php
738
 * @param bool $from_fatal_error = false If we are exiting due to a fatal error
739
 *
740
 */
741
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
742
{
743
	global $context, $txt, $db_show_debug;
744
745
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
746
747
	// Attempt to prevent a recursive loop.
748
	++$level;
749
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
750
		exit;
751
752
	if ($from_fatal_error)
753
		$has_fatal_error = true;
754
755
	// Clear out the stat cache.
756
	trackStats();
757
758
	Notifications::instance()->send();
759
760
	// If we have mail to send, send it.
761
	if (!empty($context['flush_mail']))
762
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
763
		AddMailQueue(true);
764
765
	$do_header = $header === null ? !$header_done : $header;
766
	if ($do_footer === null)
767
		$do_footer = $do_header;
768
769
	// Has the template/header been done yet?
770
	if ($do_header)
771
	{
772
		// Was the page title set last minute? Also update the HTML safe one.
773
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
774
			$context['page_title_html_safe'] = Util::htmlspecialchars(un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
775
776
		// Start up the session URL fixer.
777
		ob_start('ob_sessrewrite');
778
779
		call_integration_buffer();
780
781
		// Display the screen in the logical order.
782
		template_header();
783
		$header_done = true;
784
	}
785
786
	if ($do_footer)
787
	{
788
		// Show the footer.
789
		theme()->getTemplates()->loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
790
791
		// Just so we don't get caught in an endless loop of errors from the footer...
792
		if (!$footer_done)
793
		{
794
			$footer_done = true;
795
			template_footer();
796
797
			// Add $db_show_debug = true; to Settings.php if you want to show the debugging information.
798
			// (since this is just debugging... it's okay that it's after </html>.)
799
			if ($db_show_debug === true)
800
			{
801
				if (!isset($_REQUEST['xml']) && ((!isset($_GET['action']) || $_GET['action'] != 'viewquery') && !isset($_GET['api'])))
802
				{
803
					Debug::instance()->display();
804
				}
805
			}
806
		}
807
	}
808
809
	// Need user agent
810
	$req = request();
811
812
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
813
	$invalid_old_url = array(
814
		'action=dlattach',
815
		'action=jsoption',
816
		';xml',
817
		';api',
818
	);
819
	call_integration_hook('integrate_invalid_old_url', array(&$invalid_old_url));
820
	$make_old = true;
821
	foreach ($invalid_old_url as $url)
822
	{
823
		if (strpos($_SERVER['REQUEST_URL'], $url) !== false)
824
		{
825
			$make_old = false;
826
			break;
827
		}
828
	}
829
	if ($make_old === true)
830
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
831
832
	// For session check verification.... don't switch browsers...
833
	$_SESSION['USER_AGENT'] = $req->user_agent();
834
835
	// Hand off the output to the portal, etc. we're integrated with.
836
	call_integration_hook('integrate_exit', array($do_footer));
837
838
	// Don't exit if we're coming from index.php; that will pass through normally.
839
	if (!$from_index)
840
		exit;
841
}
842
843
/**
844
 * Sets the class of the current topic based on is_very_hot, veryhot, hot, etc
845
 *
846
 * @param mixed[] $topic_context array of topic information
847
 */
848
function determineTopicClass(&$topic_context)
849
{
850
	// Set topic class depending on locked status and number of replies.
851 1
	if ($topic_context['is_very_hot'])
852
		$topic_context['class'] = 'veryhot';
853 1
	elseif ($topic_context['is_hot'])
854
		$topic_context['class'] = 'hot';
855
	else
856 1
		$topic_context['class'] = 'normal';
857
858 1
	$topic_context['class'] .= !empty($topic_context['is_poll']) ? '_poll' : '_post';
859
860 1
	if ($topic_context['is_locked'])
861
		$topic_context['class'] .= '_locked';
862
863 1
	if ($topic_context['is_sticky'])
864
		$topic_context['class'] .= '_sticky';
865 1
}
866
867
/**
868
 * Sets up the basic theme context stuff.
869
 *
870
 * @param bool $forceload = false
871
 *
872
 * @return
873
 */
874
function setupThemeContext($forceload = false)
875
{
876
	return theme()->setupThemeContext($forceload);
877
}
878
879
/**
880
 * Helper function to convert memory string settings to bytes
881
 *
882
 * @param string $val The byte string, like 256M or 1G
883
 *
884
 * @return integer The string converted to a proper integer in bytes
885
 */
886
function memoryReturnBytes($val)
887
{
888 1
	if (is_integer($val))
889
		return $val;
890
891
	// Separate the number from the designator
892 1
	$val = trim($val);
893 1
	$num = intval(substr($val, 0, strlen($val) - 1));
894 1
	$last = strtolower(substr($val, -1));
895
896
	// Convert to bytes
897
	switch ($last)
898
	{
899
		// fall through select g = 1024*1024*1024
900 1
		case 'g':
901 1
			$num *= 1024;
902
		// fall through select m = 1024*1024
903 1
		case 'm':
904 1
			$num *= 1024;
905
		// fall through select k = 1024
906
		case 'k':
907 1
			$num *= 1024;
908
	}
909
910 1
	return $num;
911
}
912
913
/**
914
 * This is the only template included in the sources.
915
 */
916
function template_rawdata()
917
{
918
	return theme()->template_rawdata();
919
}
920
921
/**
922
 * The header template
923
 */
924
function template_header()
925
{
926
	return theme()->template_header();
927
}
928
929
/**
930
 * Show the copyright.
931
 */
932
function theme_copyright()
933
{
934
	return theme()->theme_copyright();
935
}
936
937
/**
938
 * The template footer
939
 */
940
function template_footer()
941
{
942
	return theme()->template_footer();
943
}
944
945
/**
946
 * Output the Javascript files
947
 *
948
 * What it does:
949
 *
950
 * - tabbing in this function is to make the HTML source look proper
951
 * - outputs jQuery/jQueryUI from the proper source (local/CDN)
952
 * - if deferred is set function will output all JS (source & inline) set to load at page end
953
 * - if the admin option to combine files is set, will use Combiner.class
954
 *
955
 * @param bool $do_deferred = false
956
 */
957
function template_javascript($do_deferred = false)
958
{
959
	theme()->template_javascript($do_deferred);
960
	return;
961
}
962
963
/**
964
 * Output the CSS files
965
 *
966
 * What it does:
967
 *  - If the admin option to combine files is set, will use Combiner.class
968
 */
969
function template_css()
970
{
971
	theme()->template_css();
972
	return;
973
}
974
975
/**
976
 * Calls on template_show_error from index.template.php to show warnings
977
 * and security errors for admins
978
 */
979
function template_admin_warning_above()
980
{
981
	theme()->template_admin_warning_above();
982
	return;
983
}
984
985
/**
986
 * Convert a single IP to a ranged IP.
987
 *
988
 * - Internal function used to convert a user-readable format to a format suitable for the database.
989
 *
990
 * @param string $fullip A full dot notation IP address
991
 *
992
 * @return array|string 'unknown' if the ip in the input was '255.255.255.255'
993
 */
994
function ip2range($fullip)
995
{
996
	// If its IPv6, validate it first.
997
	if (isValidIPv6($fullip) !== false)
998
	{
999
		$ip_parts = explode(':', expandIPv6($fullip, false));
1000
		$ip_array = array();
1001
1002
		if (count($ip_parts) != 8)
1003
			return array();
1004
1005
		for ($i = 0; $i < 8; $i++)
1006
		{
1007
			if ($ip_parts[$i] == '*')
1008
				$ip_array[$i] = array('low' => '0', 'high' => hexdec('ffff'));
1009 View Code Duplication
			elseif (preg_match('/^([0-9A-Fa-f]{1,4})\-([0-9A-Fa-f]{1,4})$/', $ip_parts[$i], $range) == 1)
1010
				$ip_array[$i] = array('low' => hexdec($range[1]), 'high' => hexdec($range[2]));
1011
			elseif (is_numeric(hexdec($ip_parts[$i])))
1012
				$ip_array[$i] = array('low' => hexdec($ip_parts[$i]), 'high' => hexdec($ip_parts[$i]));
1013
		}
1014
1015
		return $ip_array;
1016
	}
1017
1018
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
1019
	if ($fullip == 'unknown')
1020
		$fullip = '255.255.255.255';
1021
1022
	$ip_parts = explode('.', $fullip);
1023
	$ip_array = array();
1024
1025
	if (count($ip_parts) != 4)
1026
		return array();
1027
1028
	for ($i = 0; $i < 4; $i++)
1029
	{
1030
		if ($ip_parts[$i] == '*')
1031
			$ip_array[$i] = array('low' => '0', 'high' => '255');
1032 View Code Duplication
		elseif (preg_match('/^(\d{1,3})\-(\d{1,3})$/', $ip_parts[$i], $range) == 1)
1033
			$ip_array[$i] = array('low' => $range[1], 'high' => $range[2]);
1034
		elseif (is_numeric($ip_parts[$i]))
1035
			$ip_array[$i] = array('low' => $ip_parts[$i], 'high' => $ip_parts[$i]);
1036
	}
1037
1038
	// Makes it simpler to work with.
1039
	$ip_array[4] = array('low' => 0, 'high' => 0);
1040
	$ip_array[5] = array('low' => 0, 'high' => 0);
1041
	$ip_array[6] = array('low' => 0, 'high' => 0);
1042
	$ip_array[7] = array('low' => 0, 'high' => 0);
1043
1044
	return $ip_array;
1045
}
1046
1047
/**
1048
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
1049
 *
1050
 * @param string $ip A full dot notation IP address
1051
 *
1052
 * @return string
1053
 */
1054
function host_from_ip($ip)
1055
{
1056
	global $modSettings;
1057
1058
	$cache = Cache::instance();
1059
1060
	$host = '';
1061
	if ($cache->getVar($host, 'hostlookup-' . $ip, 600) || empty($ip))
1062
		return $host;
1063
1064
	$t = microtime(true);
1065
1066
	// Try the Linux host command, perhaps?
1067
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
1068
	{
1069
		if (!isset($modSettings['host_to_dis']))
1070
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
1071
		else
1072
			$test = @shell_exec('host ' . @escapeshellarg($ip));
1073
1074
		// Did host say it didn't find anything?
1075
		if (strpos($test, 'not found') !== false)
1076
			$host = '';
1077
		// Invalid server option?
1078
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
1079
			updateSettings(array('host_to_dis' => 1));
1080
		// Maybe it found something, after all?
1081
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
1082
			$host = $match[1];
1083
	}
1084
1085
	// This is nslookup; usually only Windows, but possibly some Unix?
1086
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
1087
	{
1088
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
1089
1090 View Code Duplication
		if (strpos($test, 'Non-existent domain') !== false)
1091
			$host = '';
1092
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
1093
			$host = $match[1];
1094
	}
1095
1096
	// This is the last try :/.
1097
	if (!isset($host) || $host === false)
1098
		$host = @gethostbyaddr($ip);
1099
1100
	// It took a long time, so let's cache it!
1101
	if (microtime(true) - $t > 0.5)
1102
		$cache->put('hostlookup-' . $ip, $host, 600);
1103
1104
	return $host;
1105
}
1106
1107
/**
1108
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
1109
 *
1110
 * @param string $text The string to process
1111
 * @param int $max_chars defaults to 20
1112
 *     - if encrypt = true this is the maximum number of bytes to use in integer hashes (for searching)
1113
 *     - if encrypt = false this is the maximum number of letters in each word
1114
 * @param bool $encrypt = false Used for custom search indexes to return an int[] array representing the words
1115
 *
1116
 * @return array
1117
 */
1118
function text2words($text, $max_chars = 20, $encrypt = false)
1119
{
1120
	// Step 1: Remove entities/things we don't consider words:
1121 11
	$words = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', strtr($text, array('<br />' => ' ')));
1122
1123
	// Step 2: Entities we left to letters, where applicable, lowercase.
1124 11
	$words = un_htmlspecialchars(Util::strtolower($words));
1125
1126
	// Step 3: Ready to split apart and index!
1127 11
	$words = explode(' ', $words);
1128
1129 11
	if ($encrypt)
1130
	{
1131
		// Range of characters that crypt will produce (0-9, a-z, A-Z .)
1132
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
1133
		$returned_ints = array();
1134
		foreach ($words as $word)
1135
		{
1136
			if (($word = trim($word, '-_\'')) !== '')
1137
			{
1138
				// Get a crypt representation of this work
1139
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
1140
				$total = 0;
1141
1142
				// Create an integer representation
1143
				for ($i = 0; $i < $max_chars; $i++)
1144
					$total += $possible_chars[ord($encrypted{$i})] * pow(63, $i);
1145
1146
				// Return the value
1147
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
1148
			}
1149
		}
1150
		return array_unique($returned_ints);
1151
	}
1152
	else
1153
	{
1154
		// Trim characters before and after and add slashes for database insertion.
1155 11
		$returned_words = array();
1156 11
		foreach ($words as $word)
1157 11
			if (($word = trim($word, '-_\'')) !== '')
1158 11
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
1159
1160
		// Filter out all words that occur more than once.
1161 11
		return array_unique($returned_words);
1162
	}
1163
}
1164
1165
/**
1166
 * Sets up all of the top menu buttons
1167
 *
1168
 * What it does:
1169
 *
1170
 * - Defines every master item in the menu, as well as any sub-items
1171
 * - Ensures the chosen action is set so the menu is highlighted
1172
 * - Saves them in the cache if it is available and on
1173
 * - Places the results in $context
1174
 */
1175
function setupMenuContext()
1176
{
1177
	return theme()->setupMenuContext();
1178
}
1179
1180
/**
1181
 * Process functions of an integration hook.
1182
 *
1183
 * What it does:
1184
 *
1185
 * - Calls all functions of the given hook.
1186
 * - Supports static class method calls.
1187
 *
1188
 * @param string $hook The name of the hook to call
1189
 * @param mixed[] $parameters = array() Parameters to pass to the hook
1190
 *
1191
 * @return mixed[] the results of the functions
1192
 */
1193
function call_integration_hook($hook, $parameters = array())
1194
{
1195 39
	return Hooks::instance()->hook($hook, $parameters);
1196
}
1197
1198
/**
1199
 * Includes files for hooks that only do that (i.e. integrate_pre_include)
1200
 *
1201
 * @param string $hook The name to include
1202
 */
1203
function call_integration_include_hook($hook)
1204
{
1205 8
	Hooks::instance()->include_hook($hook);
1206 8
}
1207
1208
/**
1209
 * Special hook call executed during obExit
1210
 */
1211
function call_integration_buffer()
1212
{
1213
	Hooks::instance()->buffer_hook();
1214
}
1215
1216
/**
1217
 * Add a function for integration hook.
1218
 *
1219
 * - Does nothing if the function is already added.
1220
 *
1221
 * @param string $hook The name of the hook to add
1222
 * @param string $function The function associated with the hook
1223
 * @param string $file The file that contains the function
1224
 * @param bool $permanent = true if true, updates the value in settings table
1225
 */
1226
function add_integration_function($hook, $function, $file = '', $permanent = true)
1227
{
1228 1
	Hooks::instance()->add($hook, $function, $file, $permanent);
1229 1
}
1230
1231
/**
1232
 * Remove an integration hook function.
1233
 *
1234
 * What it does:
1235
 *
1236
 * - Removes the given function from the given hook.
1237
 * - Does nothing if the function is not available.
1238
 *
1239
 * @param string $hook The name of the hook to remove
1240
 * @param string $function The name of the function
1241
 * @param string $file The file its located in
1242
 */
1243
function remove_integration_function($hook, $function, $file = '')
1244
{
1245 1
	Hooks::instance()->remove($hook, $function, $file);
1246 1
}
1247
1248
/**
1249
 * Decode numeric html entities to their UTF8 equivalent character.
1250
 *
1251
 * What it does:
1252
 *
1253
 * - Callback function for preg_replace_callback in subs-members
1254
 * - Uses capture group 2 in the supplied array
1255
 * - Does basic scan to ensure characters are inside a valid range
1256
 *
1257
 * @param mixed[] $matches matches from a preg_match_all
1258
 *
1259
 * @return string $string
1260
 */
1261
function replaceEntities__callback($matches)
1262
{
1263
	if (!isset($matches[2]))
1264
		return '';
1265
1266
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
1267
1268
	// remove left to right / right to left overrides
1269
	if ($num === 0x202D || $num === 0x202E)
1270
		return '';
1271
1272
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
1273
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
1274
		return '&#' . $num . ';';
1275
1276
	// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
1277
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
1278
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
1279
		return '';
1280
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1281
	elseif ($num < 0x80)
1282
		return chr($num);
1283
	// <0x800 (2048)
1284 View Code Duplication
	elseif ($num < 0x800)
1285
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1286
	// < 0x10000 (65536)
1287 View Code Duplication
	elseif ($num < 0x10000)
1288
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1289
	// <= 0x10FFFF (1114111)
1290 View Code Duplication
	else
1291
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1292
}
1293
1294
/**
1295
 * Converts html entities to utf8 equivalents
1296
 *
1297
 * What it does:
1298
 *
1299
 * - Callback function for preg_replace_callback
1300
 * - Uses capture group 1 in the supplied array
1301
 * - Does basic checks to keep characters inside a viewable range.
1302
 *
1303
 * @param mixed[] $matches array of matches as output from preg_match_all
1304
 *
1305
 * @return string $string
1306
 */
1307
function fixchar__callback($matches)
1308
{
1309
	if (!isset($matches[1]))
1310
		return '';
1311
1312
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
1313
1314
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
1315
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
1316
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
1317
		return '';
1318
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1319
	elseif ($num < 0x80)
1320
		return chr($num);
1321
	// <0x800 (2048)
1322 View Code Duplication
	elseif ($num < 0x800)
1323
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1324
	// < 0x10000 (65536)
1325 View Code Duplication
	elseif ($num < 0x10000)
1326
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1327
	// <= 0x10FFFF (1114111)
1328 View Code Duplication
	else
1329
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1330
}
1331
1332
/**
1333
 * Strips out invalid html entities, replaces others with html style &#123; codes
1334
 *
1335
 * What it does:
1336
 *
1337
 * - Callback function used of preg_replace_callback in various $ent_checks,
1338
 * - For example strpos, strlen, substr etc
1339
 *
1340
 * @param mixed[] $matches array of matches for a preg_match_all
1341
 *
1342
 * @return string
1343
 */
1344
function entity_fix__callback($matches)
1345
{
1346 3
	if (!isset($matches[2]))
1347
		return '';
1348
1349 3
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
1350
1351
	// We don't allow control characters, characters out of range, byte markers, etc
1352 3 View Code Duplication
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
1353 2
		return '';
1354
	else
1355 1
		return '&#' . $num . ';';
1356
}
1357
1358
/**
1359
 * Retrieve additional search engines, if there are any, as an array.
1360
 *
1361
 * @return mixed[] array of engines
1362
 */
1363
function prepareSearchEngines()
1364
{
1365
	global $modSettings;
1366
1367
	$engines = array();
1368
	if (!empty($modSettings['additional_search_engines']))
1369
	{
1370
		$search_engines = Util::unserialize($modSettings['additional_search_engines']);
1371
		foreach ($search_engines as $engine)
1372
			$engines[strtolower(preg_replace('~[^A-Za-z0-9 ]~', '', $engine['name']))] = $engine;
1373
	}
1374
1375
	return $engines;
1376
}
1377
1378
/**
1379
 * This function receives a request handle and attempts to retrieve the next result.
1380
 *
1381
 * What it does:
1382
 *
1383
 * - It is used by the controller callbacks from the template, such as
1384
 * posts in topic display page, posts search results page, or personal messages.
1385
 *
1386
 * @param resource $messages_request holds a query result
1387
 * @param bool $reset
1388
 *
1389
 * @return integer|boolean
1390
 */
1391
function currentContext($messages_request, $reset = false)
1392
{
1393
	// Can't work with a database without a database :P
1394
	$db = database();
1395
1396
	// Start from the beginning...
1397
	if ($reset)
1398
		return $db->data_seek($messages_request, 0);
1399
1400
	// If the query has already returned false, get out of here
1401
	if (empty($messages_request))
1402
		return false;
1403
1404
	// Attempt to get the next message.
1405
	$message = $db->fetch_assoc($messages_request);
1406
	if (!$message)
1407
	{
1408
		$db->free_result($messages_request);
1409
1410
		return false;
1411
	}
1412
1413
	return $message;
1414
}
1415
1416
/**
1417
 * Helper function to insert an array in to an existing array
1418
 *
1419
 * What it does:
1420
 *
1421
 * - Intended for addon use to allow such things as
1422
 * - Adding in a new menu item to an existing menu array
1423
 *
1424
 * @param mixed[] $input the array we will insert to
1425
 * @param string $key the key in the array that we are looking to find for the insert action
1426
 * @param mixed[] $insert the actual data to insert before or after the key
1427
 * @param string $where adding before or after
1428
 * @param bool $assoc if the array is a assoc array with named keys or a basic index array
1429
 * @param bool $strict search for identical elements, this means it will also check the types of the needle.
1430
 *
1431
 * @return array|mixed[]
1432
 */
1433
function elk_array_insert($input, $key, $insert, $where = 'before', $assoc = true, $strict = false)
1434
{
1435
	// Search for key names or values
1436
	if ($assoc)
1437
		$position = array_search($key, array_keys($input), $strict);
1438
	else
1439
		$position = array_search($key, $input, $strict);
1440
1441
	// If the key is not found, just insert it at the end
1442
	if ($position === false)
1443
		return array_merge($input, $insert);
1444
1445
	if ($where === 'after')
1446
		$position++;
1447
1448
	// Insert as first
1449
	if ($position === 0)
1450
		$input = array_merge($insert, $input);
1451
	else
1452
		$input = array_merge(array_slice($input, 0, $position), $insert, array_slice($input, $position));
1453
1454
	return $input;
1455
}
1456
1457
/**
1458
 * Run a scheduled task now
1459
 *
1460
 * What it does:
1461
 *
1462
 * - From time to time it may be necessary to fire a scheduled task ASAP
1463
 * - This function sets the scheduled task to be called before any other one
1464
 *
1465
 * @param string $task the name of a scheduled task
1466
 */
1467
function scheduleTaskImmediate($task)
1468
{
1469
	global $modSettings;
1470
1471
	if (!isset($modSettings['scheduleTaskImmediate']))
1472
		$scheduleTaskImmediate = array();
1473
	else
1474
		$scheduleTaskImmediate = Util::unserialize($modSettings['scheduleTaskImmediate']);
1475
1476
	// If it has not been scheduled, the do so now
1477
	if (!isset($scheduleTaskImmediate[$task]))
1478
	{
1479
		$scheduleTaskImmediate[$task] = 0;
1480
		updateSettings(array('scheduleTaskImmediate' => serialize($scheduleTaskImmediate)));
1481
1482
		require_once(SUBSDIR . '/ScheduledTasks.subs.php');
1483
1484
		// Ensure the task is on
1485
		toggleTaskStatusByName($task, true);
1486
1487
		// Before trying to run it **NOW** :P
1488
		calculateNextTrigger($task, true);
1489
	}
1490
}
1491
1492
/**
1493
 * For diligent people: remove scheduleTaskImmediate when done, otherwise
1494
 * a maximum of 10 executions is allowed
1495
 *
1496
 * @param string $task the name of a scheduled task
1497
 * @param bool $calculateNextTrigger if recalculate the next task to execute
1498
 */
1499
function removeScheduleTaskImmediate($task, $calculateNextTrigger = true)
1500
{
1501
	global $modSettings;
1502
1503
	// Not on, bail
1504
	if (!isset($modSettings['scheduleTaskImmediate']))
1505
		return;
1506
	else
1507
		$scheduleTaskImmediate = Util::unserialize($modSettings['scheduleTaskImmediate']);
1508
1509
	// Clear / remove the task if it was set
1510
	if (isset($scheduleTaskImmediate[$task]))
1511
	{
1512
		unset($scheduleTaskImmediate[$task]);
1513
		updateSettings(array('scheduleTaskImmediate' => serialize($scheduleTaskImmediate)));
1514
1515
		// Recalculate the next task to execute
1516
		if ($calculateNextTrigger)
1517
		{
1518
			require_once(SUBSDIR . '/ScheduledTasks.subs.php');
1519
			calculateNextTrigger($task);
1520
		}
1521
	}
1522
}
1523
1524
/**
1525
 * Helper function to replace commonly used urls in text strings
1526
 *
1527
 * @event integrate_basic_url_replacement add additional place holder replacements
1528
 * @param string $string the string to inject URLs into
1529
 *
1530
 * @return string the input string with the place-holders replaced with
1531
 *           the correct URLs
1532
 */
1533
function replaceBasicActionUrl($string)
1534
{
1535 3
	global $scripturl, $context, $boardurl;
1536 3
	static $find_replace = null;
1537
1538 3
	if ($find_replace === null)
1539
	{
1540
		$find_replace = array(
1541 1
			'{forum_name}' => $context['forum_name'],
1542 1
			'{forum_name_html_safe}' => $context['forum_name_html_safe'],
1543 1
			'{forum_name_html_unsafe}' => un_htmlspecialchars($context['forum_name_html_safe']),
1544 1
			'{script_url}' => $scripturl,
1545 1
			'{board_url}' => $boardurl,
1546 1
			'{login_url}' => getUrl('action', ['action' => 'login']),
1547 1
			'{register_url}' => getUrl('action', ['action' => 'register']),
1548 1
			'{activate_url}' => getUrl('action', ['action' => 'register', 'sa' => 'activate']),
1549 1
			'{help_url}' => getUrl('action', ['action' => 'help']),
1550 1
			'{admin_url}' => getUrl('admin', ['action' => 'admin']),
1551 1
			'{moderate_url}' => getUrl('moderate', ['action' => 'moderate']),
1552 1
			'{recent_url}' => getUrl('action', ['action' => 'recent']),
1553 1
			'{search_url}' => getUrl('action', ['action' => 'search']),
1554 1
			'{who_url}' => getUrl('action', ['action' => 'who']),
1555 1
			'{credits_url}' => getUrl('action', ['action' => 'who', 'sa' => 'credits']),
1556 1
			'{calendar_url}' => getUrl('action', ['action' => 'calendar']),
1557 1
			'{memberlist_url}' => getUrl('action', ['action' => 'memberlist']),
1558 1
			'{stats_url}' => getUrl('action', ['action' => 'stats']),
1559
		);
1560 1
		call_integration_hook('integrate_basic_url_replacement', array(&$find_replace));
1561
	}
1562
1563 3
	return str_replace(array_keys($find_replace), array_values($find_replace), $string);
1564
}
1565
1566
/**
1567
 * This function creates a new GenericList from all the passed options.
1568
 *
1569
 * What it does:
1570
 *
1571
 * - Calls integration hook integrate_list_"unique_list_id" to allow easy modifying
1572
 *
1573
 * @event integrate_list_$listID called before every createlist to allow access to its listoptions
1574
 * @param mixed[] $listOptions associative array of option => value
1575
 */
1576
function createList($listOptions)
1577
{
1578
	call_integration_hook('integrate_list_' . $listOptions['id'], array(&$listOptions));
1579
1580
	$list = new Generic_List($listOptions);
1581
1582
	$list->buildList();
1583
}
1584
1585
/**
1586
 * This handy function retrieves a Request instance and passes it on.
1587
 *
1588
 * What it does:
1589
 *
1590
 * - To get hold of a Request, you can use this function or directly Request::instance().
1591
 * - This is for convenience, it simply delegates to Request::instance().
1592
 */
1593
function request()
1594
{
1595 18
	return Request::instance();
1596
}
1597
1598
/**
1599
 * Meant to replace any usage of $db_last_error.
1600
 *
1601
 * What it does:
1602
 *
1603
 * - Reads the file db_last_error.txt, if a time() is present returns it,
1604
 * otherwise returns 0.
1605
 */
1606
function db_last_error()
1607
{
1608
	$time = trim(file_get_contents(BOARDDIR . '/db_last_error.txt'));
1609
1610
	if (preg_match('~^\d{10}$~', $time) === 1)
1611
		return $time;
1612
	else
1613
		return 0;
1614
}
1615
1616
/**
1617
 * This function has the only task to retrieve the correct prefix to be used
1618
 * in responses.
1619
 *
1620
 * @return string - The prefix in the default language of the forum
1621
 */
1622
function response_prefix()
1623
{
1624 1
	global $language, $user_info, $txt;
1625 1
	static $response_prefix = null;
1626
1627 1
	$cache = Cache::instance();
1628
1629
	// Get a response prefix, but in the forum's default language.
1630 1
	if ($response_prefix === null && (!$cache->getVar($response_prefix, 'response_prefix') || !$response_prefix))
1631
	{
1632 1 View Code Duplication
		if ($language === $user_info['language'])
1633 1
			$response_prefix = $txt['response_prefix'];
1634
		else
1635
		{
1636
			theme()->getTemplates()->loadLanguageFile('index', $language, false);
1637
			$response_prefix = $txt['response_prefix'];
1638
			theme()->getTemplates()->loadLanguageFile('index');
1639
		}
1640
1641 1
		$cache->put('response_prefix', $response_prefix, 600);
1642
	}
1643
1644 1
	return $response_prefix;
1645
}
1646
1647
/**
1648
 * A very simple function to determine if an email address is "valid" for Elkarte.
1649
 *
1650
 * - A valid email for ElkArte is something that resembles an email (filter_var) and
1651
 * is less than 255 characters (for database limits)
1652
 *
1653
 * @param string $value - The string to evaluate as valid email
1654
 *
1655
 * @return string|false - The email if valid, false if not a valid email
1656
 */
1657
function isValidEmail($value)
1658
{
1659 1
	$value = trim($value);
1660 1
	if (filter_var($value, FILTER_VALIDATE_EMAIL) && Util::strlen($value) < 255)
1661 1
		return $value;
1662
	else
1663
		return false;
1664
}
1665
1666
/**
1667
 * Adds a protocol (http/s, ftp/mailto) to the beginning of an url if missing
1668
 *
1669
 * @param string $url - The url
1670
 * @param string[] $protocols - A list of protocols to check, the first is
1671
 *                 added if none is found (optional, default array('http://', 'https://'))
1672
 *
1673
 * @return string - The url with the protocol
1674
 */
1675
function addProtocol($url, $protocols = array())
1676
{
1677 3
	if (empty($protocols))
1678
	{
1679 3
		$pattern = '~^(http://|https://)~i';
1680 3
		$protocols = array('http://');
1681
	}
1682
	else
1683
	{
1684
		$pattern = '~^(' . implode('|', array_map(function ($val) {return preg_quote($val, '~'); }, $protocols)) . ')~i';
1685
	}
1686
1687 3
	$found = false;
1688
	$url = preg_replace_callback($pattern, function ($match) use (&$found) {
1689 3
		$found = true;
1690
1691 3
		return strtolower($match[0]);
1692 3
	}, $url);
1693
1694 3
	if ($found === true)
1695
	{
1696 3
			return $url;
1697
	}
1698
1699 1
	return $protocols[0] . $url;
1700
}
1701
1702
/**
1703
 * Removes nested quotes from a text string.
1704
 *
1705
 * @param string $text - The body we want to remove nested quotes from
1706
 *
1707
 * @return string - The same body, just without nested quotes
1708
 */
1709
function removeNestedQuotes($text)
1710
{
1711
	global $modSettings;
1712
1713
	// Remove any nested quotes, if necessary.
1714
	if (!empty($modSettings['removeNestedQuotes']))
1715
	{
1716
		return preg_replace(array('~\n?\[quote.*?\].+?\[/quote\]\n?~is', '~^\n~', '~\[/quote\]~'), '', $text);
1717
	}
1718
	else
1719
	{
1720
		return $text;
1721
	}
1722
}
1723
1724
/**
1725
 * Change a \t to a span that will show a tab
1726
 *
1727
 * @param string $string
1728
 *
1729
 * @return string
1730
 */
1731
function tabToHtmlTab($string)
1732
{
1733 2
	return str_replace("\t", "<span class=\"tab\">\t</span>", $string);
1734
}
1735
1736
/**
1737
 * Remove <br />
1738
 *
1739
 * @param string $string
1740
 *
1741
 * @return string
1742
 */
1743
function removeBr($string)
1744
{
1745
	return str_replace('<br />', '', $string);
1746
}
1747
1748
/**
1749
 * Are we using this browser?
1750
 *
1751
 * - Wrapper function for detectBrowser
1752
 *
1753
 * @param string $browser the browser we are checking for.
1754
 *
1755
 * @return bool
1756
 */
1757
function isBrowser($browser)
1758
{
1759 7
	global $context;
1760
1761
	// Don't know any browser!
1762 7
	if (empty($context['browser']))
1763
		detectBrowser();
1764
1765 7
	return !empty($context['browser'][$browser]) || !empty($context['browser']['is_' . $browser]) ? true : false;
1766
}
1767
1768
/**
1769
 * Replace all vulgar words with respective proper words. (substring or whole words..)
1770
 *
1771
 * What it does:
1772
 * - it censors the passed string.
1773
 * - if the admin setting allow_no_censored is on it does not censor unless force is also set.
1774
 * - if the admin setting allow_no_censored is off will censor words unless the user has set
1775
 * it to not censor in their profile and force is off
1776
 * - it caches the list of censored words to reduce parsing.
1777
 * - Returns the censored text
1778
 *
1779
 * @param string $text
1780
 * @param bool $force = false
1781
 *
1782
 * @return string
1783
 */
1784
function censor($text, $force = false)
1785
{
1786 5
	global $modSettings;
1787 5
	static $censor = null;
1788
1789 5
	if ($censor === null)
1790
	{
1791 1
		$censor = new Censor(explode("\n", $modSettings['censor_vulgar']), explode("\n", $modSettings['censor_proper']), $modSettings);
1792
	}
1793
1794 5
	return $censor->censor($text, $force);
1795
}
1796
1797
/**
1798
 * Helper function able to determine if the current member can see at least
1799
 * one button of a button strip.
1800
 *
1801
 * @param mixed[] $button_strip
1802
 *
1803
 * @return bool
1804
 */
1805
function can_see_button_strip($button_strip)
1806
{
1807
	global $context;
1808
1809
	foreach ($button_strip as $key => $value)
1810
	{
1811
		if (!isset($value['test']) || !empty($context[$value['test']]))
1812
			return true;
1813
	}
1814
1815
	return false;
1816
}
1817
1818
/**
1819
 * @return Themes\DefaultTheme\Theme
1820
 */
1821
function theme()
1822
{
1823 48
	return $GLOBALS['context']['theme_instance'];
1824
}
1825
1826
/**
1827
 * Stops the execution with a 1x1 gif file
1828
 *
1829
 * @param bool $expired Sends an expired header.
1830
 */
1831
function dieGif($expired = false)
1832
{
1833
	// The following logging is just for debug, it should be removed before final
1834
	// or at least once the bug is fixes #2391
1835
	$filename = '';
1836
	$linenum = '';
1837
	if (headers_sent($filename, $linenum))
1838
	{
1839
		if (empty($filename))
1840
		{
1841
			ob_clean();
1842
		}
1843
		else
1844
		{
1845
			Errors::instance()->log_error('Headers already sent in ' . $filename . ' at line ' . $linenum);
1846
		}
1847
	}
1848
1849
	if ($expired === true)
1850
	{
1851
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
1852
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
1853
	}
1854
1855
	header('Content-Type: image/gif');
1856
	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");
1857
}
1858
1859
/**
1860
 * Prepare ob_start with or without gzip compression
1861
 *
1862
 * @param bool $use_compression Starts compressed headers.
1863
 */
1864
function obStart($use_compression = false)
1865
{
1866
	// This is done to clear any output that was made before now.
1867
	while (ob_get_level() > 0)
1868
	{
1869
		@ob_end_clean();
1870
	}
1871
1872
	if ($use_compression === true)
1873
	{
1874
		ob_start('ob_gzhandler');
1875
	}
1876
	else
1877
	{
1878
		ob_start();
1879
		header('Content-Encoding: none');
1880
	}
1881
}
1882
1883
/**
1884
 * Returns an URL based on the parameters passed and the selected generator
1885
 *
1886
 * @param string $type The type of the URL (depending on the type, the
1887
 *                     generator can act differently
1888
 * @param mixed[] $params All the parameters of the URL
1889
 *
1890
 * @return string An URL
1891
 */
1892
function getUrl($type, $params)
1893
{
1894 5
	static $generator = null;
1895
1896 5
	if ($generator === null)
1897
	{
1898
		$generator = initUrlGenerator();
1899
	}
1900 5
	return $generator->get($type, $params);
1901
}
1902
1903
/**
1904
 * Returns the query part of an URL based on the parameters passed and the selected generator
1905
 *
1906
 * @param string $type The type of the URL (depending on the type, the
1907
 *                     generator can act differently
1908
 * @param mixed[] $params All the parameters of the URL
1909
 *
1910
 * @return string The query part of an URL
1911
 */
1912
function getUrlQuery($type, $params)
1913
{
1914
	static $generator = null;
1915
1916
	if ($generator === null)
1917
	{
1918
		$generator = initUrlGenerator();
1919
	}
1920
	return $generator->getQuery($type, $params);
1921
}
1922
1923
/**
1924
 * Initialize the URL generator
1925
 *
1926
 * @return object The URL generator object
1927
 */
1928
function initUrlGenerator()
1929
{
1930
	global $scripturl, $context, $modSettings;
1931
	static $generator = null;
1932
1933
	if ($generator === null)
1934
	{
1935
		$generator = new Url_Generator([
1936
			'generator' => ucfirst($modSettings['url_format'] ?? 'standard'),
1937
			'scripturl' => $scripturl,
1938
			'replacements' => [
1939
				'{session_data}' => $context['session_var'] . '=' . $context['session_id']
1940
			]
1941
		]);
1942
		$generator->register('Topic');
1943
		$generator->register('Board');
1944
		$generator->register('Profile');
1945
	}
1946
	return $generator;
1947
}
1948