Completed
Push — development ( 7ace62...380f58 )
by Stephen
20s
created

Subs.php ➔ updateSettings()   C

Complexity

Conditions 17
Paths 14

Size

Total Lines 62
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 35
CRAP Score 17.0062

Importance

Changes 0
Metric Value
cc 17
eloc 32
nc 14
nop 3
dl 0
loc 62
rs 6.1162
c 0
b 0
f 0
ccs 35
cts 36
cp 0.9722
crap 17.0062

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 1.1 Release Candidate 1
15
 *
16
 */
17
18
/**
19
 * Update some basic statistics.
20
 *
21
 * @deprecated since 1.1 - use directly the update{Statistic}Stats functions instead
22
 *
23
 * 'member' statistic updates the latest member, the total member
24
 *  count, and the number of unapproved members.
25
 * 'member' also only counts approved members when approval is on, but
26
 *  is much more efficient with it off.
27
 *
28
 * 'message' changes the total number of messages, and the
29
 *  highest message id by id_msg - which can be parameters 1 and 2,
30
 *  respectively.
31
 *
32
 * 'topic' updates the total number of topics, or if parameter1 is true
33
 *  simply increments them.
34
 *
35
 * 'subject' updates the log_search_subjects in the event of a topic being
36
 *  moved, removed or split.  parameter1 is the topicid, parameter2 is the new subject
37
 *
38
 * 'postgroups' case updates those members who match condition's
39
 *  post-based membergroups in the database (restricted by parameter1).
40
 *
41
 * @param string $type Stat type - can be 'member', 'message', 'topic', 'subject' or 'postgroups'
42
 * @param int|string|boolean|mixed[]|null $parameter1 pass through value
43
 * @param int|string|boolean|mixed[]|null $parameter2 pass through value
44
 */
45
function updateStats($type, $parameter1 = null, $parameter2 = null)
46
{
47
	switch ($type)
48
	{
49
		case 'member':
50
			require_once(SUBSDIR . '/Members.subs.php');
51
			updateMemberStats($parameter1, $parameter2);
52
			break;
53
		case 'message':
54
			require_once(SUBSDIR . '/Messages.subs.php');
55
			updateMessageStats($parameter1, $parameter2);
56
			break;
57
		case 'subject':
58
			require_once(SUBSDIR . '/Messages.subs.php');
59
			updateSubjectStats($parameter1, $parameter2);
60
			break;
61
		case 'topic':
62
			require_once(SUBSDIR . '/Topic.subs.php');
63
			updateTopicStats($parameter1);
64
			break;
65
		case 'postgroups':
66
			require_once(SUBSDIR . '/Membergroups.subs.php');
67
			updatePostGroupStats($parameter1, $parameter2);
68
			break;
69
		default:
70
			trigger_error('updateStats(): Invalid statistic type \'' . $type . '\'', E_USER_NOTICE);
71
	}
72
}
73
74
/**
75
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
76
 *
77
 * What it does:
78
 *
79
 * - Updates both the settings table and $modSettings array.
80
 * - All of changeArray's indexes and values are assumed to have escaped apostrophes (')!
81
 * - If a variable is already set to what you want to change it to, that
82
 *   Variable will be skipped over; it would be unnecessary to reset.
83
 * - When update is true, UPDATEs will be used instead of REPLACE.
84
 * - When update is true, the value can be true or false to increment
85
 *  or decrement it, respectively.
86
 *
87
 * @param mixed[] $changeArray An associative array of what we're changing in 'setting' => 'value' format
88
 * @param bool $update Use an UPDATE query instead of a REPLACE query
89
 * @param bool $debug = false Not used at this time, see todo
90
 * @todo: add debugging features, $debug isn't used
91
 */
92
function updateSettings($changeArray, $update = false, $debug = false)
0 ignored issues
show
Unused Code introduced by
The parameter $debug is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
93
{
94 21
	global $modSettings;
95
96 21
	$db = database();
97 21
	$cache = Cache::instance();
98
99 21
	if (empty($changeArray) || !is_array($changeArray))
100 21
		return;
101
102
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
103
	if ($update)
104 21
	{
105 10
		foreach ($changeArray as $variable => $value)
106
		{
107 10
			$db->query('', '
108
				UPDATE {db_prefix}settings
109 10
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
110 10
				WHERE variable = {string:variable}',
111
				array(
112 10
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
113 10
					'variable' => $variable,
114
				)
115 10
			);
116
117 10
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
118 10
		}
119
120
		// Clean out the cache and make sure the cobwebs are gone too.
121 10
		$cache->remove('modSettings');
122
123 10
		return;
124
	}
125
126 17
	$replaceArray = array();
127 17
	foreach ($changeArray as $variable => $value)
128
	{
129
		// Don't bother if it's already like that ;).
130 17
		if (isset($modSettings[$variable]) && $modSettings[$variable] == $value)
131 17
			continue;
132
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
133 14
		elseif (!isset($modSettings[$variable]) && empty($value))
134
			continue;
135
136 14
		$replaceArray[] = array($variable, $value);
137
138 14
		$modSettings[$variable] = $value;
139 17
	}
140
141 17
	if (empty($replaceArray))
142 17
		return;
143
144 14
	$db->insert('replace',
145 14
		'{db_prefix}settings',
146 14
		array('variable' => 'string-255', 'value' => 'string-65534'),
147 14
		$replaceArray,
148 14
		array('variable')
149 14
	);
150
151
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
152 14
	$cache->remove('modSettings');
153 14
}
154
155
/**
156
 * Deletes one setting from the settings table and takes care of $modSettings as well
157
 *
158
 * @param string|string[] $toRemove the setting or the settings to be removed
159
 */
160
function removeSettings($toRemove)
161
{
162 1
	global $modSettings;
163
164 1
	$db = database();
165
166 1
	if (empty($toRemove))
167 1
		return;
168
169 1
	if (!is_array($toRemove))
170 1
		$toRemove = array($toRemove);
171
172
	// Remove the setting from the db
173 1
	$db->query('', '
174
		DELETE FROM {db_prefix}settings
175 1
		WHERE variable IN ({array_string:setting_name})',
176
		array(
177 1
			'setting_name' => $toRemove,
178
		)
179 1
	);
180
181
	// Remove it from $modSettings now so it does not persist
182 1
	foreach ($toRemove as $setting)
183 1
		if (isset($modSettings[$setting]))
184 1
			unset($modSettings[$setting]);
185
186
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
187 1
	Cache::instance()->remove('modSettings');
188 1
}
189
190
/**
191
 * Constructs a page list.
192
 *
193
 * What it does:
194
 *
195
 * - Builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15.
196
 * - Flexible_start causes it to use "url.page" instead of "url;start=page".
197
 * - Very importantly, cleans up the start value passed, and forces it to
198
 *   be a multiple of num_per_page.
199
 * - Checks that start is not more than max_value.
200
 * - Base_url should be the URL without any start parameter on it.
201
 * - Uses the compactTopicPagesEnable and compactTopicPagesContiguous
202
 *   settings to decide how to display the menu.
203
 *
204
 * @example is available near the function definition.
205
 * @example $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages, $maxindex, true);
206
 *
207
 * @param string $base_url The base URL to be used for each link.
208
 * @param int &$start The start position, by reference. If this is not a multiple
209
 * of the number of items per page, it is sanitized to be so and the value will persist upon the function's return.
210
 * @param int $max_value The total number of items you are paginating for.
211
 * @param int $num_per_page The number of items to be displayed on a given page.
212
 * @param bool $flexible_start = false Use "url.page" instead of "url;start=page"
213
 * @param mixed[] $show associative array of option => boolean paris
214
 */
215
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show = array())
216
{
217 2
	global $modSettings, $context, $txt, $settings;
218
219
	// Save whether $start was less than 0 or not.
220 2
	$start = (int) $start;
221 2
	$start_invalid = $start < 0;
222
	$show_defaults = array(
223 2
		'prev_next' => true,
224 2
		'all' => false,
225 2
	);
226
227 2
	$show = array_merge($show_defaults, $show);
228
229
	// Make sure $start is a proper variable - not less than 0.
230
	if ($start_invalid)
231 2
		$start = 0;
232
	// Not greater than the upper bound.
233 2
	elseif ($start >= $max_value)
234
		$start = max(0, (int) $max_value - (((int) $max_value % (int) $num_per_page) == 0 ? $num_per_page : ((int) $max_value % (int) $num_per_page)));
235
	// And it has to be a multiple of $num_per_page!
236
	else
237 2
		$start = max(0, (int) $start - ((int) $start % (int) $num_per_page));
238
239 2
	$context['current_page'] = $start / $num_per_page;
240
241 2
	$base_link = str_replace('{base_link}', ($flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'), $settings['page_index_template']['base_link']);
242
243
	// Compact pages is off or on?
244 2
	if (empty($modSettings['compactTopicPagesEnable']))
245 2
	{
246
		// Show the left arrow.
247
		$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']));
248
249
		// Show all the pages.
250
		$display_page = 1;
251
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
252
			$pageindex .= $start == $counter && !$start_invalid && empty($show['all_selected']) ? sprintf($settings['page_index_template']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
253
254
		// Show the right arrow.
255
		$display_page = ($start + $num_per_page) > $max_value ? $max_value : ($start + $num_per_page);
256
		if ($start != $counter - $max_value && !$start_invalid && $show['prev_next'] && empty($show['all_selected']))
257
			$pageindex .= $display_page > $counter - $num_per_page ? ' ' : sprintf($base_link, $display_page, str_replace('{next_txt}', $txt['next'], $settings['page_index_template']['next_page']));
258
	}
259
	else
260
	{
261
		// If they didn't enter an odd value, pretend they did.
262 2
		$PageContiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
263
264
		// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
265 2
		if (!empty($start) && $show['prev_next'])
266 2
			$pageindex = sprintf($base_link, $start - $num_per_page, str_replace('{prev_txt}', $txt['prev'], $settings['page_index_template']['previous_page']));
267
		else
268 2
			$pageindex = '';
269
270
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
271 2
		if ($start > $num_per_page * $PageContiguous)
272 2
			$pageindex .= sprintf($base_link, 0, '1');
273
274
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
275 2
		if ($start > $num_per_page * ($PageContiguous + 1))
276 2
			$pageindex .= str_replace('{custom}', 'data-baseurl="' . htmlspecialchars(JavaScriptEscape(($flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d')), ENT_COMPAT, 'UTF-8') . '" data-perpage="' . $num_per_page . '" data-firstpage="' . $num_per_page . '" data-lastpage="' . ($start - $num_per_page * $PageContiguous) . '"', $settings['page_index_template']['expand_pages']);
277
278
		// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
279 2
		for ($nCont = $PageContiguous; $nCont >= 1; $nCont--)
280 2 View Code Duplication
			if ($start >= $num_per_page * $nCont)
281 2
			{
282
				$tmpStart = $start - $num_per_page * $nCont;
283
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
284
			}
285
286
		// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
287 2
		if (!$start_invalid && empty($show['all_selected']))
288 2
			$pageindex .= sprintf($settings['page_index_template']['current_page'], ($start / $num_per_page + 1));
289
		else
290
			$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
291
292
		// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
293 2
		$tmpMaxPages = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
294 2
		for ($nCont = 1; $nCont <= $PageContiguous; $nCont++)
295 2 View Code Duplication
			if ($start + $num_per_page * $nCont <= $tmpMaxPages)
296 2
			{
297
				$tmpStart = $start + $num_per_page * $nCont;
298
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
299
			}
300
301
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
302 2
		if ($start + $num_per_page * ($PageContiguous + 1) < $tmpMaxPages)
303 2
			$pageindex .= str_replace('{custom}', 'data-baseurl="' . htmlspecialchars(JavaScriptEscape(($flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d')), ENT_COMPAT, 'UTF-8') . '" data-perpage="' . $num_per_page . '" data-firstpage="' . ($start + $num_per_page * ($PageContiguous + 1)) . '" data-lastpage="' . $tmpMaxPages . '"', $settings['page_index_template']['expand_pages']);
304
305
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
306 2
		if ($start + $num_per_page * $PageContiguous < $tmpMaxPages)
307 2
			$pageindex .= sprintf($base_link, $tmpMaxPages, $tmpMaxPages / $num_per_page + 1);
308
309
		// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
310 2
		if ($start != $tmpMaxPages && $show['prev_next'] && empty($show['all_selected']))
311 2
			$pageindex .= sprintf($base_link, $start + $num_per_page, str_replace('{next_txt}', $txt['next'], $settings['page_index_template']['next_page']));
312
	}
313
314
	// The "all" button
315 2
	if ($show['all'])
316 2
	{
317
		if (!empty($show['all_selected']))
318
			$pageindex .= sprintf($settings['page_index_template']['current_page'], $txt['all']);
319
		else
320
			$pageindex .= sprintf(str_replace('%1$d', '%1$s', $base_link), '0;all', str_replace('{all_txt}', $txt['all'], $settings['page_index_template']['all']));
321
	}
322
323 2
	return $pageindex;
324
}
325
326
/**
327
 * Formats a number.
328
 *
329
 * What it does:
330
 *
331
 * - Uses the format of number_format to decide how to format the number.
332
 *   for example, it might display "1 234,50".
333
 * - Caches the formatting data from the setting for optimization.
334
 *
335
 * @param float $number The float value to apply comma formatting
336
 * @param integer|bool $override_decimal_count = false or number of decimals
337
 *
338
 * @return string
339
 */
340
function comma_format($number, $override_decimal_count = false)
341
{
342 2
	global $txt;
343 2
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
344
345
	// Cache these values...
346 2
	if ($decimal_separator === null)
347 2
	{
348
		// Not set for whatever reason?
349 1
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
350 1
			return $number;
351
352
		// Cache these each load...
353 1
		$thousands_separator = $matches[1];
354 1
		$decimal_separator = $matches[2];
355 1
		$decimal_count = strlen($matches[3]);
356 1
	}
357
358
	// Format the string with our friend, number_format.
359 2
	return number_format($number, (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
360
}
361
362
/**
363
 * Formats a number to a multiple of thousands x, x k, x M, x G, x T
364
 *
365
 * @param float $number The value to format
366
 * @param integer|bool $override_decimal_count = false or number of decimals
367
 *
368
 * @return string
369
 */
370
function thousands_format($number, $override_decimal_count = false)
371
{
372
	foreach (array('', ' k', ' M', ' G', ' T') as $kb)
373
	{
374
		if ($number < 1000)
375
		{
376
			break;
377
		}
378
379
		$number /= 1000;
380
	}
381
382
	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 372. 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...
383
}
384
385
/**
386
 * Formats a number to a computer byte size value xB, xKB, xMB, xGB
387
 *
388
 * @param int $number
389
 *
390
 * @return string
391
 */
392
function byte_format($number)
393
{
394
	global $txt;
395
396
	$kb = '';
397
	foreach (array('byte', 'kilobyte', 'megabyte', 'gigabyte') as $kb)
398
	{
399
		if ($number < 1024)
400
		{
401
			break;
402
		}
403
404
		$number /= 1024;
405
	}
406
407
	return comma_format($number) . ' ' . $txt[$kb];
408
}
409
410
/**
411
 * Format a time to make it look purdy.
412
 *
413
 * What it does:
414
 *
415
 * - Returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
416
 * - Applies all necessary time offsets to the timestamp, unless offset_type is set.
417
 * - If todayMod is set and show_today was not not specified or true, an
418
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
419
 * - Performs localization (more than just strftime would do alone.)
420
 *
421
 * @param int $log_time A unix timestamp
422
 * @param string|bool $show_today = true show "Today"/"Yesterday",
423
 *   false shows the date, a string can force a date format to use %b %d, %Y
424
 * @param string|bool $offset_type = false If false, uses both user time offset and forum offset.
425
 *   If 'forum', uses only the forum offset. Otherwise no offset is applied.
426
 */
427
function standardTime($log_time, $show_today = true, $offset_type = false)
428
{
429 6
	global $user_info, $txt, $modSettings;
430 6
	static $non_twelve_hour, $support_e = null;
431
432 6
	if ($support_e === null)
433 6
	{
434 1
		$support_e = detectServer()->is('windows');
435 1
	}
436
437
	// Offset the time.
438 6
	if (!$offset_type)
439 6
		$time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
440
	// Just the forum offset?
441 5
	elseif ($offset_type === 'forum')
442
		$time = $log_time + $modSettings['time_offset'] * 3600;
443
	else
444 5
		$time = $log_time;
445
446
	// We can't have a negative date (on Windows, at least.)
447 6
	if ($log_time < 0)
448 6
		$log_time = 0;
449
450
	// Today and Yesterday?
451 6
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
452 6
	{
453
		// Get the current time.
454 6
		$nowtime = forum_time();
455
456 6
		$then = @getdate($time);
457 6
		$now = @getdate($nowtime);
458
459
		// Try to make something of a time format string...
460 6
		$s = strpos($user_info['time_format'], '%S') === false ? '' : ':%S';
461 6
		if (strpos($user_info['time_format'], '%H') === false && strpos($user_info['time_format'], '%T') === false)
462 6
		{
463 6
			$h = strpos($user_info['time_format'], '%l') === false ? '%I' : '%l';
464 6
			$today_fmt = $h . ':%M' . $s . ' %p';
465 6
		}
466
		else
467
			$today_fmt = '%H:%M' . $s;
468
469
		// Same day of the year, same year.... Today!
470 6
		if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
471 6
			return sprintf($txt['today'], standardTime($log_time, $today_fmt, $offset_type));
472
473
		// 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...
474 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))
475 3
			return sprintf($txt['yesterday'], standardTime($log_time, $today_fmt, $offset_type));
476 3
	}
477
478 6
	$str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
479
480 6
	if (setlocale(LC_TIME, $txt['lang_locale']))
481 6
	{
482
		if (!isset($non_twelve_hour))
483
			$non_twelve_hour = trim(strftime('%p')) === '';
484 View Code Duplication
		if ($non_twelve_hour && strpos($str, '%p') !== false)
485
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
486
487
		foreach (array('%a', '%A', '%b', '%B') as $token)
488
			if (strpos($str, $token) !== false)
489
				$str = str_replace($token, !empty($txt['lang_capitalize_dates']) ? Util::ucwords(strftime($token, $time)) : strftime($token, $time), $str);
490
	}
491
	else
492
	{
493
		// Do-it-yourself time localization.  Fun.
494 6
		foreach (array('%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months') as $token => $text_label)
495 6
			if (strpos($str, $token) !== false)
496 6
				$str = str_replace($token, $txt[$text_label][(int) strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
497
498 6 View Code Duplication
		if (strpos($str, '%p') !== false)
499 6
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
500
	}
501
502
	// Windows doesn't support %e; on some versions, strftime fails altogether if used, so let's prevent that.
503 6
	if ($support_e && strpos($str, '%e') !== false)
504 6
		$str = str_replace('%e', ltrim(strftime('%d', $time), '0'), $str);
505
506
	// Format any other characters..
507 6
	return strftime($str, $time);
508
}
509
510
/**
511
 * Used to render a timestamp to html5 <time> tag format.
512
 *
513
 * @param int $timestamp A unix timestamp
514
 *
515
 * @return string
516
 */
517
function htmlTime($timestamp)
518
{
519 5
	global $txt, $context;
520
521 5
	if (empty($timestamp))
522 5
		return '';
523
524 5
	$timestamp = forum_time(true, $timestamp);
525 5
	$time = date('Y-m-d H:i', $timestamp);
526 5
	$stdtime = standardTime($timestamp, true, true);
527
528
	// @todo maybe htmlspecialchars on the title attribute?
529 5
	return '<time title="' . (!empty($context['using_relative_time']) ? $stdtime : $txt['last_post']) . '" datetime="' . $time . '" data-timestamp="' . $timestamp . '">' . $stdtime . '</time>';
530
}
531
532
/**
533
 * Gets the current time with offset.
534
 *
535
 * What it does:
536
 *
537
 * - Always applies the offset in the time_offset setting.
538
 *
539
 * @param bool $use_user_offset = true if use_user_offset is true, applies the user's offset as well
540
 * @param int|null $timestamp = null A unix timestamp (null to use current time)
541
 *
542
 * @return int seconds since the unix epoch
543
 */
544
function forum_time($use_user_offset = true, $timestamp = null)
545
{
546 6
	global $user_info, $modSettings;
547
548 6
	if ($timestamp === null)
549 6
		$timestamp = time();
550 6
	elseif ($timestamp == 0)
551
		return 0;
552
553 6
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
554
}
555
556
/**
557
 * Removes special entities from strings.  Compatibility...
558
 *
559
 * - Faster than html_entity_decode
560
 * - Removes the base entities ( &amp; &quot; &#039; &lt; and &gt;. ) from text with htmlspecialchars_decode
561
 * - Additionally converts &nbsp with str_replace
562
 *
563
 * @param string $string The string to apply htmlspecialchars_decode
564
 *
565
 * @return string string without entities
566
 */
567
function un_htmlspecialchars($string)
568
{
569 20
	$string = htmlspecialchars_decode($string, ENT_QUOTES);
570 20
	$string = str_replace('&nbsp;', ' ', $string);
571
572 20
	return $string;
573
}
574
575
/**
576
 * Calculates all the possible permutations (orders) of an array.
577
 *
578
 * What it does:
579
 *
580
 * - Caution: should not be called on arrays bigger than 8 elements as this function is memory hungry
581
 * - returns an array containing each permutation.
582
 * - e.g. (1,2,3) returns (1,2,3), (1,3,2), (2,1,3), (2,3,1), (3,1,2), and (3,2,1)
583
 * - A combinations without repetition N! function so 3! = 6 and 10! = 3,628,800 combinations
584
 * - Used by parse_bbc to allow bbc tag parameters to be in any order and still be
585
 * parsed properly
586
 *
587
 * @deprecated since 1.0.5
588
 * @param mixed[] $array index array of values
589
 *
590
 * @return mixed[] array representing all permutations of the supplied array
591
 */
592
function permute($array)
593
{
594
	$orders = array($array);
595
596
	$n = count($array);
597
	$p = range(0, $n);
598
	for ($i = 1; $i < $n; null)
599
	{
600
		$p[$i]--;
601
		$j = $i % 2 != 0 ? $p[$i] : 0;
602
603
		$temp = $array[$i];
604
		$array[$i] = $array[$j];
605
		$array[$j] = $temp;
606
607
		for ($i = 1; $p[$i] == 0; $i++)
608
			$p[$i] = $i;
609
610
		$orders[] = $array;
611
	}
612
613
	return $orders;
614
}
615
616
/**
617
 * Lexicographic permutation function.
618
 *
619
 * This is a special type of permutation which involves the order of the set. The next
620
 * lexicographic permutation of '32541' is '34125'. Numerically, it is simply the smallest
621
 * set larger than the current one.
622
 *
623
 * The benefit of this over a recursive solution is that the whole list does NOT need
624
 * to be held in memory. So it's actually possible to run 30! permutations without
625
 * causing a memory overflow.
626
 *
627
 * Source: O'Reilly PHP Cookbook
628
 *
629
 * @param mixed[] $p The array keys to apply permutation
630
 * @param int $size The size of our permutation array
631
 *
632
 * @return mixed[] the next permutation of the passed array $p
633
 */
634
function pc_next_permutation($p, $size)
635
{
636
	// Slide down the array looking for where we're smaller than the next guy
637 2
	for ($i = $size - 1; isset($p[$i]) && $p[$i] >= $p[$i + 1]; --$i)
0 ignored issues
show
Unused Code introduced by
This for loop is empty and can be removed.

This check looks for for loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
638
	{
639 2
	}
640
641
	// If this doesn't occur, we've finished our permutations
642
	// the array is reversed: (1, 2, 3, 4) => (4, 3, 2, 1)
643 2
	if ($i === -1)
644 2
	{
645 2
		return false;
646
	}
647
648
	// Slide down the array looking for a bigger number than what we found before
649 2
	for ($j = $size; $p[$j] <= $p[$i]; --$j)
0 ignored issues
show
Unused Code introduced by
This for loop is empty and can be removed.

This check looks for for loops that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

Consider removing the loop.

Loading history...
650
	{
651 2
	}
652
653
	// Swap them
654 2
	$tmp = $p[$i];
655 2
	$p[$i] = $p[$j];
656 2
	$p[$j] = $tmp;
657
658
	// Now reverse the elements in between by swapping the ends
659 2
	for (++$i, $j = $size; $i < $j; ++$i, --$j)
660
	{
661 2
		$tmp = $p[$i];
662 2
		$p[$i] = $p[$j];
663 2
		$p[$j] = $tmp;
664 2
	}
665
666 2
	return $p;
667
}
668
669
/**
670
 * Parse bulletin board code in a string, as well as smileys optionally.
671
 *
672
 * @deprecated since 1.1b1
673
 *
674
 * What it does:
675
 *
676
 * - Only parses bbc tags which are not disabled in disabledBBC.
677
 * - Handles basic HTML, if enablePostHTML is on.
678
 * - Caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
679
 * - Only parses smileys if smileys is true.
680
 *
681
 * @param string|false $message if false return list of enabled bbc codes
682
 * @param bool $smileys = true if to parse smileys as well
683
 *
684
 * @return string
685
 */
686
function parse_bbc($message, $smileys = true)
687
{
688
	// Don't waste cycles
689 2
	if ($message === '')
690 2
		return '';
691
692 2
	$parser = \BBC\ParserWrapper::getInstance();
693
694
	// This is a deprecated way of getting codes
695 2
	if ($message === false)
696 2
	{
697
		return $parser->getCodes();
698
	}
699
700 2
	return $parser->parseMessage($message, $smileys);
701
}
702
703
/**
704
 * Parse smileys in the passed message.
705
 *
706
 * What it does:
707
 *
708
 * - The smiley parsing function which makes pretty faces appear :).
709
 * - If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
710
 * - These are specifically not parsed in code tags [url=mailto:[email protected]]
711
 * - Caches the smileys from the database or array in memory.
712
 * - Doesn't return anything, but rather modifies message directly.
713
 *
714
 * @param string $message The string containing smileys to parse
715
 * @deprecated since 1.1b1
716
 */
717
function parsesmileys(&$message)
718
{
719
	// No smiley set at all?!
720
	if ($GLOBALS['user_info']['smiley_set'] == 'none' || trim($message) == '')
721
	{
722
		return;
723
	}
724
725
	$wrapper = \BBC\ParserWrapper::getInstance();
726
	$parser = $wrapper->getSmileyParser();
727
	$message = $parser->parseBlock($message);
728
}
729
730
/**
731
 * Highlight any code.
732
 *
733
 * What it does:
734
 *
735
 * - Uses PHP's highlight_string() to highlight PHP syntax
736
 * - does special handling to keep the tabs in the code available.
737
 * - used to parse PHP code from inside [code] and [php] tags.
738
 *
739
 * @param string $code The string containing php code
740
 *
741
 * @return string the code with highlighted HTML.
742
 */
743
function highlight_php_code($code)
744
{
745
	// Remove special characters.
746
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", "\t" => '___TAB();', '&#91;' => '[')));
747
748
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
749
750
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
751
	$buffer = preg_replace('~___TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
752
753
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
754
}
755
756
/**
757
 * Ends execution and redirects the user to a new location
758
 *
759
 * What it does:
760
 *
761
 * - Makes sure the browser doesn't come back and repost the form data.
762
 * - Should be used whenever anything is posted.
763
 * - Calls AddMailQueue to process any mail queue items its can
764
 * - Calls call_integration_hook integrate_redirect before headers are sent
765
 * - Diverts final execution to obExit() which means a end to processing and sending of final output
766
 *
767
 * @event integrate_redirect called before headers are sent
768
 * @param string $setLocation = '' The URL to redirect to
769
 * @param bool $refresh = false, enable to send a refresh header, default is a location header
770
 * @throws Elk_Exception
771
 */
772
function redirectexit($setLocation = '', $refresh = false)
773
{
774
	global $scripturl, $context, $modSettings, $db_show_debug;
775
776
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
777
	if (!empty($context['flush_mail']))
778
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
779
		AddMailQueue(true);
780
781
	Notifications::getInstance()->send();
782
783
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
784
785
	if ($add)
786
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
787
788
	// Put the session ID in.
789 View Code Duplication
	if (empty($_COOKIE) && defined('SID') && SID != '')
790
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
791
	// Keep that debug in their for template debugging!
792
	elseif (isset($_GET['debug']))
793
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
794
795
	if (!empty($modSettings['queryless_urls']) && detectServer()->supportRewrite())
796
	{
797
		if (defined('SID') && SID != '')
798
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~', 'redirectexit_callback', $setLocation);
799
		else
800
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~', 'redirectexit_callback', $setLocation);
801
	}
802
803
	// Maybe integrations want to change where we are heading?
804
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh));
805
806
	// We send a Refresh header only in special cases because Location looks better. (and is quicker...)
807
	if ($refresh)
808
		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 808
  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 808
  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 808
  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 73
  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 73
  2. $setLocation is passed through strtr()
    in sources/Subs.php on line 808
  5. Path: Read from $_REQUEST, and $_REQUEST['search'] is escaped by urlencode() for all (url-encoded) context(s), and $scripturl . '?action=memberlist;sa=search;fields=name,email;search=' . urlencode($_REQUEST['search']) is passed to redirectexit() in sources/controllers/Search.controller.php on line 80
  1. Read from $_REQUEST, and $_REQUEST['search'] is escaped by urlencode() for all (url-encoded) context(s), and $scripturl . '?action=memberlist;sa=search;fields=name,email;search=' . urlencode($_REQUEST['search']) is passed to redirectexit()
    in sources/controllers/Search.controller.php on line 80
  2. $setLocation is passed through strtr()
    in sources/Subs.php on line 808
  6. Path: Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit() in sources/Load.php on line 494
  1. Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit()
    in sources/Load.php on line 494
  2. $setLocation is passed through strtr()
    in sources/Subs.php on line 808
  7. Path: Read from $_GET, and $_GET is passed through each(), and $k is assigned in sources/Load.php on line 1654
  1. Read from $_GET, and $_GET is passed through each(), and $k is assigned
    in sources/Load.php on line 1654
  2. 'wwwRedirect;' . $k . '=' . $v is passed to redirectexit()
    in sources/Load.php on line 1656
  3. $setLocation is passed through strtr()
    in sources/Subs.php on line 808
  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 147
  3. Data_Validator::$_data is assigned
    in sources/subs/DataValidator.class.php on line 249
  4. Tainted property Data_Validator::$_data is read
    in sources/subs/DataValidator.class.php on line 281
  5. Data_Validator::validation_data() returns tainted data
    in sources/subs/HttpReq.class.php on line 359
  6. HttpReq::cleanValue() returns tainted data, and HttpReq::$_param is assigned
    in sources/subs/HttpReq.class.php on line 219
  7. Tainted property HttpReq::$_param is read
    in sources/subs/HttpReq.class.php on line 190
  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 808

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...
809
	else
810
		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 810
  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 810
  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 810
  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 73
  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 73
  2. $setLocation is passed through str_replace()
    in sources/Subs.php on line 810
  5. Path: Read from $_REQUEST, and $_REQUEST['search'] is escaped by urlencode() for all (url-encoded) context(s), and $scripturl . '?action=memberlist;sa=search;fields=name,email;search=' . urlencode($_REQUEST['search']) is passed to redirectexit() in sources/controllers/Search.controller.php on line 80
  1. Read from $_REQUEST, and $_REQUEST['search'] is escaped by urlencode() for all (url-encoded) context(s), and $scripturl . '?action=memberlist;sa=search;fields=name,email;search=' . urlencode($_REQUEST['search']) is passed to redirectexit()
    in sources/controllers/Search.controller.php on line 80
  2. $setLocation is passed through str_replace()
    in sources/Subs.php on line 810
  6. Path: Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit() in sources/Load.php on line 494
  1. Read from $_REQUEST, and 'topic=' . $topic . '.msg' . $_REQUEST['msg'] . '#msg' . $_REQUEST['msg'] is passed to redirectexit()
    in sources/Load.php on line 494
  2. $setLocation is passed through str_replace()
    in sources/Subs.php on line 810
  7. Path: Read from $_GET, and $_GET is passed through each(), and $k is assigned in sources/Load.php on line 1654
  1. Read from $_GET, and $_GET is passed through each(), and $k is assigned
    in sources/Load.php on line 1654
  2. 'wwwRedirect;' . $k . '=' . $v is passed to redirectexit()
    in sources/Load.php on line 1656
  3. $setLocation is passed through str_replace()
    in sources/Subs.php on line 810
  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 147
  3. Data_Validator::$_data is assigned
    in sources/subs/DataValidator.class.php on line 249
  4. Tainted property Data_Validator::$_data is read
    in sources/subs/DataValidator.class.php on line 281
  5. Data_Validator::validation_data() returns tainted data
    in sources/subs/HttpReq.class.php on line 359
  6. HttpReq::cleanValue() returns tainted data, and HttpReq::$_param is assigned
    in sources/subs/HttpReq.class.php on line 219
  7. Tainted property HttpReq::$_param is read
    in sources/subs/HttpReq.class.php on line 190
  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 810

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...
811
812
	// Debugging.
813
	if ($db_show_debug === true)
814
		$_SESSION['debug_redirect'] = Debug::get()->get_db();
815
816
	obExit(false);
817
}
818
819
/**
820
 * URL fixer for redirect exit
821
 *
822
 * What it does:
823
 *
824
 * - Similar to the callback function used in ob_sessrewrite
825
 * - Evoked by enabling queryless_urls for systems that support that function
826
 *
827
 * @param mixed[] $matches results from the calling preg
828
 */
829
function redirectexit_callback($matches)
830
{
831
	global $scripturl;
832
833
	if (defined('SID') && SID != '')
834
		return $scripturl . '/' . strtr($matches[1], '&;=', '//,') . '.html?' . SID . (isset($matches[2]) ? $matches[2] : '');
835 View Code Duplication
	else
836
		return $scripturl . '/' . strtr($matches[1], '&;=', '//,') . '.html' . (isset($matches[2]) ? $matches[2] : '');
837
}
838
839
/**
840
 * Ends execution.
841
 *
842
 * What it does:
843
 *
844
 * - Takes care of template loading and remembering the previous URL.
845
 * - Calls ob_start() with ob_sessrewrite to fix URLs if necessary.
846
 *
847
 * @event integrate_invalid_old_url allows adding to "from" urls we don't save
848
 * @event integrate_exit inform portal, etc. that we're integrated with to exit
849
 * @param bool|null $header = null Output the header
850
 * @param bool|null $do_footer = null Output the footer
851
 * @param bool $from_index = false If we're coming from index.php
852
 * @param bool $from_fatal_error = false If we are exiting due to a fatal error
853
 *
854
 * @throws Elk_Exception
855
 */
856
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
857
{
858
	global $context, $txt, $db_show_debug;
859
860
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
861
862
	// Attempt to prevent a recursive loop.
863
	++$level;
864
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
865
		exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The function obExit() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
866
867
	if ($from_fatal_error)
868
		$has_fatal_error = true;
869
870
	// Clear out the stat cache.
871
	trackStats();
872
873
	Notifications::getInstance()->send();
874
875
	// If we have mail to send, send it.
876
	if (!empty($context['flush_mail']))
877
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
878
		AddMailQueue(true);
879
880
	$do_header = $header === null ? !$header_done : $header;
881
	if ($do_footer === null)
882
		$do_footer = $do_header;
883
884
	// Has the template/header been done yet?
885
	if ($do_header)
886
	{
887
		// Was the page title set last minute? Also update the HTML safe one.
888
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
889
			$context['page_title_html_safe'] = Util::htmlspecialchars(un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
890
891
		// Start up the session URL fixer.
892
		ob_start('ob_sessrewrite');
893
894
		call_integration_buffer();
895
896
		// Display the screen in the logical order.
897
		template_header();
898
		$header_done = true;
899
	}
900
901
	if ($do_footer)
902
	{
903
		// Show the footer.
904
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
905
906
		// Just so we don't get caught in an endless loop of errors from the footer...
907
		if (!$footer_done)
908
		{
909
			$footer_done = true;
910
			template_footer();
911
912
			// Add $db_show_debug = true; to Settings.php if you want to show the debugging information.
913
			// (since this is just debugging... it's okay that it's after </html>.)
914
			if ($db_show_debug === true)
915
				if (!isset($_REQUEST['xml']) && ((!isset($_GET['action']) || $_GET['action'] != 'viewquery') && !isset($_GET['api'])))
916
					Debug::get()->display();
917
		}
918
	}
919
920
	// Need user agent
921
	$req = request();
922
923
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
924
	$invalid_old_url = array(
925
		'action=dlattach',
926
		'action=jsoption',
927
		'action=viewadminfile',
928
		';xml',
929
		';api',
930
	);
931
	call_integration_hook('integrate_invalid_old_url', array(&$invalid_old_url));
932
	$make_old = true;
933
	foreach ($invalid_old_url as $url)
934
	{
935
		if (strpos($_SERVER['REQUEST_URL'], $url) !== false)
936
		{
937
			$make_old = false;
938
			break;
939
		}
940
	}
941
	if ($make_old === true)
942
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
943
944
	// For session check verification.... don't switch browsers...
945
	$_SESSION['USER_AGENT'] = $req->user_agent();
946
947
	// Hand off the output to the portal, etc. we're integrated with.
948
	call_integration_hook('integrate_exit', array($do_footer));
949
950
	// Don't exit if we're coming from index.php; that will pass through normally.
951
	if (!$from_index)
952
		exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The function obExit() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
953
}
954
955
/**
956
 * Sets the class of the current topic based on is_very_hot, veryhot, hot, etc
957
 *
958
 * @param mixed[] $topic_context array of topic information
959
 */
960
function determineTopicClass(&$topic_context)
961
{
962
	// Set topic class depending on locked status and number of replies.
963 1
	if ($topic_context['is_very_hot'])
964 1
		$topic_context['class'] = 'veryhot';
965 1
	elseif ($topic_context['is_hot'])
966
		$topic_context['class'] = 'hot';
967
	else
968 1
		$topic_context['class'] = 'normal';
969
970 1
	$topic_context['class'] .= !empty($topic_context['is_poll']) ? '_poll' : '_post';
971
972 1
	if ($topic_context['is_locked'])
973 1
		$topic_context['class'] .= '_locked';
974
975 1
	if ($topic_context['is_sticky'])
976 1
		$topic_context['class'] .= '_sticky';
977
978
	// This is so old themes will still work.
979
	// @deprecated since 1.0 do not rely on it
980 1
	$topic_context['extended_class'] = &$topic_context['class'];
981 1
}
982
983
/**
984
 * Sets up the basic theme context stuff.
985
 *
986
 * @param bool $forceload = false
987
 */
988
function setupThemeContext($forceload = false)
989
{
990
	return theme()->setupThemeContext($forceload);
991
}
992
993
/**
994
 * Helper function to set the system memory to a needed value
995
 *
996
 * What it does:
997
 *
998
 * - If the needed memory is greater than current, will attempt to get more
999
 * - If in_use is set to true, will also try to take the current memory usage in to account
1000
 *
1001
 * @param string $needed The amount of memory to request, if needed, like 256M
1002
 * @param bool $in_use Set to true to account for current memory usage of the script
1003
 *
1004
 * @return boolean true if we have at least the needed memory
1005
 * @deprecated since 1.1
1006
 */
1007
function setMemoryLimit($needed, $in_use = false)
1008
{
1009
	return detectServer()->setMemoryLimit($needed, $in_use);
1010
}
1011
1012
/**
1013
 * Helper function to convert memory string settings to bytes
1014
 *
1015
 * @param string $val The byte string, like 256M or 1G
1016
 *
1017
 * @return integer The string converted to a proper integer in bytes
1018
 */
1019
function memoryReturnBytes($val)
1020
{
1021 1
	if (is_integer($val))
1022 1
		return $val;
1023
1024
	// Separate the number from the designator
1025 1
	$val = trim($val);
1026 1
	$num = intval(substr($val, 0, strlen($val) - 1));
1027 1
	$last = strtolower(substr($val, -1));
1028
1029
	// Convert to bytes
1030
	switch ($last)
1031
	{
1032
		// fall through select g = 1024*1024*1024
1033 1
		case 'g':
1034 1
			$num *= 1024;
1035
		// fall through select m = 1024*1024
1036 1
		case 'm':
1037 1
			$num *= 1024;
1038
		// fall through select k = 1024
1039 1
		case 'k':
1040 1
			$num *= 1024;
1041 1
	}
1042
1043 1
	return $num;
1044
}
1045
1046
/**
1047
 * Wrapper function for set_time_limit
1048
 *
1049
 * When called, attempts to restart the timeout counter from zero.
1050
 *
1051
 * This sets the maximum time in seconds a script is allowed to run before it is terminated by the parser.
1052
 * You can not change this setting with ini_set() when running in safe mode.
1053
 * Your web server can have other timeout configurations that may also interrupt PHP execution.
1054
 * Apache has a Timeout directive and IIS has a CGI timeout function.
1055
 * Security extension may also disable this function, such as Suhosin
1056
 * Hosts may add this to the disabled_functions list in php.ini
1057
 *
1058
 * If the current time limit is not unlimited it is possible to decrease the
1059
 * total time limit if the sum of the new time limit and the current time spent
1060
 * running the script is inferior to the original time limit. It is inherent to
1061
 * the way set_time_limit() works, it should rather be called with an
1062
 * appropriate value every time you need to allocate a certain amount of time
1063
 * to execute a task than only once at the beginning of the script.
1064
 *
1065
 * Before calling set_time_limit(), we check if this function is available
1066
 *
1067
 * @param int $time_limit The time limit
1068
 * @param bool $server_reset whether to reset the server timer or not
1069
 * @deprecated since 1.1
1070
 */
1071
function setTimeLimit($time_limit, $server_reset = true)
1072
{
1073
	return detectServer()->setTimeLimit($time_limit, $server_reset);
1074
}
1075
1076
/**
1077
 * This is the only template included in the sources.
1078
 */
1079
function template_rawdata()
1080
{
1081
	return theme()->template_rawdata();
1082
}
1083
1084
/**
1085
 * The header template
1086
 */
1087
function template_header()
1088
{
1089
	return theme()->template_header();
1090
}
1091
1092
/**
1093
 * Show the copyright.
1094
 */
1095
function theme_copyright()
1096
{
1097
	return theme()->theme_copyright();
1098
}
1099
1100
/**
1101
 * The template footer
1102
 */
1103
function template_footer()
1104
{
1105
	return theme()->template_footer();
1106
}
1107
1108
/**
1109
 * Output the Javascript files
1110
 *
1111
 * What it does:
1112
 *
1113
 * - tabbing in this function is to make the HTML source look proper
1114
 * - outputs jQuery/jQueryUI from the proper source (local/CDN)
1115
 * - if deferred is set function will output all JS (source & inline) set to load at page end
1116
 * - if the admin option to combine files is set, will use Combiner.class
1117
 *
1118
 * @param bool $do_deferred = false
1119
 */
1120
function template_javascript($do_deferred = false)
1121
{
1122
	theme()->template_javascript($do_deferred);
1123
	return;
1124
}
1125
1126
/**
1127
 * Output the CSS files
1128
 *
1129
 * What it does:
1130
 *  - If the admin option to combine files is set, will use Combiner.class
1131
 */
1132
function template_css()
1133
{
1134
	theme()->template_css();
1135
	return;
1136
}
1137
1138
/**
1139
 * Calls on template_show_error from index.template.php to show warnings
1140
 * and security errors for admins
1141
 */
1142
function template_admin_warning_above()
1143
{
1144
	theme()->template_admin_warning_above();
1145
	return;
1146
}
1147
1148
/**
1149
 * Convert a single IP to a ranged IP.
1150
 *
1151
 * - Internal function used to convert a user-readable format to a format suitable for the database.
1152
 *
1153
 * @param string $fullip A full dot notation IP address
1154
 *
1155
 * @return array|string 'unknown' if the ip in the input was '255.255.255.255'
1156
 */
1157
function ip2range($fullip)
1158
{
1159
	// If its IPv6, validate it first.
1160
	if (isValidIPv6($fullip) !== false)
1161
	{
1162
		$ip_parts = explode(':', expandIPv6($fullip, false));
1163
		$ip_array = array();
1164
1165
		if (count($ip_parts) != 8)
1166
			return array();
1167
1168
		for ($i = 0; $i < 8; $i++)
1169
		{
1170
			if ($ip_parts[$i] == '*')
1171
				$ip_array[$i] = array('low' => '0', 'high' => hexdec('ffff'));
1172 View Code Duplication
			elseif (preg_match('/^([0-9A-Fa-f]{1,4})\-([0-9A-Fa-f]{1,4})$/', $ip_parts[$i], $range) == 1)
1173
				$ip_array[$i] = array('low' => hexdec($range[1]), 'high' => hexdec($range[2]));
1174
			elseif (is_numeric(hexdec($ip_parts[$i])))
1175
				$ip_array[$i] = array('low' => hexdec($ip_parts[$i]), 'high' => hexdec($ip_parts[$i]));
1176
		}
1177
1178
		return $ip_array;
1179
	}
1180
1181
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
1182
	if ($fullip == 'unknown')
1183
		$fullip = '255.255.255.255';
1184
1185
	$ip_parts = explode('.', $fullip);
1186
	$ip_array = array();
1187
1188
	if (count($ip_parts) != 4)
1189
		return array();
1190
1191
	for ($i = 0; $i < 4; $i++)
1192
	{
1193
		if ($ip_parts[$i] == '*')
1194
			$ip_array[$i] = array('low' => '0', 'high' => '255');
1195 View Code Duplication
		elseif (preg_match('/^(\d{1,3})\-(\d{1,3})$/', $ip_parts[$i], $range) == 1)
1196
			$ip_array[$i] = array('low' => $range[1], 'high' => $range[2]);
1197
		elseif (is_numeric($ip_parts[$i]))
1198
			$ip_array[$i] = array('low' => $ip_parts[$i], 'high' => $ip_parts[$i]);
1199
	}
1200
1201
	// Makes it simpler to work with.
1202
	$ip_array[4] = array('low' => 0, 'high' => 0);
1203
	$ip_array[5] = array('low' => 0, 'high' => 0);
1204
	$ip_array[6] = array('low' => 0, 'high' => 0);
1205
	$ip_array[7] = array('low' => 0, 'high' => 0);
1206
1207
	return $ip_array;
1208
}
1209
1210
/**
1211
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
1212
 *
1213
 * @param string $ip A full dot notation IP address
1214
 *
1215
 * @return string
1216
 */
1217
function host_from_ip($ip)
1218
{
1219
	global $modSettings;
1220
1221
	$cache = Cache::instance();
1222
1223
	$host = '';
1224
	if ($cache->getVar($host, 'hostlookup-' . $ip, 600) || empty($ip))
1225
		return $host;
1226
1227
	$t = microtime(true);
1228
1229
	// Try the Linux host command, perhaps?
1230
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
1231
	{
1232
		if (!isset($modSettings['host_to_dis']))
1233
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
1234
		else
1235
			$test = @shell_exec('host ' . @escapeshellarg($ip));
1236
1237
		// Did host say it didn't find anything?
1238
		if (strpos($test, 'not found') !== false)
1239
			$host = '';
1240
		// Invalid server option?
1241
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
1242
			updateSettings(array('host_to_dis' => 1));
1243
		// Maybe it found something, after all?
1244
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
1245
			$host = $match[1];
1246
	}
1247
1248
	// This is nslookup; usually only Windows, but possibly some Unix?
1249
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
1250
	{
1251
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
1252
1253 View Code Duplication
		if (strpos($test, 'Non-existent domain') !== false)
1254
			$host = '';
1255
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
1256
			$host = $match[1];
1257
	}
1258
1259
	// This is the last try :/.
1260
	if (!isset($host) || $host === false)
1261
		$host = @gethostbyaddr($ip);
1262
1263
	// It took a long time, so let's cache it!
1264
	if (microtime(true) - $t > 0.5)
1265
		$cache->put('hostlookup-' . $ip, $host, 600);
1266
1267
	return $host;
1268
}
1269
1270
/**
1271
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
1272
 *
1273
 * @param string $text The string to process
1274
 * @param int $max_chars defaults to 20
1275
 *     - if encrypt = true this is the maximum number of bytes to use in integer hashes (for searching)
1276
 *     - if encrypt = false this is the maximum number of letters in each word
1277
 * @param bool $encrypt = false Used for custom search indexes to return an int[] array representing the words
1278
 */
1279
function text2words($text, $max_chars = 20, $encrypt = false)
1280
{
1281
	// Step 1: Remove entities/things we don't consider words:
1282 10
	$words = preg_replace('~(?:[\x0B\0\x{A0}\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~u', ' ', strtr($text, array('<br />' => ' ')));
1283
1284
	// Step 2: Entities we left to letters, where applicable, lowercase.
1285 10
	$words = un_htmlspecialchars(Util::strtolower($words));
1286
1287
	// Step 3: Ready to split apart and index!
1288 10
	$words = explode(' ', $words);
1289
1290
	if ($encrypt)
1291 10
	{
1292
		// Range of characters that crypt will produce (0-9, a-z, A-Z .)
1293
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
1294
		$returned_ints = array();
1295
		foreach ($words as $word)
1296
		{
1297
			if (($word = trim($word, '-_\'')) !== '')
1298
			{
1299
				// Get a crypt representation of this work
1300
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
1301
				$total = 0;
1302
1303
				// Create an integer representation
1304
				for ($i = 0; $i < $max_chars; $i++)
1305
					$total += $possible_chars[ord($encrypted{$i})] * pow(63, $i);
1306
1307
				// Return the value
1308
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
1309
			}
1310
		}
1311
		return array_unique($returned_ints);
1312
	}
1313
	else
1314
	{
1315
		// Trim characters before and after and add slashes for database insertion.
1316 10
		$returned_words = array();
1317 10
		foreach ($words as $word)
1318 10
			if (($word = trim($word, '-_\'')) !== '')
1319 10
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
1320
1321
		// Filter out all words that occur more than once.
1322 10
		return array_unique($returned_words);
1323
	}
1324
}
1325
1326
/**
1327
 * Creates an image/text button
1328
 *
1329
 * @param string $name
1330
 * @param string $alt
1331
 * @param string $label = ''
1332
 * @param string|boolean $custom = ''
1333
 * @param boolean $force_use = false
1334
 *
1335
 * @return string
1336
 *
1337
 * @deprecated since 1.0 this will be removed at some point, do not rely on this function
1338
 */
1339
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
1340
{
1341
	global $settings, $txt;
1342
1343
	// Does the current loaded theme have this and we are not forcing the usage of this function?
1344
	if (function_exists('template_create_button') && !$force_use)
1345
		return template_create_button($name, $alt, $label = '', $custom = '');
1346
1347
	if (!$settings['use_image_buttons'])
1348
		return $txt[$alt];
1349
	elseif (!empty($settings['use_buttons']))
1350
		return '<img src="' . $settings['images_url'] . '/buttons/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . ' />' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
1351
	else
1352
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . ' />';
1353
}
1354
1355
/**
1356
 * Sets up all of the top menu buttons
1357
 *
1358
 * What it does:
1359
 *
1360
 * - Defines every master item in the menu, as well as any sub-items
1361
 * - Ensures the chosen action is set so the menu is highlighted
1362
 * - Saves them in the cache if it is available and on
1363
 * - Places the results in $context
1364
 */
1365
function setupMenuContext()
1366
{
1367
	return theme()->setupMenuContext();
1368
}
1369
1370
/**
1371
 * Generate a random seed and ensure it's stored in settings.
1372
 * @deprecated
1373
 */
1374
function elk_seed_generator()
1375
{
1376
	global $modSettings;
1377
1378
	// Change the seed.
1379
	if (mt_rand(1, 250) == 69 || empty($modSettings['rand_seed']))
1380
		updateSettings(array('rand_seed' => mt_rand()));
1381
}
1382
1383
/**
1384
 * Process functions of an integration hook.
1385
 *
1386
 * What it does:
1387
 *
1388
 * - Calls all functions of the given hook.
1389
 * - Supports static class method calls.
1390
 *
1391
 * @param string $hook The name of the hook to call
1392
 * @param mixed[] $parameters = array() Parameters to pass to the hook
1393
 *
1394
 * @return mixed[] the results of the functions
1395
 */
1396
function call_integration_hook($hook, $parameters = array())
1397
{
1398 37
	return Hooks::get()->hook($hook, $parameters);
1399
}
1400
1401
/**
1402
 * Includes files for hooks that only do that (i.e. integrate_pre_include)
1403
 *
1404
 * @param string $hook The name to include
1405
 */
1406
function call_integration_include_hook($hook)
1407
{
1408 8
	Hooks::get()->include_hook($hook);
1409 8
}
1410
1411
/**
1412
 * Special hook call executed during obExit
1413
 */
1414
function call_integration_buffer()
1415
{
1416
	Hooks::get()->buffer_hook();
1417
}
1418
1419
/**
1420
 * Add a function for integration hook.
1421
 *
1422
 * - Does nothing if the function is already added.
1423
 *
1424
 * @param string $hook The name of the hook to add
1425
 * @param string $function The function associated with the hook
1426
 * @param string $file The file that contains the function
1427
 * @param bool $permanent = true if true, updates the value in settings table
1428
 */
1429
function add_integration_function($hook, $function, $file = '', $permanent = true)
1430
{
1431 1
	Hooks::get()->add($hook, $function, $file, $permanent);
1432 1
}
1433
1434
/**
1435
 * Remove an integration hook function.
1436
 *
1437
 * What it does:
1438
 *
1439
 * - Removes the given function from the given hook.
1440
 * - Does nothing if the function is not available.
1441
 *
1442
 * @param string $hook The name of the hook to remove
1443
 * @param string $function The name of the function
1444
 * @param string $file The file its located in
1445
 */
1446
function remove_integration_function($hook, $function, $file = '')
1447
{
1448 1
	Hooks::get()->remove($hook, $function, $file);
1449 1
}
1450
1451
/**
1452
 * Decode numeric html entities to their UTF8 equivalent character.
1453
 *
1454
 * What it does:
1455
 *
1456
 * - Callback function for preg_replace_callback in subs-members
1457
 * - Uses capture group 2 in the supplied array
1458
 * - Does basic scan to ensure characters are inside a valid range
1459
 *
1460
 * @param mixed[] $matches matches from a preg_match_all
1461
 *
1462
 * @return string $string
1463
 */
1464
function replaceEntities__callback($matches)
1465
{
1466
	if (!isset($matches[2]))
1467
		return '';
1468
1469
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
1470
1471
	// remove left to right / right to left overrides
1472
	if ($num === 0x202D || $num === 0x202E)
1473
		return '';
1474
1475
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
1476
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
1477
		return '&#' . $num . ';';
1478
1479
	// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
1480
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
1481
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
1482
		return '';
1483
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1484
	elseif ($num < 0x80)
1485
		return chr($num);
1486
	// <0x800 (2048)
1487 View Code Duplication
	elseif ($num < 0x800)
1488
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1489
	// < 0x10000 (65536)
1490 View Code Duplication
	elseif ($num < 0x10000)
1491
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1492
	// <= 0x10FFFF (1114111)
1493 View Code Duplication
	else
1494
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1495
}
1496
1497
/**
1498
 * Converts html entities to utf8 equivalents
1499
 *
1500
 * What it does:
1501
 *
1502
 * - Callback function for preg_replace_callback
1503
 * - Uses capture group 1 in the supplied array
1504
 * - Does basic checks to keep characters inside a viewable range.
1505
 *
1506
 * @param mixed[] $matches array of matches as output from preg_match_all
1507
 *
1508
 * @return string $string
1509
 */
1510
function fixchar__callback($matches)
1511
{
1512
	if (!isset($matches[1]))
1513
		return '';
1514
1515
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
1516
1517
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
1518
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
1519
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
1520
		return '';
1521
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
1522
	elseif ($num < 0x80)
1523
		return chr($num);
1524
	// <0x800 (2048)
1525 View Code Duplication
	elseif ($num < 0x800)
1526
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
1527
	// < 0x10000 (65536)
1528 View Code Duplication
	elseif ($num < 0x10000)
1529
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1530
	// <= 0x10FFFF (1114111)
1531 View Code Duplication
	else
1532
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
1533
}
1534
1535
/**
1536
 * Strips out invalid html entities, replaces others with html style &#123; codes
1537
 *
1538
 * What it does:
1539
 *
1540
 * - Callback function used of preg_replace_callback in various $ent_checks,
1541
 * - For example strpos, strlen, substr etc
1542
 *
1543
 * @param mixed[] $matches array of matches for a preg_match_all
1544
 *
1545
 * @return string
1546
 */
1547
function entity_fix__callback($matches)
1548
{
1549 3
	if (!isset($matches[2]))
1550 3
		return '';
1551
1552 3
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
1553
1554
	// We don't allow control characters, characters out of range, byte markers, etc
1555 3 View Code Duplication
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
1556 3
		return '';
1557
	else
1558 1
		return '&#' . $num . ';';
1559
}
1560
1561
/**
1562
 * Retrieve additional search engines, if there are any, as an array.
1563
 *
1564
 * @return mixed[] array of engines
1565
 */
1566
function prepareSearchEngines()
1567
{
1568
	global $modSettings;
1569
1570
	$engines = array();
1571
	if (!empty($modSettings['additional_search_engines']))
1572
	{
1573
		$search_engines = Util::unserialize($modSettings['additional_search_engines']);
1574
		foreach ($search_engines as $engine)
1575
			$engines[strtolower(preg_replace('~[^A-Za-z0-9 ]~', '', $engine['name']))] = $engine;
1576
	}
1577
1578
	return $engines;
1579
}
1580
1581
/**
1582
 * This function receives a request handle and attempts to retrieve the next result.
1583
 *
1584
 * What it does:
1585
 *
1586
 * - It is used by the controller callbacks from the template, such as
1587
 * posts in topic display page, posts search results page, or personal messages.
1588
 *
1589
 * @param resource $messages_request holds a query result
1590
 * @param bool $reset
1591
 *
1592
 * @return integer|boolean
1593
 */
1594
function currentContext($messages_request, $reset = false)
1595
{
1596
	// Can't work with a database without a database :P
1597
	$db = database();
1598
1599
	// Start from the beginning...
1600
	if ($reset)
1601
		return $db->data_seek($messages_request, 0);
1602
1603
	// If the query has already returned false, get out of here
1604
	if (empty($messages_request))
1605
		return false;
1606
1607
	// Attempt to get the next message.
1608
	$message = $db->fetch_assoc($messages_request);
1609
	if (!$message)
1610
	{
1611
		$db->free_result($messages_request);
1612
1613
		return false;
1614
	}
1615
1616
	return $message;
1617
}
1618
1619
/**
1620
 * Helper function to insert an array in to an existing array
1621
 *
1622
 * What it does:
1623
 *
1624
 * - Intended for addon use to allow such things as
1625
 * - Adding in a new menu item to an existing menu array
1626
 *
1627
 * @param mixed[] $input the array we will insert to
1628
 * @param string $key the key in the array that we are looking to find for the insert action
1629
 * @param mixed[] $insert the actual data to insert before or after the key
1630
 * @param string $where adding before or after
1631
 * @param bool $assoc if the array is a assoc array with named keys or a basic index array
1632
 * @param bool $strict search for identical elements, this means it will also check the types of the needle.
1633
 */
1634
function elk_array_insert($input, $key, $insert, $where = 'before', $assoc = true, $strict = false)
1635
{
1636
	// Search for key names or values
1637
	if ($assoc)
1638
		$position = array_search($key, array_keys($input), $strict);
1639
	else
1640
		$position = array_search($key, $input, $strict);
1641
1642
	// If the key is not found, just insert it at the end
1643
	if ($position === false)
1644
		return array_merge($input, $insert);
1645
1646
	if ($where === 'after')
1647
		$position++;
1648
1649
	// Insert as first
1650
	if ($position === 0)
1651
		$input = array_merge($insert, $input);
1652
	else
1653
		$input = array_merge(array_slice($input, 0, $position), $insert, array_slice($input, $position));
1654
1655
	return $input;
1656
}
1657
1658
/**
1659
 * Run a scheduled task now
1660
 *
1661
 * What it does:
1662
 *
1663
 * - From time to time it may be necessary to fire a scheduled task ASAP
1664
 * - This function sets the scheduled task to be called before any other one
1665
 *
1666
 * @param string $task the name of a scheduled task
1667
 */
1668
function scheduleTaskImmediate($task)
1669
{
1670
	global $modSettings;
1671
1672
	if (!isset($modSettings['scheduleTaskImmediate']))
1673
		$scheduleTaskImmediate = array();
1674
	else
1675
		$scheduleTaskImmediate = Util::unserialize($modSettings['scheduleTaskImmediate']);
1676
1677
	// If it has not been scheduled, the do so now
1678
	if (!isset($scheduleTaskImmediate[$task]))
1679
	{
1680
		$scheduleTaskImmediate[$task] = 0;
1681
		updateSettings(array('scheduleTaskImmediate' => serialize($scheduleTaskImmediate)));
1682
1683
		require_once(SUBSDIR . '/ScheduledTasks.subs.php');
1684
1685
		// Ensure the task is on
1686
		toggleTaskStatusByName($task, true);
1687
1688
		// Before trying to run it **NOW** :P
1689
		calculateNextTrigger($task, true);
1690
	}
1691
}
1692
1693
/**
1694
 * For diligent people: remove scheduleTaskImmediate when done, otherwise
1695
 * a maximum of 10 executions is allowed
1696
 *
1697
 * @param string $task the name of a scheduled task
1698
 * @param bool $calculateNextTrigger if recalculate the next task to execute
1699
 */
1700
function removeScheduleTaskImmediate($task, $calculateNextTrigger = true)
1701
{
1702
	global $modSettings;
1703
1704
	// Not on, bail
1705
	if (!isset($modSettings['scheduleTaskImmediate']))
1706
		return;
1707
	else
1708
		$scheduleTaskImmediate = Util::unserialize($modSettings['scheduleTaskImmediate']);
1709
1710
	// Clear / remove the task if it was set
1711
	if (isset($scheduleTaskImmediate[$task]))
1712
	{
1713
		unset($scheduleTaskImmediate[$task]);
1714
		updateSettings(array('scheduleTaskImmediate' => serialize($scheduleTaskImmediate)));
1715
1716
		// Recalculate the next task to execute
1717
		if ($calculateNextTrigger)
1718
		{
1719
			require_once(SUBSDIR . '/ScheduledTasks.subs.php');
1720
			calculateNextTrigger($task);
1721
		}
1722
	}
1723
}
1724
1725
/**
1726
 * Helper function to replace commonly used urls in text strings
1727
 *
1728
 * @event integrate_basic_url_replacement add additional place holder replacements
1729
 * @param string $string the string to inject URLs into
1730
 *
1731
 * @return string the input string with the place-holders replaced with
1732
 *           the correct URLs
1733
 */
1734
function replaceBasicActionUrl($string)
1735
{
1736 1
	global $scripturl, $context, $boardurl;
1737 1
	static $find_replace = null;
1738
1739 1
	if ($find_replace === null)
1740 1
	{
1741
		$find_replace = array(
1742 1
			'{forum_name}' => $context['forum_name'],
1743 1
			'{forum_name_html_safe}' => $context['forum_name_html_safe'],
1744 1
			'{forum_name_html_unsafe}' => un_htmlspecialchars($context['forum_name_html_safe']),
1745 1
			'{script_url}' => $scripturl,
1746 1
			'{board_url}' => $boardurl,
1747 1
			'{login_url}' => $scripturl . '?action=login',
1748 1
			'{register_url}' => $scripturl . '?action=register',
1749 1
			'{activate_url}' => $scripturl . '?action=register;sa=activate',
1750 1
			'{help_url}' => $scripturl . '?action=help',
1751 1
			'{admin_url}' => $scripturl . '?action=admin',
1752 1
			'{moderate_url}' => $scripturl . '?action=moderate',
1753 1
			'{recent_url}' => $scripturl . '?action=recent',
1754 1
			'{search_url}' => $scripturl . '?action=search',
1755 1
			'{who_url}' => $scripturl . '?action=who',
1756 1
			'{credits_url}' => $scripturl . '?action=who;sa=credits',
1757 1
			'{calendar_url}' => $scripturl . '?action=calendar',
1758 1
			'{memberlist_url}' => $scripturl . '?action=memberlist',
1759 1
			'{stats_url}' => $scripturl . '?action=stats',
1760 1
		);
1761 1
		call_integration_hook('integrate_basic_url_replacement', array(&$find_replace));
1762 1
	}
1763
1764 1
	return str_replace(array_keys($find_replace), array_values($find_replace), $string);
1765
}
1766
1767
/**
1768
 * This function creates a new GenericList from all the passed options.
1769
 *
1770
 * What it does:
1771
 *
1772
 * - Calls integration hook integrate_list_"unique_list_id" to allow easy modifying
1773
 *
1774
 * @event integrate_list_$listID called before every createlist to allow access to its listoptions
1775
 * @param mixed[] $listOptions associative array of option => value
1776
 */
1777
function createList($listOptions)
1778
{
1779
	call_integration_hook('integrate_list_' . $listOptions['id'], array(&$listOptions));
1780
1781
	$list = new Generic_List($listOptions);
1782
1783
	$list->buildList();
1784
}
1785
1786
/**
1787
 * This handy function retrieves a Request instance and passes it on.
1788
 *
1789
 * What it does:
1790
 *
1791
 * - To get hold of a Request, you can use this function or directly Request::instance().
1792
 * - This is for convenience, it simply delegates to Request::instance().
1793
 */
1794
function request()
1795
{
1796 18
	return Request::instance();
1797
}
1798
1799
/**
1800
 * Meant to replace any usage of $db_last_error.
1801
 *
1802
 * What it does:
1803
 *
1804
 * - Reads the file db_last_error.txt, if a time() is present returns it,
1805
 * otherwise returns 0.
1806
 */
1807
function db_last_error()
1808
{
1809
	$time = trim(file_get_contents(BOARDDIR . '/db_last_error.txt'));
1810
1811
	if (preg_match('~^\d{10}$~', $time) === 1)
1812
		return $time;
1813
	else
1814
		return 0;
1815
}
1816
1817
/**
1818
 * This function has the only task to retrieve the correct prefix to be used
1819
 * in responses.
1820
 *
1821
 * @return string - The prefix in the default language of the forum
1822
 */
1823
function response_prefix()
1824
{
1825 1
	global $language, $user_info, $txt;
1826 1
	static $response_prefix = null;
1827
1828 1
	$cache = Cache::instance();
1829
1830
	// Get a response prefix, but in the forum's default language.
1831 1
	if ($response_prefix === null && (!$cache->getVar($response_prefix, 'response_prefix') || !$response_prefix))
1832 1
	{
1833 1 View Code Duplication
		if ($language === $user_info['language'])
1834 1
			$response_prefix = $txt['response_prefix'];
1835
		else
1836
		{
1837
			loadLanguage('index', $language, false);
1838
			$response_prefix = $txt['response_prefix'];
1839
			loadLanguage('index');
1840
		}
1841
1842 1
		$cache->put('response_prefix', $response_prefix, 600);
1843 1
	}
1844
1845 1
	return $response_prefix;
1846
}
1847
1848
/**
1849
 * A very simple function to determine if an email address is "valid" for Elkarte.
1850
 *
1851
 * - A valid email for ElkArte is something that resembles an email (filter_var) and
1852
 * is less than 255 characters (for database limits)
1853
 *
1854
 * @param string $value - The string to evaluate as valid email
1855
 *
1856
 * @return string|false - The email if valid, false if not a valid email
1857
 */
1858
function isValidEmail($value)
1859
{
1860 1
	$value = trim($value);
1861 1
	if (filter_var($value, FILTER_VALIDATE_EMAIL) && Util::strlen($value) < 255)
1862 1
		return $value;
1863
	else
1864
		return false;
1865
}
1866
1867
/**
1868
 * Adds a protocol (http/s, ftp/mailto) to the beginning of an url if missing
1869
 *
1870
 * @param string $url - The url
1871
 * @param string[] $protocols - A list of protocols to check, the first is
1872
 *                 added if none is found (optional, default array('http://', 'https://'))
1873
 *
1874
 * @return string - The url with the protocol
1875
 */
1876
function addProtocol($url, $protocols = array())
1877
{
1878 3
	if (empty($protocols))
1879 3
	{
1880 3
		$pattern = '~^(http://|https://)~i';
1881 3
		$protocols = array('http://');
1882 3
	}
1883
	else
1884
	{
1885
		$pattern = '~^(' . implode('|', array_map(function ($val) {return preg_quote($val, '~');}, $protocols)) . ')~i';
0 ignored issues
show
Coding Style introduced by
It is generally recommended to place each PHP statement on a line by itself.

Let’s take a look at an example:

// Bad
$a = 5; $b = 6; $c = 7;

// Good
$a = 5;
$b = 6;
$c = 7;
Loading history...
1886
	}
1887
1888 3
	$found = false;
1889
	$url = preg_replace_callback($pattern, function($match) use (&$found) {
1890 3
		$found = true;
1891
1892 3
		return strtolower($match[0]);
1893 3
	}, $url);
1894
1895 3
	if ($found === true)
1896 3
	{
1897 3
			return $url;
1898
	}
1899
1900 1
	return $protocols[0] . $url;
1901
}
1902
1903
/**
1904
 * Removes nested quotes from a text string.
1905
 *
1906
 * @param string $text - The body we want to remove nested quotes from
1907
 *
1908
 * @return string - The same body, just without nested quotes
1909
 */
1910
function removeNestedQuotes($text)
1911
{
1912
	global $modSettings;
1913
1914
	// Remove any nested quotes, if necessary.
1915
	if (!empty($modSettings['removeNestedQuotes']))
1916
	{
1917
		return preg_replace(array('~\n?\[quote.*?\].+?\[/quote\]\n?~is', '~^\n~', '~\[/quote\]~'), '', $text);
1918
	}
1919
	else
1920
	{
1921
		return $text;
1922
	}
1923
}
1924
1925
/**
1926
 * Change a \t to a span that will show a tab
1927
 *
1928
 * @param string $string
1929
 *
1930
 * @return string
1931
 */
1932
function tabToHtmlTab($string)
1933
{
1934 2
	return str_replace("\t", "<span class=\"tab\">\t</span>", $string);
1935
}
1936
1937
/**
1938
 * Remove <br />
1939
 *
1940
 * @param string $string
1941
 *
1942
 * @return string
1943
 */
1944
function removeBr($string)
1945
{
1946
	return str_replace('<br />', '', $string);
1947
}
1948
1949
/**
1950
 * Are we using this browser?
1951
 *
1952
 * - Wrapper function for detectBrowser
1953
 *
1954
 * @param string $browser  the browser we are checking for.
1955
 */
1956
function isBrowser($browser)
1957
{
1958 7
	global $context;
1959
1960
	// Don't know any browser!
1961 7
	if (empty($context['browser']))
1962 7
		detectBrowser();
1963
1964 7
	return !empty($context['browser'][$browser]) || !empty($context['browser']['is_' . $browser]) ? true : false;
1965
}
1966
1967
/**
1968
 * Replace all vulgar words with respective proper words. (substring or whole words..)
1969
 *
1970
 * @deprecated use censor() or Censor class
1971
 *
1972
 * What it does:
1973
 *
1974
 * - it censors the passed string.
1975
 * - if the admin setting allow_no_censored is on it does not censor unless force is also set.
1976
 * - if the admin setting allow_no_censored is off will censor words unless the user has set
1977
 * it to not censor in their profile and force is off
1978
 * - it caches the list of censored words to reduce parsing.
1979
 * - Returns the censored text
1980
 *
1981
 * @param string &$text
1982
 * @param bool $force = false
1983
 */
1984
function censorText(&$text, $force = false)
1985
{
1986
	$text = censor($text, $force);
1987
1988
	return $text;
1989
}
1990
1991
/**
1992
 * Replace all vulgar words with respective proper words. (substring or whole words..)
1993
 *
1994
 * What it does:
1995
 *
1996
 * - it censors the passed string.
1997
 * - if the admin setting allow_no_censored is on it does not censor unless force is also set.
1998
 * - if the admin setting allow_no_censored is off will censor words unless the user has set
1999
 * it to not censor in their profile and force is off
2000
 * - it caches the list of censored words to reduce parsing.
2001
 * - Returns the censored text
2002
 *
2003
 * @param string $text
2004
 * @param bool $force = false
2005
 */
2006
function censor($text, $force = false)
2007
{
2008 5
	global $modSettings;
2009 5
	static $censor = null;
2010
2011 5
	if ($censor === null)
2012 5
	{
2013 1
		$censor = new Censor(explode("\n", $modSettings['censor_vulgar']), explode("\n", $modSettings['censor_proper']), $modSettings);
2014 1
	}
2015
2016 5
	return $censor->censor($text, $force);
2017
}
2018
2019
/**
2020
 * Helper function able to determine if the current member can see at least
2021
 * one button of a button strip.
2022
 *
2023
 * @param mixed[] $button_strip
2024
 *
2025
 * @return bool
2026
 */
2027
function can_see_button_strip($button_strip)
2028
{
2029
	global $context;
2030
2031
	foreach ($button_strip as $key => $value)
2032
	{
2033
		if (!isset($value['test']) || !empty($context[$value['test']]))
2034
			return true;
2035
	}
2036
2037
	return false;
2038
}
2039
2040
/**
2041
 * @return Themes\DefaultTheme\Theme
2042
 */
2043
function theme()
2044
{
2045 8
	return $GLOBALS['context']['theme_instance'];
2046
}
2047
2048
/**
2049
 * Stops the execution with a 1x1 gif file
2050
 *
2051
 * @param bool $expired Sends an expired header.
2052
 */
2053
function dieGif($expired = false)
2054
{
2055
	// The following logging is just for debug, it should be removed before final
2056
	// or at least once the bug is fixes #2391
2057
	$filename = '';
2058
	$linenum = '';
2059
	if (headers_sent($filename, $linenum))
2060
	{
2061
		if (empty($filename))
2062
		{
2063
			ob_clean();
2064
		}
2065
		else
2066
		{
2067
			Errors::instance()->log_error('Headers already sent in ' . $filename . ' at line ' . $linenum);
2068
		}
2069
	}
2070
2071
	if ($expired === true)
2072
	{
2073
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
2074
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
2075
	}
2076
2077
	header('Content-Type: image/gif');
2078
	die("\x47\x49\x46\x38\x39\x61\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\x00\x00\x00\x21\xF9\x04\x01\x00\x00\x00\x00\x2C\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02\x44\x01\x00\x3B");
0 ignored issues
show
Coding Style Compatibility introduced by
The function dieGif() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
2079
}
2080
2081
/**
2082
 * Prepare ob_start with or without gzip compression
2083
 *
2084
 * @param bool $use_compression Starts compressed headers.
2085
 */
2086
function obStart($use_compression = false)
2087
{
2088
	// This is done to clear any output that was made before now.
2089
	while (ob_get_level() > 0)
2090
	{
2091
		@ob_end_clean();
2092
	}
2093
2094
	if ($use_compression === true)
2095
	{
2096
		ob_start('ob_gzhandler');
2097
	}
2098
	else
2099
	{
2100
		ob_start();
2101
		header('Content-Encoding: none');
2102
	}
2103
}