Completed
Pull Request — patch_1-1-4 (#3191)
by Emanuele
13:25
created

Subs.php ➔ setOldUrl()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 25
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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