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