Completed
Push — release-2.1 ( 64d581...1bd816 )
by Mert
04:48
created

Subs.php ➔ set_tld_regex()   D

Complexity

Conditions 34
Paths 10

Size

Total Lines 188
Code Lines 112

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 34
eloc 112
nc 10
nop 1
dl 0
loc 188
rs 4.3215
c 0
b 0
f 0

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
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines http://www.simplemachines.org
10
 * @copyright 2016 Simple Machines and individual contributors
11
 * @license http://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 Beta 3
14
 */
15
16
if (!defined('SMF'))
17
	die('No direct access...');
18
19
/**
20
 * Update some basic statistics.
21
 *
22
 * 'member' statistic updates the latest member, the total member
23
 *  count, and the number of unapproved members.
24
 * 'member' also only counts approved members when approval is on, but
25
 *  is much more efficient with it off.
26
 *
27
 * 'message' changes the total number of messages, and the
28
 *  highest message id by id_msg - which can be parameters 1 and 2,
29
 *  respectively.
30
 *
31
 * 'topic' updates the total number of topics, or if parameter1 is true
32
 *  simply increments them.
33
 *
34
 * 'subject' updates the log_search_subjects in the event of a topic being
35
 *  moved, removed or split.  parameter1 is the topicid, parameter2 is the new subject
36
 *
37
 * 'postgroups' case updates those members who match condition's
38
 *  post-based membergroups in the database (restricted by parameter1).
39
 *
40
 * @param string $type Stat type - can be 'member', 'message', 'topic', 'subject' or 'postgroups'
41
 * @param mixed $parameter1 A parameter for updating the stats
42
 * @param mixed $parameter2 A 2nd parameter for updating the stats
43
 */
44
function updateStats($type, $parameter1 = null, $parameter2 = null)
45
{
46
	global $modSettings, $smcFunc;
47
48
	switch ($type)
49
	{
50
		case 'member':
51
			$changes = array(
52
				'memberlist_updated' => time(),
53
			);
54
55
			// #1 latest member ID, #2 the real name for a new registration.
56
			if (is_numeric($parameter1))
57
			{
58
				$changes['latestMember'] = $parameter1;
59
				$changes['latestRealName'] = $parameter2;
60
61
				updateSettings(array('totalMembers' => true), true);
62
			}
63
64
			// We need to calculate the totals.
65
			else
66
			{
67
				// Update the latest activated member (highest id_member) and count.
68
				$result = $smcFunc['db_query']('', '
69
				SELECT COUNT(*), MAX(id_member)
70
				FROM {db_prefix}members
71
				WHERE is_activated = {int:is_activated}',
72
					array(
73
						'is_activated' => 1,
74
					)
75
				);
76
				list ($changes['totalMembers'], $changes['latestMember']) = $smcFunc['db_fetch_row']($result);
77
				$smcFunc['db_free_result']($result);
78
79
				// Get the latest activated member's display name.
80
				$result = $smcFunc['db_query']('', '
81
				SELECT real_name
82
				FROM {db_prefix}members
83
				WHERE id_member = {int:id_member}
84
				LIMIT 1',
85
					array(
86
						'id_member' => (int) $changes['latestMember'],
87
					)
88
				);
89
				list ($changes['latestRealName']) = $smcFunc['db_fetch_row']($result);
90
				$smcFunc['db_free_result']($result);
91
92
				if (!empty($modSettings['registration_method']))
93
				{
94
					// Are we using registration approval?
95
					if ($modSettings['registration_method'] == 2 || !empty($modSettings['approveAccountDeletion']))
96
					{
97
						// Update the amount of members awaiting approval
98
						$result = $smcFunc['db_query']('', '
99
						SELECT COUNT(*)
100
						FROM {db_prefix}members
101
						WHERE is_activated IN ({array_int:activation_status})',
102
							array(
103
								'activation_status' => array(3, 4),
104
							)
105
						);
106
						list ($changes['unapprovedMembers']) = $smcFunc['db_fetch_row']($result);
107
						$smcFunc['db_free_result']($result);
108
					}
109
110
					// What about unapproved COPPA registrations?
111 View Code Duplication
					if (!empty($modSettings['coppaType']) && $modSettings['coppaType'] != 1)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
112
					{
113
						$result = $smcFunc['db_query']('', '
114
						SELECT COUNT(*)
115
						FROM {db_prefix}members
116
						WHERE is_activated = {int:coppa_approval}',
117
							array(
118
								'coppa_approval' => 5,
119
							)
120
						);
121
						list ($coppa_approvals) = $smcFunc['db_fetch_row']($result);
122
						$smcFunc['db_free_result']($result);
123
124
						// Add this to the number of unapproved members
125
						if (!empty($changes['unapprovedMembers']))
126
							$changes['unapprovedMembers'] += $coppa_approvals;
127
						else
128
							$changes['unapprovedMembers'] = $coppa_approvals;
129
					}
130
				}
131
			}
132
			updateSettings($changes);
133
			break;
134
135
		case 'message':
136
			if ($parameter1 === true && $parameter2 !== null)
137
				updateSettings(array('totalMessages' => true, 'maxMsgID' => $parameter2), true);
138
			else
139
			{
140
				// SUM and MAX on a smaller table is better for InnoDB tables.
141
				$result = $smcFunc['db_query']('', '
142
				SELECT SUM(num_posts + unapproved_posts) AS total_messages, MAX(id_last_msg) AS max_msg_id
143
				FROM {db_prefix}boards
144
				WHERE redirect = {string:blank_redirect}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
145
					AND id_board != {int:recycle_board}' : ''),
146
					array(
147
						'recycle_board' => isset($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
148
						'blank_redirect' => '',
149
					)
150
				);
151
				$row = $smcFunc['db_fetch_assoc']($result);
152
				$smcFunc['db_free_result']($result);
153
154
				updateSettings(array(
155
					'totalMessages' => $row['total_messages'] === null ? 0 : $row['total_messages'],
156
					'maxMsgID' => $row['max_msg_id'] === null ? 0 : $row['max_msg_id']
157
				));
158
			}
159
			break;
160
161
		case 'subject':
162
			// Remove the previous subject (if any).
163
			$smcFunc['db_query']('', '
164
			DELETE FROM {db_prefix}log_search_subjects
165
			WHERE id_topic = {int:id_topic}',
166
				array(
167
					'id_topic' => (int) $parameter1,
168
				)
169
			);
170
171
			// Insert the new subject.
172
			if ($parameter2 !== null)
173
			{
174
				$parameter1 = (int) $parameter1;
175
				$parameter2 = text2words($parameter2);
176
177
				$inserts = array();
178
				foreach ($parameter2 as $word)
179
					$inserts[] = array($word, $parameter1);
180
181 View Code Duplication
				if (!empty($inserts))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
182
					$smcFunc['db_insert']('ignore',
183
						'{db_prefix}log_search_subjects',
184
						array('word' => 'string', 'id_topic' => 'int'),
185
						$inserts,
186
						array('word', 'id_topic')
187
					);
188
			}
189
			break;
190
191
		case 'topic':
192
			if ($parameter1 === true)
193
				updateSettings(array('totalTopics' => true), true);
194
			else
195
			{
196
				// Get the number of topics - a SUM is better for InnoDB tables.
197
				// We also ignore the recycle bin here because there will probably be a bunch of one-post topics there.
198
				$result = $smcFunc['db_query']('', '
199
				SELECT SUM(num_topics + unapproved_topics) AS total_topics
200
				FROM {db_prefix}boards' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
201
				WHERE id_board != {int:recycle_board}' : ''),
202
					array(
203
						'recycle_board' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
204
					)
205
				);
206
				$row = $smcFunc['db_fetch_assoc']($result);
207
				$smcFunc['db_free_result']($result);
208
209
				updateSettings(array('totalTopics' => $row['total_topics'] === null ? 0 : $row['total_topics']));
210
			}
211
			break;
212
213
		case 'postgroups':
214
			// Parameter two is the updated columns: we should check to see if we base groups off any of these.
215
			if ($parameter2 !== null && !in_array('posts', $parameter2))
216
				return;
217
218
			$postgroups = cache_get_data('updateStats:postgroups', 360);
219
			if ($postgroups == null || $parameter1 == null)
220
			{
221
				// Fetch the postgroups!
222
				$request = $smcFunc['db_query']('', '
223
				SELECT id_group, min_posts
224
				FROM {db_prefix}membergroups
225
				WHERE min_posts != {int:min_posts}',
226
					array(
227
						'min_posts' => -1,
228
					)
229
				);
230
				$postgroups = array();
231
				while ($row = $smcFunc['db_fetch_assoc']($request))
232
					$postgroups[$row['id_group']] = $row['min_posts'];
233
				$smcFunc['db_free_result']($request);
234
235
				// Sort them this way because if it's done with MySQL it causes a filesort :(.
236
				arsort($postgroups);
237
238
				cache_put_data('updateStats:postgroups', $postgroups, 360);
239
			}
240
241
			// Oh great, they've screwed their post groups.
242
			if (empty($postgroups))
243
				return;
244
245
			// Set all membergroups from most posts to least posts.
246
			$conditions = '';
247
			$lastMin = 0;
248
			foreach ($postgroups as $id => $min_posts)
249
			{
250
				$conditions .= '
251
					WHEN posts >= ' . $min_posts . (!empty($lastMin) ? ' AND posts <= ' . $lastMin : '') . ' THEN ' . $id;
252
				$lastMin = $min_posts;
253
			}
254
255
			// A big fat CASE WHEN... END is faster than a zillion UPDATE's ;).
256
			$smcFunc['db_query']('', '
257
			UPDATE {db_prefix}members
258
			SET id_post_group = CASE ' . $conditions . '
259
					ELSE 0
260
				END' . ($parameter1 != null ? '
261
			WHERE ' . (is_array($parameter1) ? 'id_member IN ({array_int:members})' : 'id_member = {int:members}') : ''),
262
				array(
263
					'members' => $parameter1,
264
				)
265
			);
266
			break;
267
268
		default:
269
			trigger_error('updateStats(): Invalid statistic type \'' . $type . '\'', E_USER_NOTICE);
270
	}
271
}
272
273
/**
274
 * Updates the columns in the members table.
275
 * Assumes the data has been htmlspecialchar'd.
276
 * this function should be used whenever member data needs to be
277
 * updated in place of an UPDATE query.
278
 *
279
 * id_member is either an int or an array of ints to be updated.
280
 *
281
 * data is an associative array of the columns to be updated and their respective values.
282
 * any string values updated should be quoted and slashed.
283
 *
284
 * the value of any column can be '+' or '-', which mean 'increment'
285
 * and decrement, respectively.
286
 *
287
 * if the member's post number is updated, updates their post groups.
288
 *
289
 * @param mixed $members An array of member IDs, null to update this for all members or the ID of a single member
290
 * @param array $data The info to update for the members
291
 */
292
function updateMemberData($members, $data)
293
{
294
	global $modSettings, $user_info, $smcFunc;
295
296
	$parameters = array();
297
	if (is_array($members))
298
	{
299
		$condition = 'id_member IN ({array_int:members})';
300
		$parameters['members'] = $members;
301
	}
302
	elseif ($members === null)
303
		$condition = '1=1';
304
	else
305
	{
306
		$condition = 'id_member = {int:member}';
307
		$parameters['member'] = $members;
308
	}
309
310
	// Everything is assumed to be a string unless it's in the below.
311
	$knownInts = array(
312
		'date_registered', 'posts', 'id_group', 'last_login', 'instant_messages', 'unread_messages',
313
		'new_pm', 'pm_prefs', 'gender', 'show_online', 'pm_receive_from', 'alerts',
314
		'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning',
315
	);
316
	$knownFloats = array(
317
		'time_offset',
318
	);
319
320
	if (!empty($modSettings['integrate_change_member_data']))
321
	{
322
		// Only a few member variables are really interesting for integration.
323
		$integration_vars = array(
324
			'member_name',
325
			'real_name',
326
			'email_address',
327
			'id_group',
328
			'gender',
329
			'birthdate',
330
			'website_title',
331
			'website_url',
332
			'location',
333
			'time_format',
334
			'time_offset',
335
			'avatar',
336
			'lngfile',
337
		);
338
		$vars_to_integrate = array_intersect($integration_vars, array_keys($data));
339
340
		// Only proceed if there are any variables left to call the integration function.
341
		if (count($vars_to_integrate) != 0)
342
		{
343
			// Fetch a list of member_names if necessary
344
			if ((!is_array($members) && $members === $user_info['id']) || (is_array($members) && count($members) == 1 && in_array($user_info['id'], $members)))
345
				$member_names = array($user_info['username']);
346
			else
347
			{
348
				$member_names = array();
349
				$request = $smcFunc['db_query']('', '
350
					SELECT member_name
351
					FROM {db_prefix}members
352
					WHERE ' . $condition,
353
					$parameters
354
				);
355
				while ($row = $smcFunc['db_fetch_assoc']($request))
356
					$member_names[] = $row['member_name'];
357
				$smcFunc['db_free_result']($request);
358
			}
359
360
			if (!empty($member_names))
361
				foreach ($vars_to_integrate as $var)
362
					call_integration_hook('integrate_change_member_data', array($member_names, $var, &$data[$var], &$knownInts, &$knownFloats));
363
		}
364
	}
365
366
	$setString = '';
367
	foreach ($data as $var => $val)
368
	{
369
		$type = 'string';
370
		if (in_array($var, $knownInts))
371
			$type = 'int';
372
		elseif (in_array($var, $knownFloats))
373
			$type = 'float';
374
		elseif ($var == 'birthdate')
375
			$type = 'date';
376
		elseif ($var == 'member_ip')
377
			$type = 'inet';
378
		elseif ($var == 'member_ip2')
379
			$type = 'inet';
380
381
		// Doing an increment?
382
		if ($type == 'int' && ($val === '+' || $val === '-'))
383
		{
384
			$val = $var . ' ' . $val . ' 1';
385
			$type = 'raw';
386
		}
387
388
		// Ensure posts, instant_messages, and unread_messages don't overflow or underflow.
389
		if (in_array($var, array('posts', 'instant_messages', 'unread_messages')))
390
		{
391
			if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match))
392
			{
393
				if ($match[1] != '+ ')
394
					$val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END';
395
				$type = 'raw';
396
			}
397
		}
398
399
		$setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},';
400
		$parameters['p_' . $var] = $val;
401
	}
402
403
	$smcFunc['db_query']('', '
404
		UPDATE {db_prefix}members
405
		SET' . substr($setString, 0, -1) . '
406
		WHERE ' . $condition,
407
		$parameters
408
	);
409
410
	updateStats('postgroups', $members, array_keys($data));
411
412
	// Clear any caching?
413
	if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2 && !empty($members))
414
	{
415
		if (!is_array($members))
416
			$members = array($members);
417
418
		foreach ($members as $member)
419
		{
420
			if ($modSettings['cache_enable'] >= 3)
421
			{
422
				cache_put_data('member_data-profile-' . $member, null, 120);
423
				cache_put_data('member_data-normal-' . $member, null, 120);
424
				cache_put_data('member_data-minimal-' . $member, null, 120);
425
			}
426
			cache_put_data('user_settings-' . $member, null, 60);
427
		}
428
	}
429
}
430
431
/**
432
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
433
 *
434
 * - updates both the settings table and $modSettings array.
435
 * - all of changeArray's indexes and values are assumed to have escaped apostrophes (')!
436
 * - if a variable is already set to what you want to change it to, that
437
 *   variable will be skipped over; it would be unnecessary to reset.
438
 * - When use_update is true, UPDATEs will be used instead of REPLACE.
439
 * - when use_update is true, the value can be true or false to increment
440
 *  or decrement it, respectively.
441
 *
442
 * @param array $changeArray An array of info about what we're changing in 'setting' => 'value' format
443
 * @param bool $update Whether to use an UPDATE query instead of a REPLACE query
444
 */
445
function updateSettings($changeArray, $update = false)
446
{
447
	global $modSettings, $smcFunc;
448
449
	if (empty($changeArray) || !is_array($changeArray))
450
		return;
451
452
	$toRemove = array();
453
454
	// Go check if there is any setting to be removed.
455
	foreach ($changeArray as $k => $v)
456
		if ($v === null)
457
		{
458
			// Found some, remove them from the original array and add them to ours.
459
			unset($changeArray[$k]);
460
			$toRemove[] = $k;
461
		}
462
463
	// Proceed with the deletion.
464
	if (!empty($toRemove))
465
		$smcFunc['db_query']('', '
466
			DELETE FROM {db_prefix}settings
467
			WHERE variable IN ({array_string:remove})',
468
			array(
469
				'remove' => $toRemove,
470
			)
471
		);
472
473
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
474
	if ($update)
475
	{
476
		foreach ($changeArray as $variable => $value)
477
		{
478
			$smcFunc['db_query']('', '
479
				UPDATE {db_prefix}settings
480
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
481
				WHERE variable = {string:variable}',
482
				array(
483
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
484
					'variable' => $variable,
485
				)
486
			);
487
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
488
		}
489
490
		// Clean out the cache and make sure the cobwebs are gone too.
491
		cache_put_data('modSettings', null, 90);
492
493
		return;
494
	}
495
496
	$replaceArray = array();
497
	foreach ($changeArray as $variable => $value)
498
	{
499
		// Don't bother if it's already like that ;).
500
		if (isset($modSettings[$variable]) && $modSettings[$variable] == $value)
501
			continue;
502
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
503
		elseif (!isset($modSettings[$variable]) && empty($value))
504
			continue;
505
506
		$replaceArray[] = array($variable, $value);
507
508
		$modSettings[$variable] = $value;
509
	}
510
511
	if (empty($replaceArray))
512
		return;
513
514
	$smcFunc['db_insert']('replace',
515
		'{db_prefix}settings',
516
		array('variable' => 'string-255', 'value' => 'string-65534'),
517
		$replaceArray,
518
		array('variable')
519
	);
520
521
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
522
	cache_put_data('modSettings', null, 90);
523
}
524
525
/**
526
 * Constructs a page list.
527
 *
528
 * - builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15.
529
 * - flexible_start causes it to use "url.page" instead of "url;start=page".
530
 * - very importantly, cleans up the start value passed, and forces it to
531
 *   be a multiple of num_per_page.
532
 * - checks that start is not more than max_value.
533
 * - base_url should be the URL without any start parameter on it.
534
 * - uses the compactTopicPagesEnable and compactTopicPagesContiguous
535
 *   settings to decide how to display the menu.
536
 *
537
 * an example is available near the function definition.
538
 * $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages, $maxindex, true);
539
 *
540
 * @param string $base_url The basic URL to be used for each link.
541
 * @param int &$start The start position, by reference. If this is not a multiple of the number of items per page, it is sanitized to be so and the value will persist upon the function's return.
542
 * @param int $max_value The total number of items you are paginating for.
543
 * @param int $num_per_page The number of items to be displayed on a given page. $start will be forced to be a multiple of this value.
544
 * @param bool $flexible_start Whether a ;start=x component should be introduced into the URL automatically (see above)
545
 * @param bool $show_prevnext Whether the Previous and Next links should be shown (should be on only when navigating the list)
546
 *
547
 * @return string The complete HTML of the page index that was requested, formatted by the template.
548
 */
549
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show_prevnext = true)
550
{
551
	global $modSettings, $context, $smcFunc, $settings, $txt;
552
553
	// Save whether $start was less than 0 or not.
554
	$start = (int) $start;
555
	$start_invalid = $start < 0;
556
557
	// Make sure $start is a proper variable - not less than 0.
558
	if ($start_invalid)
559
		$start = 0;
560
	// Not greater than the upper bound.
561
	elseif ($start >= $max_value)
562
		$start = max(0, (int) $max_value - (((int) $max_value % (int) $num_per_page) == 0 ? $num_per_page : ((int) $max_value % (int) $num_per_page)));
563
	// And it has to be a multiple of $num_per_page!
564
	else
565
		$start = max(0, (int) $start - ((int) $start % (int) $num_per_page));
566
567
	$context['current_page'] = $start / $num_per_page;
568
569
	// Define some default page index settings if we don't already have it...
570
	if (!isset($settings['page_index']))
571
	{
572
		// This defines the formatting for the page indexes used throughout the forum.
573
		$settings['page_index'] = array(
574
			'extra_before' => '<span class="pages">' . $txt['pages'] . '</span>',
575
			'previous_page' => '<span class="generic_icons previous_page"></span>',
576
			'current_page' => '<span class="current_page">%1$d</span> ',
577
			'page' => '<a class="navPages" href="{URL}">%2$s</a> ',
578
			'expand_pages' => '<span class="expand_pages" onclick="expandPages(this, {LINK}, {FIRST_PAGE}, {LAST_PAGE}, {PER_PAGE});"> ... </span>',
579
			'next_page' => '<span class="generic_icons next_page"></span>',
580
			'extra_after' => '',
581
		);
582
	}
583
584
	$base_link = strtr($settings['page_index']['page'], array('{URL}' => $flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'));
585
	$pageindex = $settings['page_index']['extra_before'];
586
587
	// Compact pages is off or on?
588
	if (empty($modSettings['compactTopicPagesEnable']))
589
	{
590
		// Show the left arrow.
591
		$pageindex .= $start == 0 ? ' ' : sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
592
593
		// Show all the pages.
594
		$display_page = 1;
595
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
596
			$pageindex .= $start == $counter && !$start_invalid ? sprintf($settings['page_index']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
597
598
		// Show the right arrow.
599
		$display_page = ($start + $num_per_page) > $max_value ? $max_value : ($start + $num_per_page);
600
		if ($start != $counter - $max_value && !$start_invalid)
601
			$pageindex .= $display_page > $counter - $num_per_page ? ' ' : sprintf($base_link, $display_page, $settings['page_index']['next_page']);
602
	}
603
	else
604
	{
605
		// If they didn't enter an odd value, pretend they did.
606
		$PageContiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
607
608
		// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
609
		if (!empty($start) && $show_prevnext)
610
			$pageindex .= sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
611
		else
612
			$pageindex .= '';
613
614
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
615
		if ($start > $num_per_page * $PageContiguous)
616
			$pageindex .= sprintf($base_link, 0, '1');
617
618
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
619
		if ($start > $num_per_page * ($PageContiguous + 1))
620
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
621
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
622
				'{FIRST_PAGE}' => $num_per_page,
623
				'{LAST_PAGE}' => $start - $num_per_page * $PageContiguous,
624
				'{PER_PAGE}' => $num_per_page,
625
			));
626
627
		// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
628
		for ($nCont = $PageContiguous; $nCont >= 1; $nCont--)
629 View Code Duplication
			if ($start >= $num_per_page * $nCont)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
630
			{
631
				$tmpStart = $start - $num_per_page * $nCont;
632
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
633
			}
634
635
		// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
636
		if (!$start_invalid)
637
			$pageindex .= sprintf($settings['page_index']['current_page'], $start / $num_per_page + 1);
638
		else
639
			$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
640
641
		// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
642
		$tmpMaxPages = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
643
		for ($nCont = 1; $nCont <= $PageContiguous; $nCont++)
644 View Code Duplication
			if ($start + $num_per_page * $nCont <= $tmpMaxPages)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
645
			{
646
				$tmpStart = $start + $num_per_page * $nCont;
647
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
648
			}
649
650
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
651
		if ($start + $num_per_page * ($PageContiguous + 1) < $tmpMaxPages)
652
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
653
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
654
				'{FIRST_PAGE}' => $start + $num_per_page * ($PageContiguous + 1),
655
				'{LAST_PAGE}' => $tmpMaxPages,
656
				'{PER_PAGE}' => $num_per_page,
657
			));
658
659
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
660
		if ($start + $num_per_page * $PageContiguous < $tmpMaxPages)
661
			$pageindex .= sprintf($base_link, $tmpMaxPages, $tmpMaxPages / $num_per_page + 1);
662
663
		// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
664
		if ($start != $tmpMaxPages && $show_prevnext)
665
			$pageindex .= sprintf($base_link, $start + $num_per_page, $settings['page_index']['next_page']);
666
	}
667
	$pageindex .= $settings['page_index']['extra_after'];
668
669
	return $pageindex;
670
}
671
672
/**
673
 * - Formats a number.
674
 * - uses the format of number_format to decide how to format the number.
675
 *   for example, it might display "1 234,50".
676
 * - caches the formatting data from the setting for optimization.
677
 *
678
 * @param float $number A number
679
 * @param bool|int $override_decimal_count If set, will use the specified number of decimal places. Otherwise it's automatically determined
680
 * @return string A formatted number
681
 */
682
function comma_format($number, $override_decimal_count = false)
683
{
684
	global $txt;
685
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
686
687
	// Cache these values...
688
	if ($decimal_separator === null)
689
	{
690
		// Not set for whatever reason?
691
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
692
			return $number;
693
694
		// Cache these each load...
695
		$thousands_separator = $matches[1];
696
		$decimal_separator = $matches[2];
697
		$decimal_count = strlen($matches[3]);
698
	}
699
700
	// Format the string with our friend, number_format.
701
	return number_format($number, (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
702
}
703
704
/**
705
 * Format a time to make it look purdy.
706
 *
707
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
708
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
709
 * - if todayMod is set and show_today was not not specified or true, an
710
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
711
 * - performs localization (more than just strftime would do alone.)
712
 *
713
 * @param int $log_time A timestamp
714
 * @param bool $show_today Whether to show "Today"/"Yesterday" or just a date
715
 * @param bool|string $offset_type If false, uses both user time offset and forum offset. If 'forum', uses only the forum offset. Otherwise no offset is applied.
716
 * @return string A formatted timestamp
717
 */
718
function timeformat($log_time, $show_today = true, $offset_type = false)
719
{
720
	global $context, $user_info, $txt, $modSettings;
721
	static $non_twelve_hour;
722
723
	// Offset the time.
724
	if (!$offset_type)
725
		$time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
726
	// Just the forum offset?
727
	elseif ($offset_type == 'forum')
728
		$time = $log_time + $modSettings['time_offset'] * 3600;
729
	else
730
		$time = $log_time;
731
732
	// We can't have a negative date (on Windows, at least.)
733
	if ($log_time < 0)
734
		$log_time = 0;
735
736
	// Today and Yesterday?
737
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
738
	{
739
		// Get the current time.
740
		$nowtime = forum_time();
741
742
		$then = @getdate($time);
743
		$now = @getdate($nowtime);
744
745
		// Try to make something of a time format string...
746
		$s = strpos($user_info['time_format'], '%S') === false ? '' : ':%S';
747
		if (strpos($user_info['time_format'], '%H') === false && strpos($user_info['time_format'], '%T') === false)
748
		{
749
			$h = strpos($user_info['time_format'], '%l') === false ? '%I' : '%l';
750
			$today_fmt = $h . ':%M' . $s . ' %p';
751
		}
752
		else
753
			$today_fmt = '%H:%M' . $s;
754
755
		// Same day of the year, same year.... Today!
756
		if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
757
			return $txt['today'] . timeformat($log_time, $today_fmt, $offset_type);
0 ignored issues
show
Documentation introduced by
$today_fmt is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
758
759
		// 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...
760
		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))
761
			return $txt['yesterday'] . timeformat($log_time, $today_fmt, $offset_type);
0 ignored issues
show
Documentation introduced by
$today_fmt is of type string, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
762
	}
763
764
	$str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
765
766
	if (setlocale(LC_TIME, $txt['lang_locale']))
767
	{
768
		if (!isset($non_twelve_hour))
769
			$non_twelve_hour = trim(strftime('%p')) === '';
770 View Code Duplication
		if ($non_twelve_hour && strpos($str, '%p') !== false)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
771
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
772
773
		foreach (array('%a', '%A', '%b', '%B') as $token)
774
			if (strpos($str, $token) !== false)
775
				$str = str_replace($token, strftime($token, $time), $str);
776
	}
777
	else
778
	{
779
		// Do-it-yourself time localization.  Fun.
780
		foreach (array('%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months') as $token => $text_label)
781
			if (strpos($str, $token) !== false)
782
				$str = str_replace($token, $txt[$text_label][(int) strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
783
784 View Code Duplication
		if (strpos($str, '%p') !== false)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
785
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
786
	}
787
788
	// Windows doesn't support %e; on some versions, strftime fails altogether if used, so let's prevent that.
789
	if ($context['server']['is_windows'] && strpos($str, '%e') !== false)
790
		$str = str_replace('%e', ltrim(strftime('%d', $time), '0'), $str);
791
792
	// Format any other characters..
793
	return strftime($str, $time);
794
}
795
796
/**
797
 * Removes special entities from strings.  Compatibility...
798
 * Should be used instead of html_entity_decode for PHP version compatibility reasons.
799
 *
800
 * - removes the base entities (&lt;, &quot;, etc.) from text.
801
 * - additionally converts &nbsp; and &#039;.
802
 *
803
 * @param string $string A string
804
 * @return string The string without entities
805
 */
806
function un_htmlspecialchars($string)
0 ignored issues
show
Best Practice introduced by
The function un_htmlspecialchars() has been defined more than once; this definition is ignored, only the first definition in other/upgrade.php (L149-152) is considered.

This check looks for functions that have already been defined in other files.

Some Codebases, like WordPress, make a practice of defining functions multiple times. This may lead to problems with the detection of function parameters and types. If you really need to do this, you can mark the duplicate definition with the @ignore annotation.

/**
 * @ignore
 */
function getUser() {

}

function getUser($id, $realm) {

}

See also the PhpDoc documentation for @ignore.

Loading history...
807
{
808
	global $context;
809
	static $translation = array();
810
811
	// Determine the character set... Default to UTF-8
812
	if (empty($context['character_set']))
813
		$charset = 'UTF-8';
814
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
815
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
816
		$charset = 'ISO-8859-1';
817
	else
818
		$charset = $context['character_set'];
819
820
	if (empty($translation))
821
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
822
823
	return strtr($string, $translation);
824
}
825
826
/**
827
 * Shorten a subject + internationalization concerns.
828
 *
829
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
830
 * - respects internationalization characters and entities as one character.
831
 * - avoids trailing entities.
832
 * - returns the shortened string.
833
 *
834
 * @param string $subject The subject
835
 * @param int $len How many characters to limit it to
836
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
837
 */
838
function shorten_subject($subject, $len)
839
{
840
	global $smcFunc;
841
842
	// It was already short enough!
843
	if ($smcFunc['strlen']($subject) <= $len)
844
		return $subject;
845
846
	// Shorten it by the length it was too long, and strip off junk from the end.
847
	return $smcFunc['substr']($subject, 0, $len) . '...';
848
}
849
850
/**
851
 * Gets the current time with offset.
852
 *
853
 * - always applies the offset in the time_offset setting.
854
 *
855
 * @param bool $use_user_offset Whether to apply the user's offset as well
856
 * @param int $timestamp A timestamp (null to use current time)
857
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
858
 */
859
function forum_time($use_user_offset = true, $timestamp = null)
860
{
861
	global $user_info, $modSettings;
862
863
	if ($timestamp === null)
864
		$timestamp = time();
865
	elseif ($timestamp == 0)
866
		return 0;
867
868
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
869
}
870
871
/**
872
 * Calculates all the possible permutations (orders) of array.
873
 * should not be called on huge arrays (bigger than like 10 elements.)
874
 * returns an array containing each permutation.
875
 *
876
 * @deprecated since 2.1
877
 * @param array $array An array
878
 * @return array An array containing each permutation
879
 */
880
function permute($array)
881
{
882
	$orders = array($array);
883
884
	$n = count($array);
885
	$p = range(0, $n);
886
	for ($i = 1; $i < $n; null)
887
	{
888
		$p[$i]--;
889
		$j = $i % 2 != 0 ? $p[$i] : 0;
890
891
		$temp = $array[$i];
892
		$array[$i] = $array[$j];
893
		$array[$j] = $temp;
894
895
		for ($i = 1; $p[$i] == 0; $i++)
896
			$p[$i] = 1;
897
898
		$orders[] = $array;
899
	}
900
901
	return $orders;
902
}
903
904
/**
905
 * Parse bulletin board code in a string, as well as smileys optionally.
906
 *
907
 * - only parses bbc tags which are not disabled in disabledBBC.
908
 * - handles basic HTML, if enablePostHTML is on.
909
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
910
 * - only parses smileys if smileys is true.
911
 * - does nothing if the enableBBC setting is off.
912
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
913
 *  -returns the modified message.
914
 *
915
 * @param string $message The message
916
 * @param bool $smileys Whether to parse smileys as well
917
 * @param string $cache_id The cache ID
918
 * @param array $parse_tags If set, only parses these tags rather than all of them
919
 * @return string The parsed message
920
 */
921
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
922
{
923
	global $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir;
924
	static $bbc_codes = array(), $itemcodes = array(), $no_autolink_tags = array();
925
	static $disabled;
926
927
	// Don't waste cycles
928
	if ($message === '')
929
		return '';
930
931
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
932
	if (!isset($context['utf8']))
933
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
934
935
	// Clean up any cut/paste issues we may have
936
	$message = sanitizeMSCutPaste($message);
937
938
	// If the load average is too high, don't parse the BBC.
939
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
940
	{
941
		$context['disabled_parse_bbc'] = true;
942
		return $message;
943
	}
944
945
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
946
		$smileys = (bool) $smileys;
947
948
	if (empty($modSettings['enableBBC']) && $message !== false)
949
	{
950
		if ($smileys === true)
951
			parsesmileys($message);
952
953
		return $message;
954
	}
955
956
	// If we are not doing every tag then we don't cache this run.
957
	if (!empty($parse_tags) && !empty($bbc_codes))
958
	{
959
		$temp_bbc = $bbc_codes;
960
		$bbc_codes = array();
961
	}
962
963
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
964
	if (!empty($modSettings['autoLinkUrls']))
965
		set_tld_regex();
966
967
	// Allow mods access before entering the main parse_bbc loop
968
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
969
970
	// Sift out the bbc for a performance improvement.
971
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
972
	{
973
		if (!empty($modSettings['disabledBBC']))
974
		{
975
			$disabled = array();
976
977
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
978
979
			foreach ($temp as $tag)
980
				$disabled[trim($tag)] = true;
981
		}
982
983
		if (empty($modSettings['enableEmbeddedFlash']))
984
			$disabled['flash'] = true;
985
986
		/* The following bbc are formatted as an array, with keys as follows:
987
988
			tag: the tag's name - should be lowercase!
989
990
			type: one of...
991
				- (missing): [tag]parsed content[/tag]
992
				- unparsed_equals: [tag=xyz]parsed content[/tag]
993
				- parsed_equals: [tag=parsed data]parsed content[/tag]
994
				- unparsed_content: [tag]unparsed content[/tag]
995
				- closed: [tag], [tag/], [tag /]
996
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
997
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
998
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
999
1000
			parameters: an optional array of parameters, for the form
1001
			  [tag abc=123]content[/tag].  The array is an associative array
1002
			  where the keys are the parameter names, and the values are an
1003
			  array which may contain the following:
1004
				- match: a regular expression to validate and match the value.
1005
				- quoted: true if the value should be quoted.
1006
				- validate: callback to evaluate on the data, which is $data.
1007
				- value: a string in which to replace $1 with the data.
1008
				  either it or validate may be used, not both.
1009
				- optional: true if the parameter is optional.
1010
1011
			test: a regular expression to test immediately after the tag's
1012
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1013
			  Optional.
1014
1015
			content: only available for unparsed_content, closed,
1016
			  unparsed_commas_content, and unparsed_equals_content.
1017
			  $1 is replaced with the content of the tag.  Parameters
1018
			  are replaced in the form {param}.  For unparsed_commas_content,
1019
			  $2, $3, ..., $n are replaced.
1020
1021
			before: only when content is not used, to go before any
1022
			  content.  For unparsed_equals, $1 is replaced with the value.
1023
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1024
1025
			after: similar to before in every way, except that it is used
1026
			  when the tag is closed.
1027
1028
			disabled_content: used in place of content when the tag is
1029
			  disabled.  For closed, default is '', otherwise it is '$1' if
1030
			  block_level is false, '<div>$1</div>' elsewise.
1031
1032
			disabled_before: used in place of before when disabled.  Defaults
1033
			  to '<div>' if block_level, '' if not.
1034
1035
			disabled_after: used in place of after when disabled.  Defaults
1036
			  to '</div>' if block_level, '' if not.
1037
1038
			block_level: set to true the tag is a "block level" tag, similar
1039
			  to HTML.  Block level tags cannot be nested inside tags that are
1040
			  not block level, and will not be implicitly closed as easily.
1041
			  One break following a block level tag may also be removed.
1042
1043
			trim: if set, and 'inside' whitespace after the begin tag will be
1044
			  removed.  If set to 'outside', whitespace after the end tag will
1045
			  meet the same fate.
1046
1047
			validate: except when type is missing or 'closed', a callback to
1048
			  validate the data as $data.  Depending on the tag's type, $data
1049
			  may be a string or an array of strings (corresponding to the
1050
			  replacement.)
1051
1052
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1053
			  may be not set, 'optional', or 'required' corresponding to if
1054
			  the content may be quoted.  This allows the parser to read
1055
			  [tag="abc]def[esdf]"] properly.
1056
1057
			require_parents: an array of tag names, or not set.  If set, the
1058
			  enclosing tag *must* be one of the listed tags, or parsing won't
1059
			  occur.
1060
1061
			require_children: similar to require_parents, if set children
1062
			  won't be parsed if they are not in the list.
1063
1064
			disallow_children: similar to, but very different from,
1065
			  require_children, if it is set the listed tags will not be
1066
			  parsed inside the tag.
1067
1068
			parsed_tags_allowed: an array restricting what BBC can be in the
1069
			  parsed_equals parameter, if desired.
1070
		*/
1071
1072
		$codes = array(
1073
			array(
1074
				'tag' => 'abbr',
1075
				'type' => 'unparsed_equals',
1076
				'before' => '<abbr title="$1">',
1077
				'after' => '</abbr>',
1078
				'quoted' => 'optional',
1079
				'disabled_after' => ' ($1)',
1080
			),
1081
			array(
1082
				'tag' => 'anchor',
1083
				'type' => 'unparsed_equals',
1084
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1085
				'before' => '<span id="post_$1">',
1086
				'after' => '</span>',
1087
			),
1088
			array(
1089
				'tag' => 'attach',
1090
				'type' => 'unparsed_content',
1091
				'parameters' => array(
1092
					'name' => array('optional' => true),
1093
					'type' => array('optional' => true),
1094
					'alt' => array('optional' => true),
1095
					'title' => array('optional' => true),
1096
					'width' => array('optional' => true, 'match' => '(\d+)'),
1097
					'height' => array('optional' => true, 'match' => '(\d+)'),
1098
				),
1099
				'content' => '$1',
1100
				'validate' => function (&$tag, &$data, $disabled, $params) use ($modSettings, $context, $sourcedir, $txt)
1101
				{
1102
					$returnContext = '';
1103
1104
					// BBC or the entire attachments feature is disabled
1105
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1106
						return $data;
1107
1108
					// Save the attach ID.
1109
					$attachID = $data;
1110
1111
					// Kinda need this.
1112
					require_once($sourcedir . '/Subs-Attachments.php');
1113
1114
					$currentAttachment = parseAttachBBC($attachID);
1115
1116
					// parseAttachBBC will return a string ($txt key) rather than diying with a fatal_error. Up to you to decide what to do.
1117
					if (is_string($currentAttachment))
1118
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1119
1120
					if (!empty($currentAttachment['is_image']))
1121
					{
1122
						$alt = !empty($params['{alt}']) ? ' alt="' . $params['{alt}'] . '"' : ' alt="' . $currentAttachment['name'] . '"';
0 ignored issues
show
Unused Code introduced by
$alt is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1123
						$title = !empty($params['{title}']) ? ' title="' . $params['{alt}'] . '"' : '';
0 ignored issues
show
Unused Code introduced by
$title is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1124
1125
						if (!empty($params['{width}']) && !empty($params['{height}']))
1126
						{
1127
							$width = ' width="' . $params['{width}'] . '"';
1128
							$height = ' height="' . $params['{height}'] . '"';
1129
						}
1130 View Code Duplication
						elseif (!empty($params['{width}']) && empty($params['{height}']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1131
						{
1132
							$width = ' width="' . $params['{width}'] . '"';
1133
							$height = '';
1134
						}
1135 View Code Duplication
						elseif (empty($params['{width}']) && !empty($params['{height}']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1136
						{
1137
							$width = '';
1138
							$height = ' height="' . $params['{height}'] . '"';
1139
						}
1140
						else
1141
						{
1142
							$width = ' width="' . $currentAttachment['width'] . '"';
1143
							$height = ' height="' . $currentAttachment['height'] . '"';
1144
						}
1145
1146
						if ($currentAttachment['thumbnail']['has_thumb'] && empty($params['{width}']) && empty($params['{height}']))
1147
							$returnContext .= '
1148
													<a href="'. $currentAttachment['href']. ';image" id="link_'. $currentAttachment['id']. '" onclick="'. $currentAttachment['thumbnail']['javascript']. '"><img src="'. $currentAttachment['thumbnail']['href']. '" alt="' . $currentAttachment['name'] . '" id="thumb_'. $currentAttachment['id']. '"></a>';
1149
						else
1150
							$returnContext .= '
1151
													<img src="' . $currentAttachment['href'] . ';image" alt="' . $currentAttachment['name'] . '"' . $width . $height . '/>';
1152
					}
1153
1154
					// No image. Show a link.
1155
					else
1156
						$returnContext .= $currentAttachment['link'];
1157
1158
					// Gotta append what we just did.
1159
					$data = $returnContext;
1160
				},
1161
			),
1162
			array(
1163
				'tag' => 'b',
1164
				'before' => '<b>',
1165
				'after' => '</b>',
1166
			),
1167
			array(
1168
				'tag' => 'center',
1169
				'before' => '<div class="centertext">',
1170
				'after' => '</div>',
1171
				'block_level' => true,
1172
			),
1173
			array(
1174
				'tag' => 'code',
1175
				'type' => 'unparsed_content',
1176
				'content' => '<div class="codeheader"><span class="code floatleft">' . $txt['code'] . '</span> <a class="codeoperation smf_select_text">' . $txt['code_select'] . '</a></div><code class="bbc_code">$1</code>',
1177
				// @todo Maybe this can be simplified?
1178
				'validate' => isset($disabled['code']) ? null : function (&$tag, &$data, $disabled) use ($context)
1179
				{
1180
					if (!isset($disabled['code']))
1181
					{
1182
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1183
1184 View Code Duplication
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1185
						{
1186
							// Do PHP code coloring?
1187
							if ($php_parts[$php_i] != '&lt;?php')
1188
								continue;
1189
1190
							$php_string = '';
1191
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1192
							{
1193
								$php_string .= $php_parts[$php_i];
1194
								$php_parts[$php_i++] = '';
1195
							}
1196
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1197
						}
1198
1199
						// Fix the PHP code stuff...
1200
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1201
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1202
1203
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1204
						if ($context['browser']['is_opera'])
1205
							$data .= '&nbsp;';
1206
					}
1207
				},
1208
				'block_level' => true,
1209
			),
1210
			array(
1211
				'tag' => 'code',
1212
				'type' => 'unparsed_equals_content',
1213
				'content' => '<div class="codeheader"><span class="code floatleft">' . $txt['code'] . '</span> ($2) <a class="codeoperation smf_select_text">' . $txt['code_select'] . '</a></div><code class="bbc_code">$1</code>',
1214
				// @todo Maybe this can be simplified?
1215
				'validate' => isset($disabled['code']) ? null : function (&$tag, &$data, $disabled) use ($context)
1216
				{
1217
					if (!isset($disabled['code']))
1218
					{
1219
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1220
1221 View Code Duplication
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1222
						{
1223
							// Do PHP code coloring?
1224
							if ($php_parts[$php_i] != '&lt;?php')
1225
								continue;
1226
1227
							$php_string = '';
1228
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1229
							{
1230
								$php_string .= $php_parts[$php_i];
1231
								$php_parts[$php_i++] = '';
1232
							}
1233
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1234
						}
1235
1236
						// Fix the PHP code stuff...
1237
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1238
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1239
1240
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1241
						if ($context['browser']['is_opera'])
1242
							$data[0] .= '&nbsp;';
1243
					}
1244
				},
1245
				'block_level' => true,
1246
			),
1247
			array(
1248
				'tag' => 'color',
1249
				'type' => 'unparsed_equals',
1250
				'test' => '(#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,20}|rgb\((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s?,\s?){2}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\))\]',
1251
				'before' => '<span style="color: $1;" class="bbc_color">',
1252
				'after' => '</span>',
1253
			),
1254
			array(
1255
				'tag' => 'email',
1256
				'type' => 'unparsed_content',
1257
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1258
				// @todo Should this respect guest_hideContacts?
1259
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled 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...
1260
				{
1261
					$data = strtr($data, array('<br>' => ''));
1262
				},
1263
			),
1264
			array(
1265
				'tag' => 'email',
1266
				'type' => 'unparsed_equals',
1267
				'before' => '<a href="mailto:$1" class="bbc_email">',
1268
				'after' => '</a>',
1269
				// @todo Should this respect guest_hideContacts?
1270
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1271
				'disabled_after' => ' ($1)',
1272
			),
1273
			array(
1274
				'tag' => 'flash',
1275
				'type' => 'unparsed_commas_content',
1276
				'test' => '\d+,\d+\]',
1277
				'content' => '<embed type="application/x-shockwave-flash" src="$1" width="$2" height="$3" play="true" loop="true" quality="high" AllowScriptAccess="never">',
1278
				'validate' => function (&$tag, &$data, $disabled)
1279
				{
1280
					if (isset($disabled['url']))
1281
						$tag['content'] = '$1';
1282
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1283
					if (empty($scheme))
1284
						$data[0] = '//' . ltrim($data[0], ':/');
1285
				},
1286
				'disabled_content' => '<a href="$1" target="_blank" class="new_win">$1</a>',
1287
			),
1288
			array(
1289
				'tag' => 'font',
1290
				'type' => 'unparsed_equals',
1291
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1292
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1293
				'after' => '</span>',
1294
			),
1295
			array(
1296
				'tag' => 'html',
1297
				'type' => 'unparsed_content',
1298
				'content' => '$1',
1299
				'block_level' => true,
1300
				'disabled_content' => '$1',
1301
			),
1302
			array(
1303
				'tag' => 'hr',
1304
				'type' => 'closed',
1305
				'content' => '<hr>',
1306
				'block_level' => true,
1307
			),
1308
			array(
1309
				'tag' => 'i',
1310
				'before' => '<i>',
1311
				'after' => '</i>',
1312
			),
1313
			array(
1314
				'tag' => 'img',
1315
				'type' => 'unparsed_content',
1316
				'parameters' => array(
1317
					'alt' => array('optional' => true),
1318
					'title' => array('optional' => true),
1319
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1320
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1321
				),
1322
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized">',
1323 View Code Duplication
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled 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...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1324
				{
1325
					global $image_proxy_enabled, $image_proxy_secret, $boardurl;
1326
1327
					$data = strtr($data, array('<br>' => ''));
1328
					$scheme = parse_url($data, PHP_URL_SCHEME);
1329
					if ($image_proxy_enabled)
1330
					{
1331
						if (empty($scheme))
1332
							$data = 'http://' . ltrim($data, ':/');
1333
1334
						if ($scheme != 'https')
1335
							$data = $boardurl . '/proxy.php?request=' . urlencode($data) . '&hash=' . md5($data . $image_proxy_secret);
1336
					}
1337
					elseif (empty($scheme))
1338
						$data = '//' . ltrim($data, ':/');
1339
				},
1340
				'disabled_content' => '($1)',
1341
			),
1342
			array(
1343
				'tag' => 'img',
1344
				'type' => 'unparsed_content',
1345
				'content' => '<img src="$1" alt="" class="bbc_img">',
1346 View Code Duplication
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled 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...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1347
				{
1348
					global $image_proxy_enabled, $image_proxy_secret, $boardurl;
1349
1350
					$data = strtr($data, array('<br>' => ''));
1351
					$scheme = parse_url($data, PHP_URL_SCHEME);
1352
					if ($image_proxy_enabled)
1353
					{
1354
						if (empty($scheme))
1355
							$data = 'http://' . ltrim($data, ':/');
1356
1357
						if ($scheme != 'https')
1358
							$data = $boardurl . '/proxy.php?request=' . urlencode($data) . '&hash=' . md5($data . $image_proxy_secret);
1359
					}
1360
					elseif (empty($scheme))
1361
						$data = '//' . ltrim($data, ':/');
1362
				},
1363
				'disabled_content' => '($1)',
1364
			),
1365
			array(
1366
				'tag' => 'iurl',
1367
				'type' => 'unparsed_content',
1368
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1369 View Code Duplication
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled 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...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1370
				{
1371
					$data = strtr($data, array('<br>' => ''));
1372
					$scheme = parse_url($data, PHP_URL_SCHEME);
1373
					if (empty($scheme))
1374
						$data = '//' . ltrim($data, ':/');
1375
				},
1376
			),
1377
			array(
1378
				'tag' => 'iurl',
1379
				'type' => 'unparsed_equals',
1380
				'quoted' => 'optional',
1381
				'before' => '<a href="$1" class="bbc_link">',
1382
				'after' => '</a>',
1383
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled 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...
1384
				{
1385
					if (substr($data, 0, 1) == '#')
1386
						$data = '#post_' . substr($data, 1);
1387
					else
1388
					{
1389
						$scheme = parse_url($data, PHP_URL_SCHEME);
1390
						if (empty($scheme))
1391
							$data = '//' . ltrim($data, ':/');
1392
					}
1393
				},
1394
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1395
				'disabled_after' => ' ($1)',
1396
			),
1397
			array(
1398
				'tag' => 'left',
1399
				'before' => '<div style="text-align: left;">',
1400
				'after' => '</div>',
1401
				'block_level' => true,
1402
			),
1403
			array(
1404
				'tag' => 'li',
1405
				'before' => '<li>',
1406
				'after' => '</li>',
1407
				'trim' => 'outside',
1408
				'require_parents' => array('list'),
1409
				'block_level' => true,
1410
				'disabled_before' => '',
1411
				'disabled_after' => '<br>',
1412
			),
1413
			array(
1414
				'tag' => 'list',
1415
				'before' => '<ul class="bbc_list">',
1416
				'after' => '</ul>',
1417
				'trim' => 'inside',
1418
				'require_children' => array('li', 'list'),
1419
				'block_level' => true,
1420
			),
1421
			array(
1422
				'tag' => 'list',
1423
				'parameters' => array(
1424
					'type' => array('match' => '(none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'),
1425
				),
1426
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1427
				'after' => '</ul>',
1428
				'trim' => 'inside',
1429
				'require_children' => array('li'),
1430
				'block_level' => true,
1431
			),
1432
			array(
1433
				'tag' => 'ltr',
1434
				'before' => '<bdo dir="ltr">',
1435
				'after' => '</bdo>',
1436
				'block_level' => true,
1437
			),
1438
			array(
1439
				'tag' => 'me',
1440
				'type' => 'unparsed_equals',
1441
				'before' => '<div class="meaction">* $1 ',
1442
				'after' => '</div>',
1443
				'quoted' => 'optional',
1444
				'block_level' => true,
1445
				'disabled_before' => '/me ',
1446
				'disabled_after' => '<br>',
1447
			),
1448
			array(
1449
				'tag' => 'member',
1450
				'type' => 'unparsed_equals',
1451
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1452
				'after' => '</a>',
1453
			),
1454
			array(
1455
				'tag' => 'nobbc',
1456
				'type' => 'unparsed_content',
1457
				'content' => '$1',
1458
			),
1459
			array(
1460
				'tag' => 'php',
1461
				'type' => 'unparsed_content',
1462
				'content' => '<span class="phpcode">$1</span>',
1463
				'validate' => isset($disabled['php']) ? null : function (&$tag, &$data, $disabled)
1464
				{
1465
					if (!isset($disabled['php']))
1466
					{
1467
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1468
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1469
						if ($add_begin)
1470
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1471
					}
1472
				},
1473
				'block_level' => false,
1474
				'disabled_content' => '$1',
1475
			),
1476
			array(
1477
				'tag' => 'pre',
1478
				'before' => '<pre>',
1479
				'after' => '</pre>',
1480
			),
1481
			array(
1482
				'tag' => 'quote',
1483
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1484
				'after' => '</blockquote>',
1485
				'trim' => 'both',
1486
				'block_level' => true,
1487
			),
1488
			array(
1489
				'tag' => 'quote',
1490
				'parameters' => array(
1491
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1492
				),
1493
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1494
				'after' => '</blockquote>',
1495
				'trim' => 'both',
1496
				'block_level' => true,
1497
			),
1498
			array(
1499
				'tag' => 'quote',
1500
				'type' => 'parsed_equals',
1501
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1502
				'after' => '</blockquote>',
1503
				'trim' => 'both',
1504
				'quoted' => 'optional',
1505
				// Don't allow everything to be embedded with the author name.
1506
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1507
				'block_level' => true,
1508
			),
1509
			array(
1510
				'tag' => 'quote',
1511
				'parameters' => array(
1512
					'author' => array('match' => '([^<>]{1,192}?)'),
1513
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1514
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1515
				),
1516
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1517
				'after' => '</blockquote>',
1518
				'trim' => 'both',
1519
				'block_level' => true,
1520
			),
1521
			array(
1522
				'tag' => 'quote',
1523
				'parameters' => array(
1524
					'author' => array('match' => '(.{1,192}?)'),
1525
				),
1526
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1527
				'after' => '</blockquote>',
1528
				'trim' => 'both',
1529
				'block_level' => true,
1530
			),
1531
			array(
1532
				'tag' => 'right',
1533
				'before' => '<div style="text-align: right;">',
1534
				'after' => '</div>',
1535
				'block_level' => true,
1536
			),
1537
			array(
1538
				'tag' => 'rtl',
1539
				'before' => '<bdo dir="rtl">',
1540
				'after' => '</bdo>',
1541
				'block_level' => true,
1542
			),
1543
			array(
1544
				'tag' => 's',
1545
				'before' => '<s>',
1546
				'after' => '</s>',
1547
			),
1548
			array(
1549
				'tag' => 'size',
1550
				'type' => 'unparsed_equals',
1551
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
1552
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1553
				'after' => '</span>',
1554
			),
1555
			array(
1556
				'tag' => 'size',
1557
				'type' => 'unparsed_equals',
1558
				'test' => '[1-7]\]',
1559
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1560
				'after' => '</span>',
1561
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled 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...
1562
				{
1563
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
1564
					$data = $sizes[$data] . 'em';
1565
				},
1566
			),
1567
			array(
1568
				'tag' => 'sub',
1569
				'before' => '<sub>',
1570
				'after' => '</sub>',
1571
			),
1572
			array(
1573
				'tag' => 'sup',
1574
				'before' => '<sup>',
1575
				'after' => '</sup>',
1576
			),
1577
			array(
1578
				'tag' => 'table',
1579
				'before' => '<table class="bbc_table">',
1580
				'after' => '</table>',
1581
				'trim' => 'inside',
1582
				'require_children' => array('tr'),
1583
				'block_level' => true,
1584
			),
1585
			array(
1586
				'tag' => 'td',
1587
				'before' => '<td>',
1588
				'after' => '</td>',
1589
				'require_parents' => array('tr'),
1590
				'trim' => 'outside',
1591
				'block_level' => true,
1592
				'disabled_before' => '',
1593
				'disabled_after' => '',
1594
			),
1595
			array(
1596
				'tag' => 'time',
1597
				'type' => 'unparsed_content',
1598
				'content' => '$1',
1599
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled 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...
1600
				{
1601
					if (is_numeric($data))
1602
						$data = timeformat($data);
1603
					else
1604
						$tag['content'] = '[time]$1[/time]';
1605
				},
1606
			),
1607
			array(
1608
				'tag' => 'tr',
1609
				'before' => '<tr>',
1610
				'after' => '</tr>',
1611
				'require_parents' => array('table'),
1612
				'require_children' => array('td'),
1613
				'trim' => 'both',
1614
				'block_level' => true,
1615
				'disabled_before' => '',
1616
				'disabled_after' => '',
1617
			),
1618
			array(
1619
				'tag' => 'u',
1620
				'before' => '<u>',
1621
				'after' => '</u>',
1622
			),
1623
			array(
1624
				'tag' => 'url',
1625
				'type' => 'unparsed_content',
1626
				'content' => '<a href="$1" class="bbc_link" target="_blank">$1</a>',
1627 View Code Duplication
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled 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...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1628
				{
1629
					$data = strtr($data, array('<br>' => ''));
1630
					$scheme = parse_url($data, PHP_URL_SCHEME);
1631
					if (empty($scheme))
1632
						$data = '//' . ltrim($data, ':/');
1633
				},
1634
			),
1635
			array(
1636
				'tag' => 'url',
1637
				'type' => 'unparsed_equals',
1638
				'quoted' => 'optional',
1639
				'before' => '<a href="$1" class="bbc_link" target="_blank">',
1640
				'after' => '</a>',
1641
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled 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...
1642
				{
1643
					$scheme = parse_url($data, PHP_URL_SCHEME);
1644
					if (empty($scheme))
1645
						$data = '//' . ltrim($data, ':/');
1646
				},
1647
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1648
				'disabled_after' => ' ($1)',
1649
			),
1650
		);
1651
1652
		// Inside these tags autolink is not recommendable.
1653
		$no_autolink_tags = array(
1654
			'url',
1655
			'iurl',
1656
			'email',
1657
		);
1658
1659
		// Let mods add new BBC without hassle.
1660
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
1661
1662
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
1663
		if ($message === false)
1664
		{
1665
			if (isset($temp_bbc))
1666
				$bbc_codes = $temp_bbc;
1667
			usort($codes, function ($a, $b) {
1668
				return strcmp($a['tag'], $b['tag']);
1669
			});
1670
			return $codes;
1671
		}
1672
1673
		// So the parser won't skip them.
1674
		$itemcodes = array(
1675
			'*' => 'disc',
1676
			'@' => 'disc',
1677
			'+' => 'square',
1678
			'x' => 'square',
1679
			'#' => 'square',
1680
			'o' => 'circle',
1681
			'O' => 'circle',
1682
			'0' => 'circle',
1683
		);
1684
		if (!isset($disabled['li']) && !isset($disabled['list']))
1685
		{
1686
			foreach ($itemcodes as $c => $dummy)
1687
				$bbc_codes[$c] = array();
1688
		}
1689
1690
		// Shhhh!
1691
		if (!isset($disabled['color']))
1692
		{
1693
			$codes[] = array(
1694
				'tag' => 'chrissy',
1695
				'before' => '<span style="color: #cc0099;">',
1696
				'after' => ' :-*</span>',
1697
			);
1698
			$codes[] = array(
1699
				'tag' => 'kissy',
1700
				'before' => '<span style="color: #cc0099;">',
1701
				'after' => ' :-*</span>',
1702
			);
1703
		}
1704
1705
		foreach ($codes as $code)
1706
		{
1707
			// Make it easier to process parameters later
1708
			if (!empty($code['parameters']))
1709
				ksort($code['parameters'], SORT_STRING);
1710
1711
			// If we are not doing every tag only do ones we are interested in.
1712
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
1713
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
1714
		}
1715
		$codes = null;
0 ignored issues
show
Unused Code introduced by
$codes is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1716
	}
1717
1718
	// Shall we take the time to cache this?
1719
	if ($cache_id != '' && !empty($modSettings['cache_enable']) && (($modSettings['cache_enable'] >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
1720
	{
1721
		// It's likely this will change if the message is modified.
1722
		$cache_key = 'parse:' . $cache_id . '-' . md5(md5($message) . '-' . $smileys . (empty($disabled) ? '' : implode(',', array_keys($disabled))) . json_encode($context['browser']) . $txt['lang_locale'] . $user_info['time_offset'] . $user_info['time_format']);
1723
1724
		if (($temp = cache_get_data($cache_key, 240)) != null)
1725
			return $temp;
1726
1727
		$cache_t = microtime();
1728
	}
1729
1730
	if ($smileys === 'print')
1731
	{
1732
		// [glow], [shadow], and [move] can't really be printed.
1733
		$disabled['glow'] = true;
1734
		$disabled['shadow'] = true;
1735
		$disabled['move'] = true;
1736
1737
		// Colors can't well be displayed... supposed to be black and white.
1738
		$disabled['color'] = true;
1739
		$disabled['black'] = true;
1740
		$disabled['blue'] = true;
1741
		$disabled['white'] = true;
1742
		$disabled['red'] = true;
1743
		$disabled['green'] = true;
1744
		$disabled['me'] = true;
1745
1746
		// Color coding doesn't make sense.
1747
		$disabled['php'] = true;
1748
1749
		// Links are useless on paper... just show the link.
1750
		$disabled['ftp'] = true;
1751
		$disabled['url'] = true;
1752
		$disabled['iurl'] = true;
1753
		$disabled['email'] = true;
1754
		$disabled['flash'] = true;
1755
1756
		// @todo Change maybe?
1757
		if (!isset($_GET['images']))
1758
			$disabled['img'] = true;
1759
1760
		// @todo Interface/setting to add more?
1761
	}
1762
1763
	$open_tags = array();
1764
	$message = strtr($message, array("\n" => '<br>'));
1765
1766
	foreach ($bbc_codes as $section) {
1767
		foreach ($section as $code) {
1768
			$alltags[] = $code['tag'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$alltags was never initialized. Although not strictly required by PHP, it is generally a good practice to add $alltags = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
1769
		}
1770
	}
1771
	$alltags_regex = '\b' . implode("\b|\b", array_unique($alltags)) . '\b';
0 ignored issues
show
Bug introduced by
The variable $alltags does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1772
1773
	// The non-breaking-space looks a bit different each time.
1774
	$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
0 ignored issues
show
Unused Code introduced by
$non_breaking_space is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1775
1776
	$pos = -1;
1777
	while ($pos !== false)
1778
	{
1779
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
1780
		preg_match('~\[/?(?=' . $alltags_regex . ')~', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
1781
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
1782
1783
		// Failsafe.
1784
		if ($pos === false || $last_pos > $pos)
1785
			$pos = strlen($message) + 1;
1786
1787
		// Can't have a one letter smiley, URL, or email! (sorry.)
1788
		if ($last_pos < $pos - 1)
1789
		{
1790
			// Make sure the $last_pos is not negative.
1791
			$last_pos = max($last_pos, 0);
1792
1793
			// Pick a block of data to do some raw fixing on.
1794
			$data = substr($message, $last_pos, $pos - $last_pos);
1795
1796
			// Take care of some HTML!
1797
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
1798
			{
1799
				$data = preg_replace('~&lt;a\s+href=((?:&quot;)?)((?:https?://|ftps?://|mailto:)\S+?)\\1&gt;~i', '[url=&quot;$2&quot;]', $data);
1800
				$data = preg_replace('~&lt;/a&gt;~i', '[/url]', $data);
1801
1802
				// <br> should be empty.
1803
				$empty_tags = array('br', 'hr');
1804
				foreach ($empty_tags as $tag)
1805
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '[' . $tag . ' /]', $data);
1806
1807
				// b, u, i, s, pre... basic tags.
1808
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote');
1809
				foreach ($closable_tags as $tag)
1810
				{
1811
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
1812
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
1813
1814
					if ($diff > 0)
1815
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
1816
				}
1817
1818
				// Do <img ...> - with security... action= -> action-.
1819
				preg_match_all('~&lt;img\s+src=((?:&quot;)?)((?:https?://|ftps?://)\S+?)\\1(?:\s+alt=(&quot;.*?&quot;|\S*?))?(?:\s?/)?&gt;~i', $data, $matches, PREG_PATTERN_ORDER);
1820
				if (!empty($matches[0]))
1821
				{
1822
					$replaces = array();
1823
					foreach ($matches[2] as $match => $imgtag)
1824
					{
1825
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
1826
1827
						// Remove action= from the URL - no funny business, now.
1828
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
1829
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
1830
1831
						// Check if the image is larger than allowed.
1832
						if (!empty($modSettings['max_image_width']) && !empty($modSettings['max_image_height']))
1833
						{
1834
							list ($width, $height) = url_image_size($imgtag);
1835
1836 View Code Duplication
							if (!empty($modSettings['max_image_width']) && $width > $modSettings['max_image_width'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1837
							{
1838
								$height = (int) (($modSettings['max_image_width'] * $height) / $width);
1839
								$width = $modSettings['max_image_width'];
1840
							}
1841
1842 View Code Duplication
							if (!empty($modSettings['max_image_height']) && $height > $modSettings['max_image_height'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1843
							{
1844
								$width = (int) (($modSettings['max_image_height'] * $width) / $height);
1845
								$height = $modSettings['max_image_height'];
1846
							}
1847
1848
							// Set the new image tag.
1849
							$replaces[$matches[0][$match]] = '[img width=' . $width . ' height=' . $height . $alt . ']' . $imgtag . '[/img]';
1850
						}
1851
						else
1852
							$replaces[$matches[0][$match]] = '[img' . $alt . ']' . $imgtag . '[/img]';
1853
					}
1854
1855
					$data = strtr($data, $replaces);
1856
				}
1857
			}
1858
1859
			if (!empty($modSettings['autoLinkUrls']))
1860
			{
1861
				// Are we inside tags that should be auto linked?
1862
				$no_autolink_area = false;
1863
				if (!empty($open_tags))
1864
				{
1865
					foreach ($open_tags as $open_tag)
1866
						if (in_array($open_tag['tag'], $no_autolink_tags))
1867
							$no_autolink_area = true;
1868
				}
1869
1870
				// Don't go backwards.
1871
				// @todo Don't think is the real solution....
1872
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
1873
				if ($pos < $lastAutoPos)
1874
					$no_autolink_area = true;
1875
				$lastAutoPos = $pos;
1876
1877
				if (!$no_autolink_area)
1878
				{
1879
					// Parse any URLs
1880
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
1881
					{
1882
						$url_regex = '
1883
						(?:
1884
							# IRIs with a scheme (or at least an opening "//")
1885
							(?:
1886
								# URI scheme (or lack thereof for schemeless URLs)
1887
								(?:
1888
									# URL scheme and colon
1889
									\b[a-z][\w\-]+:
1890
									| # or
1891
									# A boundary followed by two slashes for schemeless URLs
1892
									(?<=^|\W)(?=//)
1893
								)
1894
1895
								# IRI "authority" chunk
1896
								(?:
1897
									# 2 slashes for IRIs with an "authority"
1898
									//
1899
									# then a domain name
1900
									(?:
1901
										# Either the reserved "localhost" domain name
1902
										localhost
1903
										| # or
1904
										# a run of Unicode domain name characters and a dot
1905
										[\p{L}\p{M}\p{N}\-.:@]+\.
1906
										# and then a TLD valid in the DNS or the reserved "local" TLD
1907
										(?:'. $modSettings['tld_regex'] .'|local)
1908
									)
1909
									# followed by a non-domain character or end of line
1910
									(?=[^\p{L}\p{N}\-.]|$)
1911
1912
									| # Or, if there is no "authority" per se (e.g. mailto: URLs) ...
1913
1914
									# a run of IRI characters
1915
									[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]
1916
									# and then a dot and a closing IRI label
1917
									\.[\p{L}\p{M}\p{N}\-]+
1918
								)
1919
							)
1920
1921
							| # or
1922
1923
							# Naked domains (e.g. "example.com" in "Go to example.com for an example.")
1924
							(?:
1925
								# Preceded by start of line or a non-domain character
1926
								(?<=^|[^\p{L}\p{M}\p{N}\-:@])
1927
1928
								# A run of Unicode domain name characters (excluding [:@])
1929
								[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]
1930
								# and then a dot and a valid TLD
1931
								\.' . $modSettings['tld_regex'] . '
1932
1933
								# Followed by either:
1934
								(?=
1935
									# end of line or a non-domain character (excluding [.:@])
1936
									$|[^\p{L}\p{N}\-]
1937
									| # or
1938
									# a dot followed by end of line or a non-domain character (excluding [.:@])
1939
									\.(?=$|[^\p{L}\p{N}\-])
1940
								)
1941
							)
1942
						)
1943
1944
						# IRI path, query, and fragment (if present)
1945
						(?:
1946
							# If any of these parts exist, must start with a single /
1947
							/
1948
1949
							# And then optionally:
1950
							(?:
1951
								# One or more of:
1952
								(?:
1953
									# a run of non-space, non-()<>
1954
									[^\s()<>]+
1955
									| # or
1956
									# balanced parens, up to 2 levels
1957
									\(([^\s()<>]+|(\([^\s()<>]+\)))*\)
1958
								)+
1959
1960
								# End with:
1961
								(?:
1962
									# balanced parens, up to 2 levels
1963
									\(([^\s()<>]+|(\([^\s()<>]+\)))*\)
1964
									| # or
1965
									# not a space or one of these punct char
1966
									[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]
1967
									| # or
1968
									# a trailing slash (but not two in a row)
1969
									(?<!/)/
1970
								)
1971
							)?
1972
						)?
1973
						';
1974
1975
						$data = preg_replace_callback('~' . $url_regex . '~xi' . ($context['utf8'] ? 'u' : ''), function ($matches) {
1976
							$url = array_shift($matches);
1977
1978
							$scheme = parse_url($url, PHP_URL_SCHEME);
1979
1980
							if ($scheme == 'mailto')
1981
							{
1982
								$email_address = str_replace('mailto:', '', $url);
1983
								if (!isset($disabled['email']) && filter_var($email_address, FILTER_VALIDATE_EMAIL) !== false)
0 ignored issues
show
Bug introduced by
The variable $disabled seems to never exist, and therefore isset should always return false. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
1984
									return '[email=' . $email_address . ']' . $url . '[/email]';
1985
								else
1986
									return $url;
1987
							}
1988
1989
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
1990
							if (empty($scheme))
1991
								$fullUrl = '//' . ltrim($url, ':/');
1992
							else
1993
								$fullUrl = $url;
1994
1995
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
1996
						}, $data);
1997
					}
1998
1999
					// Next, emails...
2000
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false)
2001
					{
2002
						$email_regex = '
2003
						# Preceded by a non-domain character or start of line
2004
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
2005
2006
						# An email address
2007
						[\p{L}\p{M}\p{N}_\-.]{1,80}
2008
						@
2009
						[\p{L}\p{M}\p{N}\-.]+
2010
						\.
2011
						'. $modSettings['tld_regex'] . '
2012
2013
						# Followed by either:
2014
						(?=
2015
							# end of line or a non-domain character (excluding the dot)
2016
							$|[^\p{L}\p{M}\p{N}\-]
2017
							| # or
2018
							# a dot followed by end of line or a non-domain character
2019
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2020
						)';
2021
2022
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2023
					}
2024
				}
2025
			}
2026
2027
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2028
2029
			// If it wasn't changed, no copying or other boring stuff has to happen!
2030
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2031
			{
2032
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2033
2034
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2035
				$old_pos = strlen($data) + $last_pos;
2036
				$pos = strpos($message, '[', $last_pos);
2037
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2038
			}
2039
		}
2040
2041
		// Are we there yet?  Are we there yet?
2042
		if ($pos >= strlen($message) - 1)
2043
			break;
2044
2045
		$tags = strtolower($message[$pos + 1]);
2046
2047
		if ($tags == '/' && !empty($open_tags))
2048
		{
2049
			$pos2 = strpos($message, ']', $pos + 1);
2050
			if ($pos2 == $pos + 2)
2051
				continue;
2052
2053
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2054
2055
			$to_close = array();
2056
			$block_level = null;
2057
2058
			do
2059
			{
2060
				$tag = array_pop($open_tags);
2061
				if (!$tag)
2062
					break;
2063
2064
				if (!empty($tag['block_level']))
2065
				{
2066
					// Only find out if we need to.
2067
					if ($block_level === false)
2068
					{
2069
						array_push($open_tags, $tag);
2070
						break;
2071
					}
2072
2073
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2074 View Code Duplication
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2075
					{
2076
						foreach ($bbc_codes[$look_for[0]] as $temp)
2077
							if ($temp['tag'] == $look_for)
2078
							{
2079
								$block_level = !empty($temp['block_level']);
2080
								break;
2081
							}
2082
					}
2083
2084
					if ($block_level !== true)
2085
					{
2086
						$block_level = false;
2087
						array_push($open_tags, $tag);
2088
						break;
2089
					}
2090
				}
2091
2092
				$to_close[] = $tag;
2093
			}
2094
			while ($tag['tag'] != $look_for);
2095
2096
			// Did we just eat through everything and not find it?
2097
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2098
			{
2099
				$open_tags = $to_close;
2100
				continue;
2101
			}
2102
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2103
			{
2104 View Code Duplication
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2105
				{
2106
					foreach ($bbc_codes[$look_for[0]] as $temp)
2107
						if ($temp['tag'] == $look_for)
2108
						{
2109
							$block_level = !empty($temp['block_level']);
2110
							break;
2111
						}
2112
				}
2113
2114
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2115
				if (!$block_level)
0 ignored issues
show
Bug Best Practice introduced by
The expression $block_level of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
2116
				{
2117
					foreach ($to_close as $tag)
2118
						array_push($open_tags, $tag);
2119
					continue;
2120
				}
2121
			}
2122
2123
			foreach ($to_close as $tag)
2124
			{
2125
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2126
				$pos += strlen($tag['after']) + 2;
2127
				$pos2 = $pos - 1;
2128
2129
				// See the comment at the end of the big loop - just eating whitespace ;).
2130 View Code Duplication
				if (!empty($tag['block_level']) && substr($message, $pos, 4) == '<br>')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2131
					$message = substr($message, 0, $pos) . substr($message, $pos + 4);
2132 View Code Duplication
				if (!empty($tag['trim']) && $tag['trim'] != 'inside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos), $matches) != 0)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2133
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2134
			}
2135
2136
			if (!empty($to_close))
2137
			{
2138
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
$to_close is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
2139
				$pos--;
2140
			}
2141
2142
			continue;
2143
		}
2144
2145
		// No tags for this character, so just keep going (fastest possible course.)
2146
		if (!isset($bbc_codes[$tags]))
2147
			continue;
2148
2149
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2150
		$tag = null;
2151
		foreach ($bbc_codes[$tags] as $possible)
2152
		{
2153
			$pt_strlen = strlen($possible['tag']);
2154
2155
			// Not a match?
2156
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2157
				continue;
2158
2159
			$next_c = $message[$pos + 1 + $pt_strlen];
2160
2161
			// A test validation?
2162
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2163
				continue;
2164
			// Do we want parameters?
2165
			elseif (!empty($possible['parameters']))
2166
			{
2167
				if ($next_c != ' ')
2168
					continue;
2169
			}
2170
			elseif (isset($possible['type']))
2171
			{
2172
				// Do we need an equal sign?
2173
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2174
					continue;
2175
				// Maybe we just want a /...
2176
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2177
					continue;
2178
				// An immediate ]?
2179
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2180
					continue;
2181
			}
2182
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2183
			elseif ($next_c != ']')
2184
				continue;
2185
2186
			// Check allowed tree?
2187
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2188
				continue;
2189
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2190
				continue;
2191
			// If this is in the list of disallowed child tags, don't parse it.
2192
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2193
				continue;
2194
2195
			$pos1 = $pos + 1 + $pt_strlen + 1;
2196
2197
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2198
			if ($possible['tag'] == 'quote')
2199
			{
2200
				// Start with standard
2201
				$quote_alt = false;
2202
				foreach ($open_tags as $open_quote)
2203
				{
2204
					// Every parent quote this quote has flips the styling
2205
					if ($open_quote['tag'] == 'quote')
2206
						$quote_alt = !$quote_alt;
2207
				}
2208
				// Add a class to the quote to style alternating blockquotes
2209
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2210
			}
2211
2212
			// This is long, but it makes things much easier and cleaner.
2213
			if (!empty($possible['parameters']))
2214
			{
2215
				// Build a regular expression for each parameter for the current tag.
2216
				$preg = array();
2217
				foreach ($possible['parameters'] as $p => $info)
2218
					$preg[] = '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
2219
2220
				// Extract the string that potentially holds our parameters.
2221
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
2222
				$blobs = preg_split('~\]~i', $blob[1]);
2223
2224
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
2225
2226
				// Progressively append more blobs until we find our parameters or run out of blobs
2227
				$blob_counter = 1;
2228
				while ($blob_counter <= count($blobs))
2229
				{
2230
2231
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
2232
2233
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2234
					sort($given_params, SORT_STRING);
2235
2236
					$match = preg_match('~^' . implode('', $preg) . '$~i', implode(' ', $given_params), $matches) !== 0;
2237
2238
					if ($match)
2239
						$blob_counter = count($blobs) + 1;
2240
				}
2241
2242
				// Didn't match our parameter list, try the next possible.
2243
				if (!$match)
0 ignored issues
show
Bug introduced by
The variable $match does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2244
					continue;
2245
2246
				$params = array();
2247
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2248
				{
2249
					$key = strtok(ltrim($matches[$i]), '=');
2250
					if (isset($possible['parameters'][$key]['value']))
2251
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2252
					elseif (isset($possible['parameters'][$key]['validate']))
2253
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2254
					else
2255
						$params['{' . $key . '}'] = $matches[$i + 1];
2256
2257
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2258
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2259
				}
2260
2261
				foreach ($possible['parameters'] as $p => $info)
2262
				{
2263
					if (!isset($params['{' . $p . '}']))
2264
						$params['{' . $p . '}'] = '';
2265
				}
2266
2267
				$tag = $possible;
2268
2269
				// Put the parameters into the string.
2270
				if (isset($tag['before']))
2271
					$tag['before'] = strtr($tag['before'], $params);
2272
				if (isset($tag['after']))
2273
					$tag['after'] = strtr($tag['after'], $params);
2274
				if (isset($tag['content']))
2275
					$tag['content'] = strtr($tag['content'], $params);
2276
2277
				$pos1 += strlen($given_param_string);
0 ignored issues
show
Bug introduced by
The variable $given_param_string does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2278
			}
2279
			else
2280
			{
2281
				$tag = $possible;
2282
				$params = array();
2283
			}
2284
			break;
2285
		}
2286
2287
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2288
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2289
		{
2290
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2291
				continue;
2292
2293
			$tag = $itemcodes[$message[$pos + 1]];
2294
2295
			// First let's set up the tree: it needs to be in a list, or after an li.
2296
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2297
			{
2298
				$open_tags[] = array(
2299
					'tag' => 'list',
2300
					'after' => '</ul>',
2301
					'block_level' => true,
2302
					'require_children' => array('li'),
2303
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2304
				);
2305
				$code = '<ul class="bbc_list">';
2306
			}
2307
			// We're in a list item already: another itemcode?  Close it first.
2308
			elseif ($inside['tag'] == 'li')
2309
			{
2310
				array_pop($open_tags);
2311
				$code = '</li>';
2312
			}
2313
			else
2314
				$code = '';
2315
2316
			// Now we open a new tag.
2317
			$open_tags[] = array(
2318
				'tag' => 'li',
2319
				'after' => '</li>',
2320
				'trim' => 'outside',
2321
				'block_level' => true,
2322
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2323
			);
2324
2325
			// First, open the tag...
2326
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2327
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2328
			$pos += strlen($code) - 1 + 2;
2329
2330
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2331
			$pos2 = strpos($message, '<br>', $pos);
2332
			$pos3 = strpos($message, '[/', $pos);
2333
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2334
			{
2335
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2336
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2337
2338
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2339
			}
2340
			// Tell the [list] that it needs to close specially.
2341
			else
2342
			{
2343
				// Move the li over, because we're not sure what we'll hit.
2344
				$open_tags[count($open_tags) - 1]['after'] = '';
2345
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2346
			}
2347
2348
			continue;
2349
		}
2350
2351
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2352
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2353
		{
2354
			array_pop($open_tags);
2355
2356
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2357
			$pos += strlen($inside['after']) - 1 + 2;
2358
		}
2359
2360
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2361
		if ($tag === null)
2362
			continue;
2363
2364
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2365
		if (isset($inside['disallow_children']))
2366
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2367
2368
		// Is this tag disabled?
2369
		if (isset($disabled[$tag['tag']]))
2370
		{
2371
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2372
			{
2373
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2374
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2375
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2376
			}
2377
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2378
			{
2379
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2380
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2381
			}
2382
			else
2383
				$tag['content'] = $tag['disabled_content'];
2384
		}
2385
2386
		// we use this a lot
2387
		$tag_strlen = strlen($tag['tag']);
2388
2389
		// The only special case is 'html', which doesn't need to close things.
2390
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2391
		{
2392
			$n = count($open_tags) - 1;
2393
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2394
				$n--;
2395
2396
			// Close all the non block level tags so this tag isn't surrounded by them.
2397
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2398
			{
2399
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2400
				$ot_strlen = strlen($open_tags[$i]['after']);
2401
				$pos += $ot_strlen + 2;
2402
				$pos1 += $ot_strlen + 2;
0 ignored issues
show
Bug introduced by
The variable $pos1 does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2403
2404
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2405 View Code Duplication
				if (!empty($open_tags[$i]['block_level']) && substr($message, $pos, 4) == '<br>')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2406
					$message = substr($message, 0, $pos) . substr($message, $pos + 4);
2407 View Code Duplication
				if (!empty($open_tags[$i]['trim']) && $tag['trim'] != 'inside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos), $matches) != 0)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2408
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2409
2410
				array_pop($open_tags);
2411
			}
2412
		}
2413
2414
		// No type means 'parsed_content'.
2415
		if (!isset($tag['type']))
2416
		{
2417
			// @todo Check for end tag first, so people can say "I like that [i] tag"?
2418
			$open_tags[] = $tag;
2419
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2420
			$pos += strlen($tag['before']) - 1 + 2;
2421
		}
2422
		// Don't parse the content, just skip it.
2423
		elseif ($tag['type'] == 'unparsed_content')
2424
		{
2425
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2426
			if ($pos2 === false)
2427
				continue;
2428
2429
			$data = substr($message, $pos1, $pos2 - $pos1);
2430
2431
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2432
				$data = substr($data, 4);
2433
2434
			if (isset($tag['validate']))
2435
				$tag['validate']($tag, $data, $disabled, $params);
0 ignored issues
show
Bug introduced by
The variable $params does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
2436
2437
			$code = strtr($tag['content'], array('$1' => $data));
2438
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2439
2440
			$pos += strlen($code) - 1 + 2;
2441
			$last_pos = $pos + 1;
2442
2443
		}
2444
		// Don't parse the content, just skip it.
2445
		elseif ($tag['type'] == 'unparsed_equals_content')
2446
		{
2447
			// The value may be quoted for some tags - check.
2448 View Code Duplication
			if (isset($tag['quoted']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2449
			{
2450
				$quoted = substr($message, $pos1, 6) == '&quot;';
2451
				if ($tag['quoted'] != 'optional' && !$quoted)
2452
					continue;
2453
2454
				if ($quoted)
2455
					$pos1 += 6;
2456
			}
2457
			else
2458
				$quoted = false;
2459
2460
			$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
2461
			if ($pos2 === false)
2462
				continue;
2463
2464
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2465
			if ($pos3 === false)
2466
				continue;
2467
2468
			$data = array(
2469
				substr($message, $pos2 + ($quoted == false ? 1 : 7), $pos3 - ($pos2 + ($quoted == false ? 1 : 7))),
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
2470
				substr($message, $pos1, $pos2 - $pos1)
2471
			);
2472
2473
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
2474
				$data[0] = substr($data[0], 4);
2475
2476
			// Validation for my parking, please!
2477
			if (isset($tag['validate']))
2478
				$tag['validate']($tag, $data, $disabled, $params);
2479
2480
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
2481
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2482
			$pos += strlen($code) - 1 + 2;
2483
		}
2484
		// A closed tag, with no content or value.
2485
		elseif ($tag['type'] == 'closed')
2486
		{
2487
			$pos2 = strpos($message, ']', $pos);
2488
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
2489
			$pos += strlen($tag['content']) - 1 + 2;
2490
		}
2491
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
2492
		elseif ($tag['type'] == 'unparsed_commas_content')
2493
		{
2494
			$pos2 = strpos($message, ']', $pos1);
2495
			if ($pos2 === false)
2496
				continue;
2497
2498
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2499
			if ($pos3 === false)
2500
				continue;
2501
2502
			// We want $1 to be the content, and the rest to be csv.
2503
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
2504
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
2505
2506
			if (isset($tag['validate']))
2507
				$tag['validate']($tag, $data, $disabled, $params);
2508
2509
			$code = $tag['content'];
2510 View Code Duplication
			foreach ($data as $k => $d)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2511
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2512
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2513
			$pos += strlen($code) - 1 + 2;
2514
		}
2515
		// This has parsed content, and a csv value which is unparsed.
2516
		elseif ($tag['type'] == 'unparsed_commas')
2517
		{
2518
			$pos2 = strpos($message, ']', $pos1);
2519
			if ($pos2 === false)
2520
				continue;
2521
2522
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
2523
2524
			if (isset($tag['validate']))
2525
				$tag['validate']($tag, $data, $disabled, $params);
2526
2527
			// Fix after, for disabled code mainly.
2528 View Code Duplication
			foreach ($data as $k => $d)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2529
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
2530
2531
			$open_tags[] = $tag;
2532
2533
			// Replace them out, $1, $2, $3, $4, etc.
2534
			$code = $tag['before'];
2535 View Code Duplication
			foreach ($data as $k => $d)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2536
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2537
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
2538
			$pos += strlen($code) - 1 + 2;
2539
		}
2540
		// A tag set to a value, parsed or not.
2541
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
2542
		{
2543
			// The value may be quoted for some tags - check.
2544 View Code Duplication
			if (isset($tag['quoted']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2545
			{
2546
				$quoted = substr($message, $pos1, 6) == '&quot;';
2547
				if ($tag['quoted'] != 'optional' && !$quoted)
2548
					continue;
2549
2550
				if ($quoted)
2551
					$pos1 += 6;
2552
			}
2553
			else
2554
				$quoted = false;
2555
2556
			$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
2557
			if ($pos2 === false)
2558
				continue;
2559
2560
			$data = substr($message, $pos1, $pos2 - $pos1);
2561
2562
			// Validation for my parking, please!
2563
			if (isset($tag['validate']))
2564
				$tag['validate']($tag, $data, $disabled, $params);
2565
2566
			// For parsed content, we must recurse to avoid security problems.
2567
			if ($tag['type'] != 'unparsed_equals')
2568
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
2569
2570
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
2571
2572
			$open_tags[] = $tag;
2573
2574
			$code = strtr($tag['before'], array('$1' => $data));
2575
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 7));
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
2576
			$pos += strlen($code) - 1 + 2;
2577
		}
2578
2579
		// If this is block level, eat any breaks after it.
2580 View Code Duplication
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2581
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
2582
2583
		// Are we trimming outside this tag?
2584
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
2585
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
2586
	}
2587
2588
	// Close any remaining tags.
2589
	while ($tag = array_pop($open_tags))
2590
		$message .= "\n" . $tag['after'] . "\n";
2591
2592
	// Parse the smileys within the parts where it can be done safely.
2593
	if ($smileys === true)
2594
	{
2595
		$message_parts = explode("\n", $message);
2596
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
2597
			parsesmileys($message_parts[$i]);
2598
2599
		$message = implode('', $message_parts);
2600
	}
2601
2602
	// No smileys, just get rid of the markers.
2603
	else
2604
		$message = strtr($message, array("\n" => ''));
2605
2606
	if ($message !== '' && $message[0] === ' ')
2607
		$message = '&nbsp;' . substr($message, 1);
2608
2609
	// Cleanup whitespace.
2610
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
2611
2612
	// Allow mods access to what parse_bbc created
2613
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
2614
2615
	// Cache the output if it took some time...
2616
	if (isset($cache_key, $cache_t) && array_sum(explode(' ', microtime())) - array_sum(explode(' ', $cache_t)) > 0.05)
2617
		cache_put_data($cache_key, $message, 240);
2618
2619
	// If this was a force parse revert if needed.
2620
	if (!empty($parse_tags))
2621
	{
2622
		if (empty($temp_bbc))
2623
			$bbc_codes = array();
2624
		else
2625
		{
2626
			$bbc_codes = $temp_bbc;
2627
			unset($temp_bbc);
2628
		}
2629
	}
2630
2631
	return $message;
2632
}
2633
2634
/**
2635
 * Parse smileys in the passed message.
2636
 *
2637
 * The smiley parsing function which makes pretty faces appear :).
2638
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
2639
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
2640
 * Caches the smileys from the database or array in memory.
2641
 * Doesn't return anything, but rather modifies message directly.
2642
 *
2643
 * @param string &$message The message to parse smileys in
2644
 */
2645
function parsesmileys(&$message)
2646
{
2647
	global $modSettings, $txt, $user_info, $context, $smcFunc;
2648
	static $smileyPregSearch = null, $smileyPregReplacements = array();
2649
2650
	// No smiley set at all?!
2651
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
2652
		return;
2653
2654
	// If smileyPregSearch hasn't been set, do it now.
2655
	if (empty($smileyPregSearch))
2656
	{
2657
		// Use the default smileys if it is disabled. (better for "portability" of smileys.)
2658
		if (empty($modSettings['smiley_enable']))
2659
		{
2660
			$smileysfrom = array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', '0:)');
2661
			$smileysto = array('evil.gif', 'cheesy.gif', 'rolleyes.gif', 'angry.gif', 'laugh.gif', 'smiley.gif', 'wink.gif', 'grin.gif', 'sad.gif', 'shocked.gif', 'cool.gif', 'tongue.gif', 'huh.gif', 'embarrassed.gif', 'lipsrsealed.gif', 'kiss.gif', 'cry.gif', 'undecided.gif', 'azn.gif', 'afro.gif', 'police.gif', 'angel.gif');
2662
			$smileysdescs = array('', $txt['icon_cheesy'], $txt['icon_rolleyes'], $txt['icon_angry'], '', $txt['icon_smiley'], $txt['icon_wink'], $txt['icon_grin'], $txt['icon_sad'], $txt['icon_shocked'], $txt['icon_cool'], $txt['icon_tongue'], $txt['icon_huh'], $txt['icon_embarrassed'], $txt['icon_lips'], $txt['icon_kiss'], $txt['icon_cry'], $txt['icon_undecided'], '', '', '', '');
2663
		}
2664
		else
2665
		{
2666
			// Load the smileys in reverse order by length so they don't get parsed wrong.
2667
			if (($temp = cache_get_data('parsing_smileys', 480)) == null)
2668
			{
2669
				$result = $smcFunc['db_query']('', '
2670
					SELECT code, filename, description
2671
					FROM {db_prefix}smileys
2672
					ORDER BY LENGTH(code) DESC',
2673
					array(
2674
					)
2675
				);
2676
				$smileysfrom = array();
2677
				$smileysto = array();
2678
				$smileysdescs = array();
2679
				while ($row = $smcFunc['db_fetch_assoc']($result))
2680
				{
2681
					$smileysfrom[] = $row['code'];
2682
					$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
2683
					$smileysdescs[] = $row['description'];
2684
				}
2685
				$smcFunc['db_free_result']($result);
2686
2687
				cache_put_data('parsing_smileys', array($smileysfrom, $smileysto, $smileysdescs), 480);
2688
			}
2689
			else
2690
				list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
2691
		}
2692
2693
		// The non-breaking-space is a complex thing...
2694
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
2695
2696
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
2697
		$smileyPregReplacements = array();
2698
		$searchParts = array();
2699
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
2700
2701
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
2702
		{
2703
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
2704
			$smileyCode = '<img src="' . $smileys_path . $smileysto[$i] . '" alt="' . strtr($specialChars, array(':' => '&#58;', '(' => '&#40;', ')' => '&#41;', '$' => '&#36;', '[' => '&#091;')). '" title="' . strtr($smcFunc['htmlspecialchars']($smileysdescs[$i]), array(':' => '&#58;', '(' => '&#40;', ')' => '&#41;', '$' => '&#36;', '[' => '&#091;')) . '" class="smiley">';
2705
2706
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
2707
2708
			$searchParts[] = preg_quote($smileysfrom[$i], '~');
2709
			if ($smileysfrom[$i] != $specialChars)
2710
			{
2711
				$smileyPregReplacements[$specialChars] = $smileyCode;
2712
				$searchParts[] = preg_quote($specialChars, '~');
2713
			}
2714
		}
2715
2716
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . implode('|', $searchParts) . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
2717
	}
2718
2719
	// Replace away!
2720
	$message = preg_replace_callback($smileyPregSearch,
2721
		function ($matches) use ($smileyPregReplacements)
2722
		{
2723
			return $smileyPregReplacements[$matches[1]];
2724
		}, $message);
2725
}
2726
2727
/**
2728
 * Highlight any code.
2729
 *
2730
 * Uses PHP's highlight_string() to highlight PHP syntax
2731
 * does special handling to keep the tabs in the code available.
2732
 * used to parse PHP code from inside [code] and [php] tags.
2733
 *
2734
 * @param string $code The code
2735
 * @return string The code with highlighted HTML.
2736
 */
2737
function highlight_php_code($code)
2738
{
2739
	// Remove special characters.
2740
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
2741
2742
	$oldlevel = error_reporting(0);
2743
2744
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
2745
2746
	error_reporting($oldlevel);
2747
2748
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
2749
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
2750
2751
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
2752
}
2753
2754
/**
2755
 * Make sure the browser doesn't come back and repost the form data.
2756
 * Should be used whenever anything is posted.
2757
 *
2758
 * @param string $setLocation The URL to redirect them to
2759
 * @param bool $refresh Whether to use a meta refresh instead
2760
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
2761
 */
2762
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
2763
{
2764
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
2765
2766
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
2767
	if (!empty($context['flush_mail']))
2768
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
2769
		AddMailQueue(true);
2770
2771
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
2772
2773
	if ($add)
2774
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
2775
2776
	// Put the session ID in.
2777
	if (defined('SID') && SID != '')
2778
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
2779
	// Keep that debug in their for template debugging!
2780 View Code Duplication
	elseif (isset($_GET['debug']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2781
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
2782
2783
	if (!empty($modSettings['queryless_urls']) && (empty($context['server']['is_cgi']) || ini_get('cgi.fix_pathinfo') == 1 || @get_cfg_var('cgi.fix_pathinfo') == 1) && (!empty($context['server']['is_apache']) || !empty($context['server']['is_lighttpd']) || !empty($context['server']['is_litespeed'])))
2784
	{
2785
		if (defined('SID') && SID != '')
2786
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '/') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
2787
				function ($m) use ($scripturl)
2788
				{
2789
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID. (isset($m[2]) ? "$m[2]" : "");
2790
				}, $setLocation);
2791 View Code Duplication
		else
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2792
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '/') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
2793
				function ($m) use ($scripturl)
2794
				{
2795
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
2796
				}, $setLocation);
2797
	}
2798
2799
	// Maybe integrations want to change where we are heading?
2800
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
2801
2802
	// Set the header.
2803
	header('Location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
2804
2805
	// Debugging.
2806
	if (isset($db_show_debug) && $db_show_debug === true)
2807
		$_SESSION['debug_redirect'] = $db_cache;
2808
2809
	obExit(false);
2810
}
2811
2812
/**
2813
 * Ends execution.  Takes care of template loading and remembering the previous URL.
2814
 * @param bool $header Whether to do the header
2815
 * @param bool $do_footer Whether to do the footer
2816
 * @param bool $from_index Whether we're coming from the board index
2817
 * @param bool $from_fatal_error Whether we're coming from a fatal error
2818
 */
2819
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
2820
{
2821
	global $context, $settings, $modSettings, $txt, $smcFunc;
2822
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
2823
2824
	// Attempt to prevent a recursive loop.
2825
	++$level;
2826
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
2827
		exit;
2828
	if ($from_fatal_error)
2829
		$has_fatal_error = true;
2830
2831
	// Clear out the stat cache.
2832
	trackStats();
2833
2834
	// If we have mail to send, send it.
2835
	if (!empty($context['flush_mail']))
2836
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
2837
		AddMailQueue(true);
2838
2839
	$do_header = $header === null ? !$header_done : $header;
2840
	if ($do_footer === null)
2841
		$do_footer = $do_header;
2842
2843
	// Has the template/header been done yet?
2844
	if ($do_header)
2845
	{
2846
		// Was the page title set last minute? Also update the HTML safe one.
2847
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
2848
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
2849
2850
		// Start up the session URL fixer.
2851
		ob_start('ob_sessrewrite');
2852
2853
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
2854
			$buffers = explode(',', $settings['output_buffers']);
2855
		elseif (!empty($settings['output_buffers']))
2856
			$buffers = $settings['output_buffers'];
2857
		else
2858
			$buffers = array();
2859
2860
		if (isset($modSettings['integrate_buffer']))
2861
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
2862
2863
		if (!empty($buffers))
2864
			foreach ($buffers as $function)
2865
			{
2866
				$call = call_helper($function, true);
2867
2868
				// Is it valid?
2869
				if (!empty($call))
2870
					ob_start($call);
2871
			}
2872
2873
		// Display the screen in the logical order.
2874
		template_header();
2875
		$header_done = true;
2876
	}
2877
	if ($do_footer)
2878
	{
2879
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
2880
2881
		// Anything special to put out?
2882
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
2883
			echo $context['insert_after_template'];
2884
2885
		// Just so we don't get caught in an endless loop of errors from the footer...
2886
		if (!$footer_done)
2887
		{
2888
			$footer_done = true;
2889
			template_footer();
2890
2891
			// (since this is just debugging... it's okay that it's after </html>.)
2892
			if (!isset($_REQUEST['xml']))
2893
				displayDebug();
2894
		}
2895
	}
2896
2897
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
2898
	if (strpos($_SERVER['REQUEST_URL'], 'action=dlattach') === false && strpos($_SERVER['REQUEST_URL'], 'action=viewsmfile') === false)
2899
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
2900
2901
	// For session check verification.... don't switch browsers...
2902
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
2903
2904
	// Hand off the output to the portal, etc. we're integrated with.
2905
	call_integration_hook('integrate_exit', array($do_footer));
2906
2907
	// Don't exit if we're coming from index.php; that will pass through normally.
2908
	if (!$from_index)
2909
		exit;
2910
}
2911
2912
/**
2913
 * Get the size of a specified image with better error handling.
2914
 * @todo see if it's better in Subs-Graphics, but one step at the time.
2915
 * Uses getimagesize() to determine the size of a file.
2916
 * Attempts to connect to the server first so it won't time out.
2917
 *
2918
 * @param string $url The URL of the image
2919
 * @return array|false The image size as array (width, height), or false on failure
2920
 */
2921
function url_image_size($url)
2922
{
2923
	global $sourcedir;
2924
2925
	// Make sure it is a proper URL.
2926
	$url = str_replace(' ', '%20', $url);
2927
2928
	// Can we pull this from the cache... please please?
2929
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
2930
		return $temp;
2931
	$t = microtime();
2932
2933
	// Get the host to pester...
2934
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
2935
2936
	// Can't figure it out, just try the image size.
2937
	if ($url == '' || $url == 'http://' || $url == 'https://')
2938
	{
2939
		return false;
2940
	}
2941
	elseif (!isset($match[1]))
2942
	{
2943
		$size = @getimagesize($url);
2944
	}
2945
	else
2946
	{
2947
		// Try to connect to the server... give it half a second.
2948
		$temp = 0;
2949
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
2950
2951
		// Successful?  Continue...
2952
		if ($fp != false)
2953
		{
2954
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
2955
			fwrite($fp, 'HEAD /' . $match[2] . ' HTTP/1.1' . "\r\n" . 'Host: ' . $match[1] . "\r\n" . 'User-Agent: PHP/SMF' . "\r\n" . 'Connection: close' . "\r\n\r\n");
2956
2957
			// Read in the HTTP/1.1 or whatever.
2958
			$test = substr(fgets($fp, 11), -1);
2959
			fclose($fp);
2960
2961
			// See if it returned a 404/403 or something.
2962
			if ($test < 4)
2963
			{
2964
				$size = @getimagesize($url);
2965
2966
				// This probably means allow_url_fopen is off, let's try GD.
2967
				if ($size === false && function_exists('imagecreatefromstring'))
2968
				{
2969
					include_once($sourcedir . '/Subs-Package.php');
2970
2971
					// It's going to hate us for doing this, but another request...
2972
					$image = @imagecreatefromstring(fetch_web_data($url));
2973
					if ($image !== false)
2974
					{
2975
						$size = array(imagesx($image), imagesy($image));
2976
						imagedestroy($image);
2977
					}
2978
				}
2979
			}
2980
		}
2981
	}
2982
2983
	// If we didn't get it, we failed.
2984
	if (!isset($size))
2985
		$size = false;
2986
2987
	// If this took a long time, we may never have to do it again, but then again we might...
2988 View Code Duplication
	if (array_sum(explode(' ', microtime())) - array_sum(explode(' ', $t)) > 0.8)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2989
		cache_put_data('url_image_size-' . md5($url), $size, 240);
2990
2991
	// Didn't work.
2992
	return $size;
2993
}
2994
2995
/**
2996
 * Sets up the basic theme context stuff.
2997
 * @param bool $forceload Whether to load the theme even if it's already loaded
2998
 */
2999
function setupThemeContext($forceload = false)
3000
{
3001
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3002
	global $smcFunc;
3003
	static $loaded = false;
3004
3005
	// Under SSI this function can be called more then once.  That can cause some problems.
3006
	//   So only run the function once unless we are forced to run it again.
3007
	if ($loaded && !$forceload)
3008
		return;
3009
3010
	$loaded = true;
3011
3012
	$context['in_maintenance'] = !empty($maintenance);
3013
	$context['current_time'] = timeformat(time(), false);
3014
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3015
3016
	// Get some news...
3017
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3018
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3019
	{
3020
		if (trim($context['news_lines'][$i]) == '')
3021
			continue;
3022
3023
		// Clean it up for presentation ;).
3024
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3025
	}
3026
	if (!empty($context['news_lines']))
3027
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3028
3029
	if (!$user_info['is_guest'])
3030
	{
3031
		$context['user']['messages'] = &$user_info['messages'];
3032
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3033
		$context['user']['alerts'] = &$user_info['alerts'];
3034
3035
		// Personal message popup...
3036
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3037
			$context['user']['popup_messages'] = true;
3038
		else
3039
			$context['user']['popup_messages'] = false;
3040
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3041
3042
		if (allowedTo('moderate_forum'))
3043
			$context['unapproved_members'] = (!empty($modSettings['registration_method']) && ($modSettings['registration_method'] == 2 || (!empty($modSettings['coppaType']) && $modSettings['coppaType'] == 2))) || !empty($modSettings['approveAccountDeletion']) ? $modSettings['unapprovedMembers'] : 0;
3044
3045
		$context['user']['avatar'] = array();
3046
3047
		// Check for gravatar first since we might be forcing them...
3048
		if (($modSettings['gravatarEnabled'] && substr($user_info['avatar']['url'], 0, 11) == 'gravatar://') || !empty($modSettings['gravatarOverride']))
3049
		{
3050
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3051
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3052
			else
3053
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3054
		}
3055
		// Uploaded?
3056
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3057
			$context['user']['avatar']['href'] = $user_info['avatar']['custom_dir'] ? $modSettings['custom_avatar_url'] . '/' . $user_info['avatar']['filename'] : $scripturl . '?action=dlattach;attach=' . $user_info['avatar']['id_attach'] . ';type=avatar';
3058
		// Full URL?
3059
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3060
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3061
		// Otherwise we assume it's server stored.
3062
		elseif ($user_info['avatar']['url'] != '')
3063
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3064
		// No avatar at all? Fine, we have a big fat default avatar ;)
3065
		else
3066
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3067
3068
		if (!empty($context['user']['avatar']))
3069
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3070
3071
		// Figure out how long they've been logged in.
3072
		$context['user']['total_time_logged_in'] = array(
3073
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3074
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3075
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3076
		);
3077
	}
3078
	else
3079
	{
3080
		$context['user']['messages'] = 0;
3081
		$context['user']['unread_messages'] = 0;
3082
		$context['user']['avatar'] = array();
3083
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3084
		$context['user']['popup_messages'] = false;
3085
3086
		if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
3087
			$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
3088
3089
		// If we've upgraded recently, go easy on the passwords.
3090
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3091
			$context['disable_login_hashing'] = true;
3092
	}
3093
3094
	// Setup the main menu items.
3095
	setupMenuContext();
3096
3097
	// This is here because old index templates might still use it.
3098
	$context['show_news'] = !empty($settings['enable_news']);
3099
3100
	// This is done to allow theme authors to customize it as they want.
3101
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3102
3103
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3104
	if ($context['show_pm_popup'])
3105
		addInlineJavaScript('
3106
		jQuery(document).ready(function($) {
3107
			new smc_Popup({
3108
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3109
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3110
				icon_class: \'generic_icons mail_new\'
3111
			});
3112
		});');
3113
3114
	// Add a generic "Are you sure?" confirmation message.
3115
	addInlineJavaScript('
3116
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) .';');
3117
3118
	// Now add the capping code for avatars.
3119
	if (!empty($modSettings['avatar_max_width_external']) && !empty($modSettings['avatar_max_height_external']) && !empty($modSettings['avatar_action_too_large']) && $modSettings['avatar_action_too_large'] == 'option_css_resize')
3120
		addInlineCss('
3121
img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3122
3123
	// This looks weird, but it's because BoardIndex.php references the variable.
3124
	$context['common_stats']['latest_member'] = array(
3125
		'id' => $modSettings['latestMember'],
3126
		'name' => $modSettings['latestRealName'],
3127
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3128
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3129
	);
3130
	$context['common_stats'] = array(
3131
		'total_posts' => comma_format($modSettings['totalMessages']),
3132
		'total_topics' => comma_format($modSettings['totalTopics']),
3133
		'total_members' => comma_format($modSettings['totalMembers']),
3134
		'latest_member' => $context['common_stats']['latest_member'],
3135
	);
3136
	$context['common_stats']['boardindex_total_posts'] = sprintf($txt['boardindex_total_posts'], $context['common_stats']['total_posts'], $context['common_stats']['total_topics'], $context['common_stats']['total_members']);
3137
3138
	if (empty($settings['theme_version']))
3139
		addJavaScriptVar('smf_scripturl', $scripturl);
3140
3141
	if (!isset($context['page_title']))
3142
		$context['page_title'] = '';
3143
3144
	// Set some specific vars.
3145
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3146
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3147
3148
	// Content related meta tags, including Open Graph
3149
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3150
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3151
3152
	if (!empty($context['meta_keywords']))
3153
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3154
3155
	if (!empty($context['canonical_url']))
3156
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3157
3158
	if (!empty($settings['og_image']))
3159
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3160
3161
	if (!empty($context['meta_description']))
3162
	{
3163
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3164
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3165
	}
3166
	else
3167
	{
3168
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3169
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3170
	}
3171
3172
	call_integration_hook('integrate_theme_context');
3173
}
3174
3175
/**
3176
 * Helper function to set the system memory to a needed value
3177
 * - If the needed memory is greater than current, will attempt to get more
3178
 * - if in_use is set to true, will also try to take the current memory usage in to account
3179
 *
3180
 * @param string $needed The amount of memory to request, if needed, like 256M
3181
 * @param bool $in_use Set to true to account for current memory usage of the script
3182
 * @return boolean True if we have at least the needed memory
3183
 */
3184
function setMemoryLimit($needed, $in_use = false)
3185
{
3186
	// everything in bytes
3187
	$memory_used = 0;
0 ignored issues
show
Unused Code introduced by
$memory_used is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
3188
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3189
	$memory_needed = memoryReturnBytes($needed);
3190
3191
	// should we account for how much is currently being used?
3192
	if ($in_use)
3193
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3194
3195
	// if more is needed, request it
3196
	if ($memory_current < $memory_needed)
3197
	{
3198
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
3199
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3200
	}
3201
3202
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
3203
3204
	// return success or not
3205
	return (bool) ($memory_current >= $memory_needed);
3206
}
3207
3208
/**
3209
 * Helper function to convert memory string settings to bytes
3210
 *
3211
 * @param string $val The byte string, like 256M or 1G
3212
 * @return integer The string converted to a proper integer in bytes
3213
 */
3214
function memoryReturnBytes($val)
3215
{
3216
	if (is_integer($val))
3217
		return $val;
3218
3219
	// Separate the number from the designator
3220
	$val = trim($val);
3221
	$num = intval(substr($val, 0, strlen($val) - 1));
3222
	$last = strtolower(substr($val, -1));
3223
3224
	// convert to bytes
3225
	switch ($last)
3226
	{
3227
		case 'g':
3228
			$num *= 1024;
3229
		case 'm':
3230
			$num *= 1024;
3231
		case 'k':
3232
			$num *= 1024;
3233
	}
3234
	return $num;
3235
}
3236
3237
/**
3238
 * The header template
3239
 */
3240
function template_header()
3241
{
3242
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir;
3243
3244
	setupThemeContext();
3245
3246
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
3247
	if (empty($context['no_last_modified']))
3248
	{
3249
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
3250
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
3251
3252
		// Are we debugging the template/html content?
3253
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
3254
			header('Content-Type: application/xhtml+xml');
3255 View Code Duplication
		elseif (!isset($_REQUEST['xml']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3256
			header('Content-Type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3257
	}
3258
3259
	header('Content-Type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3260
3261
	// We need to splice this in after the body layer, or after the main layer for older stuff.
3262
	if ($context['in_maintenance'] && $context['user']['is_admin'])
3263
	{
3264
		$position = array_search('body', $context['template_layers']);
3265
		if ($position === false)
3266
			$position = array_search('main', $context['template_layers']);
3267
3268
		if ($position !== false)
3269
		{
3270
			$before = array_slice($context['template_layers'], 0, $position + 1);
3271
			$after = array_slice($context['template_layers'], $position + 1);
3272
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
3273
		}
3274
	}
3275
3276
	$checked_securityFiles = false;
3277
	$showed_banned = false;
3278
	foreach ($context['template_layers'] as $layer)
3279
	{
3280
		loadSubTemplate($layer . '_above', true);
3281
3282
		// May seem contrived, but this is done in case the body and main layer aren't there...
3283
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
3284
		{
3285
			$checked_securityFiles = true;
3286
3287
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
3288
3289
			// Add your own files.
3290
			call_integration_hook('integrate_security_files', array(&$securityFiles));
3291
3292
			foreach ($securityFiles as $i => $securityFile)
3293
			{
3294
				if (!file_exists($boarddir . '/' . $securityFile))
3295
					unset($securityFiles[$i]);
3296
			}
3297
3298
			// We are already checking so many files...just few more doesn't make any difference! :P
3299 View Code Duplication
			if (!empty($modSettings['currentAttachmentUploadDir']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3300
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
3301
3302
			else
3303
			{
3304
				$path = $modSettings['attachmentUploadDir'];
3305
				$id_folder_thumb = 1;
0 ignored issues
show
Unused Code introduced by
$id_folder_thumb is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
3306
			}
3307
			secureDirectory($path, true);
3308
			secureDirectory($cachedir);
3309
3310
			// If agreement is enabled, at least the english version shall exists
3311
			if ($modSettings['requireAgreement'])
3312
				$agreement = !file_exists($boarddir . '/agreement.txt');
3313
3314
			if (!empty($securityFiles) || (!empty($modSettings['cache_enable']) && !is_writable($cachedir)) || !empty($agreement))
3315
			{
3316
				echo '
3317
		<div class="errorbox">
3318
			<p class="alert">!!</p>
3319
			<h3>', empty($securityFiles) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
3320
			<p>';
3321
3322
				foreach ($securityFiles as $securityFile)
3323
				{
3324
					echo '
3325
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
3326
3327
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
3328
						echo '
3329
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
3330
				}
3331
3332
				if (!empty($modSettings['cache_enable']) && !is_writable($cachedir))
3333
					echo '
3334
				<strong>', $txt['cache_writable'], '</strong><br>';
3335
3336
				if (!empty($agreement))
3337
					echo '
3338
				<strong>', $txt['agreement_missing'], '</strong><br>';
3339
3340
				echo '
3341
			</p>
3342
		</div>';
3343
			}
3344
		}
3345
		// If the user is banned from posting inform them of it.
3346
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
3347
		{
3348
			$showed_banned = true;
3349
			echo '
3350
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
3351
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
3352
3353
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
3354
				echo '
3355
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
3356
3357
			if (!empty($_SESSION['ban']['expire_time']))
3358
				echo '
3359
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
3360
			else
3361
				echo '
3362
					<div>', $txt['your_ban_expires_never'], '</div>';
3363
3364
			echo '
3365
				</div>';
3366
		}
3367
	}
3368
}
3369
3370
/**
3371
 * Show the copyright.
3372
 */
3373
function theme_copyright()
3374
{
3375
	global $forum_copyright, $software_year, $forum_version;
3376
3377
	// Don't display copyright for things like SSI.
3378
	if (!isset($forum_version) || !isset($software_year))
3379
		return;
3380
3381
	// Put in the version...
3382
	printf($forum_copyright, $forum_version, $software_year);
3383
}
3384
3385
/**
3386
 * The template footer
3387
 */
3388
function template_footer()
3389
{
3390
	global $context, $modSettings, $time_start, $db_count;
3391
3392
	// Show the load time?  (only makes sense for the footer.)
3393
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
3394
	$context['load_time'] = comma_format(round(array_sum(explode(' ', microtime())) - array_sum(explode(' ', $time_start)), 3));
3395
	$context['load_queries'] = $db_count;
3396
3397
	foreach (array_reverse($context['template_layers']) as $layer)
3398
		loadSubTemplate($layer . '_below', true);
3399
}
3400
3401
/**
3402
 * Output the Javascript files
3403
 * 	- tabbing in this function is to make the HTML source look good proper
3404
 *  - if defered is set function will output all JS (source & inline) set to load at page end
3405
 *
3406
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
3407
 */
3408
function template_javascript($do_deferred = false)
3409
{
3410
	global $context, $modSettings, $settings;
3411
3412
	// Use this hook to minify/optimize Javascript files and vars
3413
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
3414
3415
	$toMinify = array();
3416
	$toMinifyDefer = array();
3417
3418
	// Ouput the declared Javascript variables.
3419
	if (!empty($context['javascript_vars']) && !$do_deferred)
3420
	{
3421
		echo '
3422
	<script>';
3423
3424
		foreach ($context['javascript_vars'] as $key => $value)
3425
		{
3426
			if (empty($value))
3427
			{
3428
				echo '
3429
		var ', $key, ';';
3430
			}
3431
			else
3432
			{
3433
				echo '
3434
		var ', $key, ' = ', $value, ';';
3435
			}
3436
		}
3437
3438
		echo '
3439
	</script>';
3440
	}
3441
3442
	// While we have JavaScript files to place in the template.
3443
	foreach ($context['javascript_files'] as $id => $js_file)
3444
	{
3445
		// Last minute call! allow theme authors to disable single files.
3446
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
3447
			continue;
3448
3449
		// By default all files don't get minimized unless the file explicitly says so!
3450
		if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
3451
		{
3452
			if ($do_deferred && !empty($js_file['options']['defer']))
3453
				$toMinifyDefer[] = $js_file;
3454
3455
			elseif (!$do_deferred && empty($js_file['options']['defer']))
3456
				$toMinify[] = $js_file;
3457
3458
			// Grab a random seed.
3459
			if (!isset($minSeed))
3460
				$minSeed = $js_file['options']['seed'];
3461
		}
3462
3463
		elseif ((!$do_deferred && empty($js_file['options']['defer'])) || ($do_deferred && !empty($js_file['options']['defer'])))
3464
			echo '
3465
	<script src="', $js_file['fileUrl'], '"', !empty($js_file['options']['async']) ? ' async="async"' : '', '></script>';
3466
	}
3467
3468
	if ((!$do_deferred && !empty($toMinify)) || ($do_deferred && !empty($toMinifyDefer)))
3469
	{
3470
		$result = custMinify(($do_deferred ? $toMinifyDefer : $toMinify), 'js', $do_deferred);
3471
3472
		// Minify process couldn't work, print each individual files.
3473
		if (!empty($result) && is_array($result))
3474
			foreach ($result as $minFailedFile)
3475
				echo '
3476
	<script src="', $minFailedFile['fileUrl'], '"', !empty($minFailedFile['options']['async']) ? ' async="async"' : '', '></script>';
3477
3478
		else
3479
			echo '
3480
	<script src="', $settings['theme_url'] ,'/scripts/minified', ($do_deferred ? '_deferred' : '') ,'.js', $minSeed ,'"></script>';
0 ignored issues
show
Bug introduced by
The variable $minSeed does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3481
	}
3482
3483
	// Inline JavaScript - Actually useful some times!
3484
	if (!empty($context['javascript_inline']))
3485
	{
3486 View Code Duplication
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3487
		{
3488
			echo '
3489
<script>';
3490
3491
			foreach ($context['javascript_inline']['defer'] as $js_code)
3492
				echo $js_code;
3493
3494
			echo '
3495
</script>';
3496
		}
3497
3498 View Code Duplication
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3499
		{
3500
			echo '
3501
	<script>';
3502
3503
			foreach ($context['javascript_inline']['standard'] as $js_code)
3504
				echo $js_code;
3505
3506
			echo '
3507
	</script>';
3508
		}
3509
	}
3510
}
3511
3512
/**
3513
 * Output the CSS files
3514
 *
3515
 */
3516
function template_css()
3517
{
3518
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
3519
3520
	// Use this hook to minify/optimize CSS files
3521
	call_integration_hook('integrate_pre_css_output');
3522
3523
	$toMinify = array();
3524
	$normal = array();
3525
3526
	foreach ($context['css_files'] as $id => $file)
3527
	{
3528
		// Last minute call! allow theme authors to disable single files.
3529
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
3530
			continue;
3531
3532
		// By default all files don't get minimized unless the file explicitly says so!
3533
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']))
3534
		{
3535
			$toMinify[] = $file;
3536
3537
			// Grab a random seed.
3538
			if (!isset($minSeed))
3539
				$minSeed = $file['options']['seed'];
3540
		}
3541
3542
		else
3543
			$normal[] = $file['fileUrl'];
3544
	}
3545
3546
	if (!empty($toMinify))
3547
	{
3548
		$result = custMinify($toMinify, 'css');
3549
3550
		// Minify process couldn't work, print each individual files.
3551
		if (!empty($result) && is_array($result))
3552
			foreach ($result as $minFailedFile)
3553
				echo '
3554
	<link rel="stylesheet" href="', $minFailedFile['fileUrl'], '">';
3555
3556
		else
3557
			echo '
3558
	<link rel="stylesheet" href="', $settings['theme_url'] ,'/css/minified.css', $minSeed ,'">';
0 ignored issues
show
Bug introduced by
The variable $minSeed does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3559
	}
3560
3561
	// Print the rest after the minified files.
3562
	if (!empty($normal))
3563
		foreach ($normal as $nf)
3564
			echo '
3565
	<link rel="stylesheet" href="', $nf ,'">';
3566
3567
	if ($db_show_debug === true)
3568
	{
3569
		// Try to keep only what's useful.
3570
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
3571
		foreach ($context['css_files'] as $file)
3572
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
3573
	}
3574
3575
	if (!empty($context['css_header']))
3576
	{
3577
		echo '
3578
	<style>';
3579
3580
		foreach ($context['css_header'] as $css)
3581
			echo $css .'
3582
	';
3583
3584
		echo'
3585
	</style>';
3586
	}
3587
}
3588
3589
/**
3590
 * Get an array of previously defined files and adds them to our main minified file.
3591
 * Sets a one day cache to avoid re-creating a file on every request.
3592
 *
3593
 * @param array $data The files to minify.
3594
 * @param string $type either css or js.
3595
 * @param bool $do_deferred use for type js to indicate if the minified file will be deferred, IE, put at the closing </body> tag.
3596
 * @return bool|array If an array the minify process failed and the data is returned intact.
3597
 */
3598
function custMinify($data, $type, $do_deferred = false)
3599
{
3600
	global $sourcedir, $smcFunc, $settings, $txt, $context;
3601
3602
	$types = array('css', 'js');
3603
	$type = !empty($type) && in_array($type, $types) ? $type : false;
3604
	$data = !empty($data) ? $data : false;
3605
	$minFailed = array();
0 ignored issues
show
Unused Code introduced by
$minFailed is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
3606
3607
	if (empty($type) || empty($data))
3608
		return false;
3609
3610
	// Did we already did this?
3611
	$toCache = cache_get_data('minimized_'. $settings['theme_id'] .'_'. $type, 86400);
3612
3613
	// Already done?
3614
	if (!empty($toCache))
3615
		return true;
3616
3617
	// Yep, need a bunch of files.
3618
	require_once($sourcedir . '/minify/src/Minify.php');
3619
	require_once($sourcedir . '/minify/src/'. strtoupper($type) .'.php');
3620
	require_once($sourcedir . '/minify/src/Exception.php');
3621
	require_once($sourcedir . '/minify/src/Converter.php');
3622
3623
	// No namespaces, sorry!
3624
	$classType = 'MatthiasMullie\\Minify\\'. strtoupper($type);
3625
3626
	// Temp path.
3627
	$cTempPath = $settings['theme_dir'] .'/'. ($type == 'css' ? 'css' : 'scripts') .'/';
3628
3629
	// What kind of file are we going to create?
3630
	$toCreate = $cTempPath .'minified'. ($do_deferred ? '_deferred' : '') .'.'. $type;
3631
3632
	// File has to exists, if it isn't try to create it.
3633
	if ((!file_exists($toCreate) && @fopen($toCreate, 'w') === false) || !smf_chmod($toCreate))
3634
	{
3635
		loadLanguage('Errors');
3636
		log_error(sprintf($txt['file_not_created'], $toCreate), 'general');
3637
		cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, null);
3638
3639
		// The process failed so roll back to print each individual file.
3640
		return $data;
3641
	}
3642
3643
	$minifier = new $classType();
3644
3645
	foreach ($data as $file)
3646
	{
3647
		$tempFile = str_replace($file['options']['seed'], '', $file['filePath']);
3648
		$toAdd = file_exists($tempFile) ? $tempFile : false;
3649
3650
		// The file couldn't be located so it won't be added, log this error.
3651
		if (empty($toAdd))
3652
		{
3653
			loadLanguage('Errors');
3654
			log_error(sprintf($txt['file_minimize_fail'], $file['fileName']), 'general');
3655
			continue;
3656
		}
3657
3658
		// Add this file to the list.
3659
		$minifier->add($toAdd);
3660
	}
3661
3662
	// Create the file.
3663
	$minifier->minify($toCreate);
3664
	unset($minifier);
3665
	clearstatcache();
3666
3667
	// Minify process failed.
3668
	if (!filesize($toCreate))
3669
	{
3670
		loadLanguage('Errors');
3671
		log_error(sprintf($txt['file_not_created'], $toCreate), 'general');
3672
		cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, null);
3673
3674
		// The process failed so roll back to print each individual file.
3675
		return $data;
3676
	}
3677
3678
	// And create a long lived cache entry.
3679
	cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, $toCreate, 86400);
3680
3681
	return true;
3682
}
3683
3684
/**
3685
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
3686
 * @todo this currently returns the hash if new, and the full filename otherwise.
3687
 * Something messy like that.
3688
 * @todo and of course everything relies on this behavior and work around it. :P.
3689
 * Converters included.
3690
 *
3691
 * @param string $filename The name of the file
3692
 * @param int $attachment_id The ID of the attachment
3693
 * @param string $dir Which directory it should be in (null to use current one)
3694
 * @param bool $new Whether this is a new attachment
3695
 * @param string $file_hash The file hash
3696
 * @return string The path to the file
3697
 */
3698
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
3699
{
3700
	global $modSettings, $smcFunc;
3701
3702
	// Just make up a nice hash...
3703
	if ($new)
3704
		return sha1(md5($filename . time()) . mt_rand());
3705
3706
	// Grab the file hash if it wasn't added.
3707
	// Left this for legacy.
3708
	if ($file_hash === '')
3709
	{
3710
		$request = $smcFunc['db_query']('', '
3711
			SELECT file_hash
3712
			FROM {db_prefix}attachments
3713
			WHERE id_attach = {int:id_attach}',
3714
			array(
3715
				'id_attach' => $attachment_id,
3716
			));
3717
3718
		if ($smcFunc['db_num_rows']($request) === 0)
3719
			return false;
3720
3721
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
3722
		$smcFunc['db_free_result']($request);
3723
	}
3724
3725
	// Still no hash? mmm...
3726
	if (empty($file_hash))
3727
		$file_hash = sha1(md5($filename . time()) . mt_rand());
3728
3729
	// Are we using multiple directories?
3730 View Code Duplication
	if (!empty($modSettings['currentAttachmentUploadDir']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3731
		$path = $modSettings['attachmentUploadDir'][$dir];
3732
3733
	else
3734
		$path = $modSettings['attachmentUploadDir'];
3735
3736
	return $path . '/' . $attachment_id . '_' . $file_hash .'.dat';
3737
}
3738
3739
/**
3740
 * Convert a single IP to a ranged IP.
3741
 * internal function used to convert a user-readable format to a format suitable for the database.
3742
 *
3743
 * @param string $fullip The full IP
3744
 * @return array An array of IP parts
3745
 */
3746
function ip2range($fullip)
3747
{
3748
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
3749
	if ($fullip == 'unknown')
3750
		$fullip = '255.255.255.255';
3751
3752
	$ip_parts = explode('-', $fullip);
3753
	$ip_array = array();
3754
3755
	// if ip 22.12.31.21
3756
	if (count($ip_parts) == 1 && isValidIP($fullip))
3757
	{
3758
		$ip_array['low'] = $fullip;
3759
		$ip_array['high'] = $fullip;
3760
		return $ip_array;
3761
	} // if ip 22.12.* -> 22.12.* - 22.12.*
3762
	elseif (count($ip_parts) == 1)
3763
	{
3764
		$ip_parts[0] = $fullip;
3765
		$ip_parts[1] = $fullip;
3766
	}
3767
3768
	// if ip 22.12.31.21-12.21.31.21
3769
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
3770
	{
3771
		$ip_array['low'] = $ip_parts[0];
3772
		$ip_array['high'] = $ip_parts[1];
3773
		return $ip_array;
3774
	}
3775
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
3776
	{
3777
		$valid_low = isValidIP($ip_parts[0]);
3778
		$valid_high = isValidIP($ip_parts[1]);
3779
		$count = 0;
3780
		$mode = (preg_match('/:/',$ip_parts[0]) > 0 ? ':' : '.');
3781
		$max = ($mode == ':' ? 'ffff' : '255');
3782
		$min = 0;
3783 View Code Duplication
		if(!$valid_low)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3784
		{
3785
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
3786
			$valid_low = isValidIP($ip_parts[0]);
3787
			while (!$valid_low)
3788
			{
3789
				$ip_parts[0] .= $mode . $min;
3790
				$valid_low = isValidIP($ip_parts[0]);
3791
				$count++;
3792
				if ($count > 9) break;
3793
			}
3794
		}
3795
3796
		$count = 0;
3797 View Code Duplication
		if(!$valid_high)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3798
		{
3799
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
3800
			$valid_high = isValidIP($ip_parts[1]);
3801
			while (!$valid_high)
3802
			{
3803
				$ip_parts[1] .= $mode . $max;
3804
				$valid_high = isValidIP($ip_parts[1]);
3805
				$count++;
3806
				if ($count > 9) break;
3807
			}
3808
		}
3809
3810
		if($valid_high && $valid_low)
3811
		{
3812
			$ip_array['low'] = $ip_parts[0];
3813
			$ip_array['high'] = $ip_parts[1];
3814
		}
3815
3816
	}
3817
3818
	return $ip_array;
3819
}
3820
3821
/**
3822
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
3823
 *
3824
 * @param string $ip The IP to get the hostname from
3825
 * @return string The hostname
3826
 */
3827
function host_from_ip($ip)
3828
{
3829
	global $modSettings;
3830
3831
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
3832
		return $host;
3833
	$t = microtime();
3834
3835
	// Try the Linux host command, perhaps?
3836
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
3837
	{
3838
		if (!isset($modSettings['host_to_dis']))
3839
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
3840
		else
3841
			$test = @shell_exec('host ' . @escapeshellarg($ip));
3842
3843
		// Did host say it didn't find anything?
3844
		if (strpos($test, 'not found') !== false)
3845
			$host = '';
3846
		// Invalid server option?
3847
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
3848
			updateSettings(array('host_to_dis' => 1));
3849
		// Maybe it found something, after all?
3850
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
3851
			$host = $match[1];
3852
	}
3853
3854
	// This is nslookup; usually only Windows, but possibly some Unix?
3855
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
3856
	{
3857
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
3858
		if (strpos($test, 'Non-existent domain') !== false)
3859
			$host = '';
3860
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
3861
			$host = $match[1];
3862
	}
3863
3864
	// This is the last try :/.
3865
	if (!isset($host) || $host === false)
3866
		$host = @gethostbyaddr($ip);
3867
3868
	// It took a long time, so let's cache it!
3869 View Code Duplication
	if (array_sum(explode(' ', microtime())) - array_sum(explode(' ', $t)) > 0.5)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3870
		cache_put_data('hostlookup-' . $ip, $host, 600);
3871
3872
	return $host;
3873
}
3874
3875
/**
3876
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
3877
 *
3878
 * @param string $text The text to split into words
3879
 * @param int $max_chars The maximum number of characters per word
3880
 * @param bool $encrypt Whether to encrypt the results
3881
 * @return array An array of ints or words depending on $encrypt
3882
 */
3883
function text2words($text, $max_chars = 20, $encrypt = false)
0 ignored issues
show
Best Practice introduced by
The function text2words() has been defined more than once; this definition is ignored, only the first definition in other/upgrade.php (L157-177) is considered.

This check looks for functions that have already been defined in other files.

Some Codebases, like WordPress, make a practice of defining functions multiple times. This may lead to problems with the detection of function parameters and types. If you really need to do this, you can mark the duplicate definition with the @ignore annotation.

/**
 * @ignore
 */
function getUser() {

}

function getUser($id, $realm) {

}

See also the PhpDoc documentation for @ignore.

Loading history...
3884
{
3885
	global $smcFunc, $context;
3886
3887
	// Step 1: Remove entities/things we don't consider words:
3888
	$words = preg_replace('~(?:[\x0B\0' . ($context['utf8'] ? '\x{A0}' : '\xA0') . '\t\r\s\n(){}\\[\\]<>!@$%^*.,:+=`\~\?/\\\\]+|&(?:amp|lt|gt|quot);)+~' . ($context['utf8'] ? 'u' : ''), ' ', strtr($text, array('<br>' => ' ')));
3889
3890
	// Step 2: Entities we left to letters, where applicable, lowercase.
3891
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
3892
3893
	// Step 3: Ready to split apart and index!
3894
	$words = explode(' ', $words);
3895
3896
	if ($encrypt)
3897
	{
3898
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
3899
		$returned_ints = array();
3900
		foreach ($words as $word)
3901
		{
3902
			if (($word = trim($word, '-_\'')) !== '')
3903
			{
3904
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
3905
				$total = 0;
3906
				for ($i = 0; $i < $max_chars; $i++)
3907
					$total += $possible_chars[ord($encrypted{$i})] * pow(63, $i);
3908
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
3909
			}
3910
		}
3911
		return array_unique($returned_ints);
3912
	}
3913
	else
3914
	{
3915
		// Trim characters before and after and add slashes for database insertion.
3916
		$returned_words = array();
3917
		foreach ($words as $word)
3918
			if (($word = trim($word, '-_\'')) !== '')
3919
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
3920
3921
		// Filter out all words that occur more than once.
3922
		return array_unique($returned_words);
3923
	}
3924
}
3925
3926
/**
3927
 * Creates an image/text button
3928
 *
3929
 * @param string $name The name of the button (should be a generic_icons class or the name of an image)
3930
 * @param string $alt The alt text
3931
 * @param string $label The $txt string to use as the label
3932
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
3933
 * @param boolean $force_use Whether to force use of this when template_create_button is available
3934
 * @return string The HTML to display the button
3935
 */
3936
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
3937
{
3938
	global $settings, $txt;
3939
3940
	// Does the current loaded theme have this and we are not forcing the usage of this function?
3941
	if (function_exists('template_create_button') && !$force_use)
3942
		return template_create_button($name, $alt, $label = '', $custom = '');
3943
3944
	if (!$settings['use_image_buttons'])
3945
		return $txt[$alt];
3946
	elseif (!empty($settings['use_buttons']))
3947
		return '<span class="generic_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
3948
	else
3949
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
3950
}
3951
3952
/**
3953
 * Empty out the cache in use as best it can
3954
 *
3955
 * It may only remove the files of a certain type (if the $type parameter is given)
3956
 * Type can be user, data or left blank
3957
 * 	- user clears out user data
3958
 *  - data clears out system / opcode data
3959
 *  - If no type is specified will perform a complete cache clearing
3960
 * For cache engines that do not distinguish on types, a full cache flush will be done
3961
 *
3962
 * @param string $type The cache type ('memcached', 'apc', 'xcache', 'zend' or something else for SMF's file cache)
3963
 */
3964
function clean_cache($type = '')
0 ignored issues
show
Best Practice introduced by
The function clean_cache() has been defined more than once; this definition is ignored, only the first definition in other/upgrade.php (L183-204) is considered.

This check looks for functions that have already been defined in other files.

Some Codebases, like WordPress, make a practice of defining functions multiple times. This may lead to problems with the detection of function parameters and types. If you really need to do this, you can mark the duplicate definition with the @ignore annotation.

/**
 * @ignore
 */
function getUser() {

}

function getUser($id, $realm) {

}

See also the PhpDoc documentation for @ignore.

Loading history...
3965
{
3966
	global $cachedir, $sourcedir, $cache_accelerator, $modSettings, $memcached;
3967
3968
	switch ($cache_accelerator)
3969
	{
3970
		case 'memcached':
3971
			if (function_exists('memcache_flush') || function_exists('memcached_flush') && isset($modSettings['cache_memcached']) && trim($modSettings['cache_memcached']) != '')
3972
			{
3973
				// Not connected yet?
3974
				if (empty($memcached))
3975
					get_memcached_server();
3976
				if (!$memcached)
3977
					return;
3978
3979
				// clear it out
3980
				if (function_exists('memcache_flush'))
3981
					memcache_flush($memcached);
3982
				else
3983
					memcached_flush($memcached);
3984
			}
3985
			break;
3986
		case 'apc':
3987
			if (function_exists('apc_clear_cache'))
3988
			{
3989
				// if passed a type, clear that type out
3990
				if ($type === '' || $type === 'data')
3991
				{
3992
					apc_clear_cache('user');
3993
					apc_clear_cache('system');
3994
				}
3995
				elseif ($type === 'user')
3996
					apc_clear_cache('user');
3997
			}
3998
			break;
3999
		case 'zend':
4000
			if (function_exists('zend_shm_cache_clear'))
4001
				zend_shm_cache_clear('SMF');
4002
			break;
4003
		case 'xcache':
4004
			if (function_exists('xcache_clear_cache'))
4005
			{
4006
				//
4007
				if ($type === '')
4008
				{
4009
					xcache_clear_cache(XC_TYPE_VAR, 0);
4010
					xcache_clear_cache(XC_TYPE_PHP, 0);
4011
				}
4012
				if ($type === 'user')
4013
					xcache_clear_cache(XC_TYPE_VAR, 0);
4014
				if ($type === 'data')
4015
					xcache_clear_cache(XC_TYPE_PHP, 0);
4016
			}
4017
			break;
4018
		default:
4019
			// No directory = no game.
4020
			if (!is_dir($cachedir))
4021
				return;
4022
4023
			// Remove the files in SMF's own disk cache, if any
4024
			$dh = opendir($cachedir);
4025 View Code Duplication
			while ($file = readdir($dh))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4026
			{
4027
				if ($file != '.' && $file != '..' && $file != 'index.php' && $file != '.htaccess' && (!$type || substr($file, 0, strlen($type)) == $type))
4028
					@unlink($cachedir . '/' . $file);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
4029
			}
4030
			closedir($dh);
4031
			break;
4032
	}
4033
4034
	// Invalidate cache, to be sure!
4035
	// ... as long as index.php can be modified, anyway.
4036
	if (empty($type))
4037
		@touch($cachedir . '/' . 'index.php');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
4038
4039
	call_integration_hook('integrate_clean_cache');
4040
	clearstatcache();
4041
}
4042
4043
/**
4044
 * Sets up all of the top menu buttons
4045
 * Saves them in the cache if it is available and on
4046
 * Places the results in $context
4047
 *
4048
 */
4049
function setupMenuContext()
4050
{
4051
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings;
4052
4053
	// Set up the menu privileges.
4054
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
4055
	$context['allow_admin'] = allowedTo(array('admin_forum', 'manage_boards', 'manage_permissions', 'moderate_forum', 'manage_membergroups', 'manage_bans', 'send_mail', 'edit_news', 'manage_attachments', 'manage_smileys'));
4056
4057
	$context['allow_memberlist'] = allowedTo('view_mlist');
4058
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
4059
	$context['allow_moderation_center'] = $context['user']['can_mod'];
4060
	$context['allow_pm'] = allowedTo('pm_read');
4061
4062
	$cacheTime = $modSettings['lastActive'] * 60;
4063
4064
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
4065
	if (!isset($context['allow_calendar_event']))
4066
	{
4067
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
4068
4069
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
4070 View Code Duplication
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4071
		{
4072
			$boards_can_post = boardsAllowedTo('post_new');
4073
			$context['allow_calendar_event'] &= !empty($boards_can_post);
4074
		}
4075
	}
4076
4077
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
4078
	if (!$context['user']['is_guest'])
4079
	{
4080
		addInlineJavaScript('
4081
	var user_menus = new smc_PopupMenu();
4082
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
4083
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u='. $context['user']['id'] .'");', true);
4084
		if ($context['allow_pm'])
4085
			addInlineJavaScript('
4086
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
4087
4088
		if (!empty($modSettings['enable_ajax_alerts']))
4089
		{
4090
			require_once($sourcedir . '/Subs-Notify.php');
4091
4092
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
4093
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
4094
4095
			addInlineJavaScript('
4096
	var new_alert_title = "' . $context['forum_name'] . '";
4097
	var alert_timeout = ' . $timeout . ';');
4098
			loadJavaScriptFile('alerts.js', array(), 'smf_alerts');
4099
		}
4100
	}
4101
4102
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
4103
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
4104
	{
4105
		$buttons = array(
4106
			'home' => array(
4107
				'title' => $txt['home'],
4108
				'href' => $scripturl,
4109
				'show' => true,
4110
				'sub_buttons' => array(
4111
				),
4112
				'is_last' => $context['right_to_left'],
4113
			),
4114
			'search' => array(
4115
				'title' => $txt['search'],
4116
				'href' => $scripturl . '?action=search',
4117
				'show' => $context['allow_search'],
4118
				'sub_buttons' => array(
4119
				),
4120
			),
4121
			'admin' => array(
4122
				'title' => $txt['admin'],
4123
				'href' => $scripturl . '?action=admin',
4124
				'show' => $context['allow_admin'],
4125
				'sub_buttons' => array(
4126
					'featuresettings' => array(
4127
						'title' => $txt['modSettings_title'],
4128
						'href' => $scripturl . '?action=admin;area=featuresettings',
4129
						'show' => allowedTo('admin_forum'),
4130
					),
4131
					'packages' => array(
4132
						'title' => $txt['package'],
4133
						'href' => $scripturl . '?action=admin;area=packages',
4134
						'show' => allowedTo('admin_forum'),
4135
					),
4136
					'errorlog' => array(
4137
						'title' => $txt['errlog'],
4138
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
4139
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
4140
					),
4141
					'permissions' => array(
4142
						'title' => $txt['edit_permissions'],
4143
						'href' => $scripturl . '?action=admin;area=permissions',
4144
						'show' => allowedTo('manage_permissions'),
4145
					),
4146
					'memberapprove' => array(
4147
						'title' => $txt['approve_members_waiting'],
4148
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
4149
						'show' => !empty($context['unapproved_members']),
4150
						'is_last' => true,
4151
					),
4152
				),
4153
			),
4154
			'moderate' => array(
4155
				'title' => $txt['moderate'],
4156
				'href' => $scripturl . '?action=moderate',
4157
				'show' => $context['allow_moderation_center'],
4158
				'sub_buttons' => array(
4159
					'modlog' => array(
4160
						'title' => $txt['modlog_view'],
4161
						'href' => $scripturl . '?action=moderate;area=modlog',
4162
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4163
					),
4164
					'poststopics' => array(
4165
						'title' => $txt['mc_unapproved_poststopics'],
4166
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
4167
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4168
					),
4169
					'attachments' => array(
4170
						'title' => $txt['mc_unapproved_attachments'],
4171
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
4172
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4173
					),
4174
					'reports' => array(
4175
						'title' => $txt['mc_reported_posts'],
4176
						'href' => $scripturl . '?action=moderate;area=reportedposts',
4177
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4178
					),
4179
					'reported_members' => array(
4180
						'title' => $txt['mc_reported_members'],
4181
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
4182
						'show' => allowedTo('moderate_forum'),
4183
						'is_last' => true,
4184
					)
4185
				),
4186
			),
4187
			'calendar' => array(
4188
				'title' => $txt['calendar'],
4189
				'href' => $scripturl . '?action=calendar',
4190
				'show' => $context['allow_calendar'],
4191
				'sub_buttons' => array(
4192
					'view' => array(
4193
						'title' => $txt['calendar_menu'],
4194
						'href' => $scripturl . '?action=calendar',
4195
						'show' => $context['allow_calendar_event'],
4196
					),
4197
					'post' => array(
4198
						'title' => $txt['calendar_post_event'],
4199
						'href' => $scripturl . '?action=calendar;sa=post',
4200
						'show' => $context['allow_calendar_event'],
4201
						'is_last' => true,
4202
					),
4203
				),
4204
			),
4205
			'mlist' => array(
4206
				'title' => $txt['members_title'],
4207
				'href' => $scripturl . '?action=mlist',
4208
				'show' => $context['allow_memberlist'],
4209
				'sub_buttons' => array(
4210
					'mlist_view' => array(
4211
						'title' => $txt['mlist_menu_view'],
4212
						'href' => $scripturl . '?action=mlist',
4213
						'show' => true,
4214
					),
4215
					'mlist_search' => array(
4216
						'title' => $txt['mlist_search'],
4217
						'href' => $scripturl . '?action=mlist;sa=search',
4218
						'show' => true,
4219
						'is_last' => true,
4220
					),
4221
				),
4222
			),
4223
			'signup' => array(
4224
				'title' => $txt['register'],
4225
				'href' => $scripturl . '?action=signup',
4226
				'show' => $user_info['is_guest'] && $context['can_register'],
4227
				'sub_buttons' => array(
4228
				),
4229
				'is_last' => !$context['right_to_left'],
4230
			),
4231
			'logout' => array(
4232
				'title' => $txt['logout'],
4233
				'href' => $scripturl . '?action=logout;%1$s=%2$s',
4234
				'show' => !$user_info['is_guest'],
4235
				'sub_buttons' => array(
4236
				),
4237
				'is_last' => !$context['right_to_left'],
4238
			),
4239
		);
4240
4241
		// Allow editing menu buttons easily.
4242
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
4243
4244
		// Now we put the buttons in the context so the theme can use them.
4245
		$menu_buttons = array();
4246
		foreach ($buttons as $act => $button)
4247
			if (!empty($button['show']))
4248
			{
4249
				$button['active_button'] = false;
4250
4251
				// This button needs some action.
4252
				if (isset($button['action_hook']))
4253
					$needs_action_hook = true;
4254
4255
				// Make sure the last button truly is the last button.
4256
				if (!empty($button['is_last']))
4257
				{
4258
					if (isset($last_button))
4259
						unset($menu_buttons[$last_button]['is_last']);
4260
					$last_button = $act;
4261
				}
4262
4263
				// Go through the sub buttons if there are any.
4264
				if (!empty($button['sub_buttons']))
4265
					foreach ($button['sub_buttons'] as $key => $subbutton)
4266
					{
4267
						if (empty($subbutton['show']))
4268
							unset($button['sub_buttons'][$key]);
4269
4270
						// 2nd level sub buttons next...
4271
						if (!empty($subbutton['sub_buttons']))
4272
						{
4273
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
4274
							{
4275
								if (empty($sub_button2['show']))
4276
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
4277
							}
4278
						}
4279
					}
4280
4281
				// Does this button have its own icon?
4282
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
4283
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
4284
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
4285
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
4286
				elseif (isset($button['icon']))
4287
					$button['icon'] = '<span class="generic_icons ' . $button['icon'] . '"></span>';
4288
				else
4289
					$button['icon'] = '<span class="generic_icons ' . $act . '"></span>';
4290
4291
				$menu_buttons[$act] = $button;
4292
			}
4293
4294
		if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2)
4295
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
4296
	}
4297
4298
	$context['menu_buttons'] = $menu_buttons;
4299
4300
	// Logging out requires the session id in the url.
4301
	if (isset($context['menu_buttons']['logout']))
4302
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
4303
4304
	// Figure out which action we are doing so we can set the active tab.
4305
	// Default to home.
4306
	$current_action = 'home';
4307
4308
	if (isset($context['menu_buttons'][$context['current_action']]))
4309
		$current_action = $context['current_action'];
4310
	elseif ($context['current_action'] == 'search2')
4311
		$current_action = 'search';
4312
	elseif ($context['current_action'] == 'theme')
4313
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
4314
	elseif ($context['current_action'] == 'register2')
4315
		$current_action = 'register';
4316
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
4317
		$current_action = 'login';
4318
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
4319
		$current_action = 'moderate';
4320
4321
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
4322
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
4323
	{
4324
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
4325
		$context[$current_action] = true;
4326
	}
4327
	elseif ($context['current_action'] == 'pm')
4328
	{
4329
		$current_action = 'self_pm';
4330
		$context['self_pm'] = true;
4331
	}
4332
4333
	$total_mod_reports = 0;
4334
4335
	if (!empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1' && !empty($context['open_mod_reports']))
4336
	{
4337
		$total_mod_reports = $context['open_mod_reports'];
4338
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['title'] .= ' <span class="amt">' . $context['open_mod_reports'] . '</span>';
4339
	}
4340
4341
	// Show how many errors there are
4342 View Code Duplication
	if (!empty($context['num_errors']) && allowedTo('admin_forum'))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4343
	{
4344
		$context['menu_buttons']['admin']['title'] .= ' <span class="amt">' . $context['num_errors'] . '</span>';
4345
		$context['menu_buttons']['admin']['sub_buttons']['errorlog']['title'] .= ' <span class="amt">' . $context['num_errors'] . '</span>';
4346
	}
4347
4348
	/**
4349
	 * @todo For some reason, $context['open_member_reports'] isn't getting set
4350
	 */
4351
	if (!empty($context['open_member_reports']) && allowedTo('moderate_forum'))
4352
	{
4353
		$total_mod_reports += $context['open_member_reports'];
4354
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['title'] .= ' <span class="amt">' . $context['open_member_reports'] . '</span>';
4355
	}
4356
4357 View Code Duplication
	if (!empty($context['unapproved_members']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4358
	{
4359
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['title'] .= ' <span class="amt">' . $context['unapproved_members'] . '</span>';
4360
		$context['menu_buttons']['admin']['title'] .= ' <span class="amt">' . $context['unapproved_members'] . '</span>';
4361
	}
4362
4363
	// Do we have any open reports?
4364
	if ($total_mod_reports > 0)
4365
	{
4366
		$context['menu_buttons']['moderate']['title'] .= ' <span class="amt">' . $total_mod_reports . '</span>';
4367
	}
4368
4369
	// Not all actions are simple.
4370
	if (!empty($needs_action_hook))
4371
		call_integration_hook('integrate_current_action', array(&$current_action));
4372
4373
	if (isset($context['menu_buttons'][$current_action]))
4374
		$context['menu_buttons'][$current_action]['active_button'] = true;
4375
}
4376
4377
/**
4378
 * Generate a random seed and ensure it's stored in settings.
4379
 */
4380
function smf_seed_generator()
4381
{
4382
	updateSettings(array('rand_seed' => microtime() * 1000000));
4383
}
4384
4385
/**
4386
 * Process functions of an integration hook.
4387
 * calls all functions of the given hook.
4388
 * supports static class method calls.
4389
 *
4390
 * @param string $hook The hook name
4391
 * @param array $parameters An array of parameters this hook implements
4392
 * @return array The results of the functions
4393
 */
4394
function call_integration_hook($hook, $parameters = array())
4395
{
4396
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
4397
	global $context, $txt;
4398
4399
	if ($db_show_debug === true)
4400
		$context['debug']['hooks'][] = $hook;
4401
4402
	// Need to have some control.
4403
	if (!isset($context['instances']))
4404
		$context['instances'] = array();
4405
4406
	$results = array();
4407
	if (empty($modSettings[$hook]))
4408
		return $results;
4409
4410
	// Define some needed vars.
4411
	$function = false;
4412
4413
	$functions = explode(',', $modSettings[$hook]);
4414
	// Loop through each function.
4415
	foreach ($functions as $function)
4416
	{
4417
		// Hook has been marked as "disabled". Skip it!
4418
		if (strpos($function, '!') !== false)
4419
			continue;
4420
4421
		$call = call_helper($function, true);
4422
4423
		// Is it valid?
4424
		if (!empty($call))
4425
			$results[$function] = call_user_func_array($call, $parameters);
4426
4427
		// Whatever it was suppose to call, it failed :(
4428
		elseif (!empty($function))
4429
		{
4430
			loadLanguage('Errors');
4431
4432
			// Get a full path to show on error.
4433
			if (strpos($function, '|') !== false)
4434
			{
4435
				list ($file, $string) = explode('|', $function);
4436
				$absPath = empty($settings['theme_dir']) ? (strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir))) : (strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir'])));
4437
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
4438
			}
4439
4440
			// "Assume" the file resides on $boarddir somewhere...
4441
			else
4442
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
4443
		}
4444
	}
4445
4446
	return $results;
4447
}
4448
4449
/**
4450
 * Add a function for integration hook.
4451
 * does nothing if the function is already added.
4452
 *
4453
 * @param string $hook The complete hook name.
4454
 * @param string $function The function name. Can be a call to a method via Class::method.
4455
 * @param bool $permanent If true, updates the value in settings table.
4456
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4457
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4458
 */
4459
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
4460
{
4461
	global $smcFunc, $modSettings;
4462
4463
	// Any objects?
4464
	if ($object)
4465
		$function = $function . '#';
4466
4467
	// Any files  to load?
4468
	if (!empty($file) && is_string($file))
4469
		$function = $file . (!empty($function) ? '|' . $function : '');
4470
4471
	// Get the correct string.
4472
	$integration_call = $function;
4473
4474
	// Is it going to be permanent?
4475
	if ($permanent)
4476
	{
4477
		$request = $smcFunc['db_query']('', '
4478
			SELECT value
4479
			FROM {db_prefix}settings
4480
			WHERE variable = {string:variable}',
4481
			array(
4482
				'variable' => $hook,
4483
			)
4484
		);
4485
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
4486
		$smcFunc['db_free_result']($request);
4487
4488
		if (!empty($current_functions))
4489
		{
4490
			$current_functions = explode(',', $current_functions);
4491
			if (in_array($integration_call, $current_functions))
4492
				return;
4493
4494
			$permanent_functions = array_merge($current_functions, array($integration_call));
4495
		}
4496
		else
4497
			$permanent_functions = array($integration_call);
4498
4499
		updateSettings(array($hook => implode(',', $permanent_functions)));
4500
	}
4501
4502
	// Make current function list usable.
4503
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4504
4505
	// Do nothing, if it's already there.
4506
	if (in_array($integration_call, $functions))
4507
		return;
4508
4509
	$functions[] = $integration_call;
4510
	$modSettings[$hook] = implode(',', $functions);
4511
}
4512
4513
/**
4514
 * Remove an integration hook function.
4515
 * Removes the given function from the given hook.
4516
 * Does nothing if the function is not available.
4517
 *
4518
 * @param string $hook The complete hook name.
4519
 * @param string $function The function name. Can be a call to a method via Class::method.
4520
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
4521
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4522
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4523
 * @see add_integration_function
4524
 */
4525
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
0 ignored issues
show
Unused Code introduced by
The parameter $permanent 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...
4526
{
4527
	global $smcFunc, $modSettings;
4528
4529
	// Any objects?
4530
	if ($object)
4531
		$function = $function . '#';
4532
4533
	// Any files  to load?
4534
	if (!empty($file) && is_string($file))
4535
		$function = $file . '|' . $function;
4536
4537
	// Get the correct string.
4538
	$integration_call = $function;
4539
4540
	// Get the permanent functions.
4541
	$request = $smcFunc['db_query']('', '
4542
		SELECT value
4543
		FROM {db_prefix}settings
4544
		WHERE variable = {string:variable}',
4545
		array(
4546
			'variable' => $hook,
4547
		)
4548
	);
4549
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
4550
	$smcFunc['db_free_result']($request);
4551
4552
	if (!empty($current_functions))
4553
	{
4554
		$current_functions = explode(',', $current_functions);
4555
4556
		if (in_array($integration_call, $current_functions))
4557
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
4558
	}
4559
4560
	// Turn the function list into something usable.
4561
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4562
4563
	// You can only remove it if it's available.
4564
	if (!in_array($integration_call, $functions))
4565
		return;
4566
4567
	$functions = array_diff($functions, array($integration_call));
4568
	$modSettings[$hook] = implode(',', $functions);
4569
}
4570
4571
/**
4572
 * Receives a string and tries to figure it out if its a method or a function.
4573
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
4574
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
4575
 * Prepare and returns a callable depending on the type of method/function found.
4576
 *
4577
 * @param mixed $string The string containing a function name or a static call. The function can also accept a closure, object or a callable array (object/class, valid_callable)
4578
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
4579
 * @return string|array|boolean Either a string or an array that contains a callable function name or an array with a class and method to call. Boolean false if the given string cannot produce a callable var.
4580
 */
4581
function call_helper($string, $return = false)
4582
{
4583
	global $context, $smcFunc, $txt, $db_show_debug;
4584
4585
	// Really?
4586
	if (empty($string))
4587
		return false;
4588
4589
	// An array? should be a "callable" array IE array(object/class, valid_callable).
4590
	// A closure? should be a callable one.
4591
	if (is_array($string) || $string instanceof Closure)
4592
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
4593
4594
	// No full objects, sorry! pass a method or a property instead!
4595
	if (is_object($string))
4596
		return false;
4597
4598
	// Stay vitaminized my friends...
4599
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
4600
4601
	// The soon to be populated var.
4602
	$func = false;
0 ignored issues
show
Unused Code introduced by
$func is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
4603
4604
	// Is there a file to load?
4605
	$string = load_file($string);
4606
4607
	// Loaded file failed
4608
	if (empty($string))
4609
		return false;
4610
4611
	// Found a method.
4612
	if (strpos($string, '::') !== false)
4613
	{
4614
		list ($class, $method) = explode('::', $string);
4615
4616
		// Check if a new object will be created.
4617
		if (strpos($method, '#') !== false)
4618
		{
4619
			// Need to remove the # thing.
4620
			$method = str_replace('#', '', $method);
4621
4622
			// Don't need to create a new instance for every method.
4623
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
4624
			{
4625
				$context['instances'][$class] = new $class;
4626
4627
				// Add another one to the list.
4628
				if ($db_show_debug === true)
4629
				{
4630
					if (!isset($context['debug']['instances']))
4631
						$context['debug']['instances'] = array();
4632
4633
					$context['debug']['instances'][$class] = $class;
4634
				}
4635
			}
4636
4637
			$func = array($context['instances'][$class], $method);
4638
		}
4639
4640
		// Right then. This is a call to a static method.
4641
		else
4642
			$func = array($class, $method);
4643
	}
4644
4645
	// Nope! just a plain regular function.
4646
	else
4647
		$func = $string;
4648
4649
	// Right, we got what we need, time to do some checks.
4650
	if (!is_callable($func, false, $callable_name))
4651
	{
4652
		loadLanguage('Errors');
4653
		log_error(sprintf($txt['subAction_fail'], $callable_name), 'general');
4654
4655
		// Gotta tell everybody.
4656
		return false;
4657
	}
4658
4659
	// Everything went better than expected.
4660
	else
4661
	{
4662
		// What are we gonna do about it?
4663
		if ($return)
4664
			return $func;
4665
4666
		// If this is a plain function, avoid the heat of calling call_user_func().
4667
		else
4668
		{
4669
			if (is_array($func))
4670
				call_user_func($func);
4671
4672
			else
4673
				$func();
4674
		}
4675
	}
4676
}
4677
4678
/**
4679
 * Receives a string and tries to figure it out if it contains info to load a file.
4680
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
4681
 * The string should be format as follows File.php|. You can use the following wildcards: $boarddir, $sourcedir and if available at the moment of execution, $themedir.
4682
 *
4683
 * @param string $string The string containing a valid format.
4684
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
4685
 */
4686
function load_file($string)
4687
{
4688
	global $sourcedir, $txt, $boarddir, $settings;
4689
4690
	if (empty($string))
4691
		return false;
4692
4693
	if (strpos($string, '|') !== false)
4694
	{
4695
		list ($file, $string) = explode('|', $string);
4696
4697
		// Match the wildcards to their regular vars.
4698
		if (empty($settings['theme_dir']))
4699
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
4700
4701
		else
4702
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
4703
4704
		// Load the file if it can be loaded.
4705
		if (file_exists($absPath))
4706
			require_once($absPath);
4707
4708
		// No? try a fallback to $sourcedir
4709
		else
4710
		{
4711
			$absPath = $sourcedir .'/'. $file;
4712
4713
			if (file_exists($absPath))
4714
				require_once($absPath);
4715
4716
			// Sorry, can't do much for you at this point.
4717
			else
4718
			{
4719
				loadLanguage('Errors');
4720
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
4721
4722
				// File couldn't be loaded.
4723
				return false;
4724
			}
4725
		}
4726
	}
4727
4728
	return $string;
4729
}
4730
4731
/**
4732
 * Prepares an array of "likes" info for the topic specified by $topic
4733
 * @param integer $topic The topic ID to fetch the info from.
4734
 * @return array An array of IDs of messages in the specified topic that the current user likes
4735
 */
4736
function prepareLikesContext($topic)
4737
{
4738
	global $user_info, $smcFunc;
4739
4740
	// Make sure we have something to work with.
4741
	if (empty($topic))
4742
		return array();
4743
4744
4745
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
4746
	$user = $user_info['id'];
4747
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
4748
	$ttl = 180;
4749
4750
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
4751
	{
4752
		$temp = array();
4753
		$request = $smcFunc['db_query']('', '
4754
			SELECT content_id
4755
			FROM {db_prefix}user_likes AS l
4756
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
4757
			WHERE l.id_member = {int:current_user}
4758
				AND l.content_type = {literal:msg}
4759
				AND m.id_topic = {int:topic}',
4760
			array(
4761
				'current_user' => $user,
4762
				'topic' => $topic,
4763
			)
4764
		);
4765
		while ($row = $smcFunc['db_fetch_assoc']($request))
4766
			$temp[] = (int) $row['content_id'];
4767
4768
		cache_put_data($cache_key, $temp, $ttl);
4769
	}
4770
4771
	return $temp;
4772
}
4773
4774
/**
4775
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
4776
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
4777
 * that are not normally displayable.  This converts the popular ones that
4778
 * appear from a cut and paste from windows.
4779
 *
4780
 * @param string $string The string
4781
 * @return string The sanitized string
4782
 */
4783
function sanitizeMSCutPaste($string)
4784
{
4785
	global $context;
4786
4787
	if (empty($string))
4788
		return $string;
4789
4790
	// UTF-8 occurences of MS special characters
4791
	$findchars_utf8 = array(
4792
		"\xe2\80\x9a",	// single low-9 quotation mark
4793
		"\xe2\80\x9e",	// double low-9 quotation mark
4794
		"\xe2\80\xa6",	// horizontal ellipsis
4795
		"\xe2\x80\x98",	// left single curly quote
4796
		"\xe2\x80\x99",	// right single curly quote
4797
		"\xe2\x80\x9c",	// left double curly quote
4798
		"\xe2\x80\x9d",	// right double curly quote
4799
		"\xe2\x80\x93",	// en dash
4800
		"\xe2\x80\x94",	// em dash
4801
	);
4802
4803
	// windows 1252 / iso equivalents
4804
	$findchars_iso = array(
4805
		chr(130),
4806
		chr(132),
4807
		chr(133),
4808
		chr(145),
4809
		chr(146),
4810
		chr(147),
4811
		chr(148),
4812
		chr(150),
4813
		chr(151),
4814
	);
4815
4816
	// safe replacements
4817
	$replacechars = array(
4818
		',',	// &sbquo;
4819
		',,',	// &bdquo;
4820
		'...',	// &hellip;
4821
		"'",	// &lsquo;
4822
		"'",	// &rsquo;
4823
		'"',	// &ldquo;
4824
		'"',	// &rdquo;
4825
		'-',	// &ndash;
4826
		'--',	// &mdash;
4827
	);
4828
4829
	if ($context['utf8'])
4830
		$string = str_replace($findchars_utf8, $replacechars, $string);
4831
	else
4832
		$string = str_replace($findchars_iso, $replacechars, $string);
4833
4834
	return $string;
4835
}
4836
4837
/**
4838
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
4839
 *
4840
 * Callback function for preg_replace_callback in subs-members
4841
 * Uses capture group 2 in the supplied array
4842
 * Does basic scan to ensure characters are inside a valid range
4843
 *
4844
 * @param array $matches An array of matches (relevant info should be the 3rd item)
4845
 * @return string A fixed string
4846
 */
4847
function replaceEntities__callback($matches)
4848
{
4849
	global $context;
4850
4851
	if (!isset($matches[2]))
4852
		return '';
4853
4854
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
4855
4856
	// remove left to right / right to left overrides
4857
	if ($num === 0x202D || $num === 0x202E)
4858
		return '';
4859
4860
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
4861
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
4862
		return '&#' . $num . ';';
4863
4864
	if (empty($context['utf8']))
4865
	{
4866
		// no control characters
4867
		if ($num < 0x20)
4868
			return '';
4869
		// text is text
4870
		elseif ($num < 0x80)
4871
			return chr($num);
4872
		// all others get html-ised
4873
		else
4874
			return '&#' . $matches[2] . ';';
4875
	}
4876
	else
4877
	{
4878
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
4879
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
4880
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
4881
			return '';
4882
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
4883
		elseif ($num < 0x80)
4884
			return chr($num);
4885
		// <0x800 (2048)
4886 View Code Duplication
		elseif ($num < 0x800)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4887
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
4888
		// < 0x10000 (65536)
4889 View Code Duplication
		elseif ($num < 0x10000)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4890
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4891
		// <= 0x10FFFF (1114111)
4892 View Code Duplication
		else
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4893
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4894
	}
4895
}
4896
4897
/**
4898
 * Converts html entities to utf8 equivalents
4899
 *
4900
 * Callback function for preg_replace_callback
4901
 * Uses capture group 1 in the supplied array
4902
 * Does basic checks to keep characters inside a viewable range.
4903
 *
4904
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
4905
 * @return string The fixed string
4906
 */
4907
function fixchar__callback($matches)
4908
{
4909
	if (!isset($matches[1]))
4910
		return '';
4911
4912
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
4913
4914
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
4915
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
4916
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
4917
		return '';
4918
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
4919
	elseif ($num < 0x80)
4920
		return chr($num);
4921
	// <0x800 (2048)
4922 View Code Duplication
	elseif ($num < 0x800)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4923
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
4924
	// < 0x10000 (65536)
4925 View Code Duplication
	elseif ($num < 0x10000)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4926
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4927
	// <= 0x10FFFF (1114111)
4928 View Code Duplication
	else
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4929
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4930
}
4931
4932
/**
4933
 * Strips out invalid html entities, replaces others with html style &#123; codes
4934
 *
4935
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
4936
 * strpos, strlen, substr etc
4937
 *
4938
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
4939
 * @return string The fixed string
4940
 */
4941
function entity_fix__callback($matches)
4942
{
4943
	if (!isset($matches[2]))
4944
		return '';
4945
4946
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
4947
4948
	// we don't allow control characters, characters out of range, byte markers, etc
4949
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
4950
		return '';
4951
	else
4952
		return '&#' . $num . ';';
4953
}
4954
4955
/**
4956
 * Return a Gravatar URL based on
4957
 * - the supplied email address,
4958
 * - the global maximum rating,
4959
 * - the global default fallback,
4960
 * - maximum sizes as set in the admin panel.
4961
 *
4962
 * It is SSL aware, and caches most of the parameters.
4963
 *
4964
 * @param string $email_address The user's email address
4965
 * @return string The gravatar URL
4966
 */
4967
function get_gravatar_url($email_address)
4968
{
4969
	global $modSettings, $smcFunc;
4970
	static $url_params = null;
4971
4972
	if ($url_params === null)
4973
	{
4974
		$ratings = array('G', 'PG', 'R', 'X');
4975
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
4976
		$url_params = array();
4977 View Code Duplication
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4978
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
4979 View Code Duplication
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
4980
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
4981
		if (!empty($modSettings['avatar_max_width_external']))
4982
			$size_string = (int) $modSettings['avatar_max_width_external'];
4983
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
4984
			if ((int) $modSettings['avatar_max_height_external'] < $size_string)
4985
				$size_string = $modSettings['avatar_max_height_external'];
4986
4987
		if (!empty($size_string))
4988
			$url_params[] = 's=' . $size_string;
4989
	}
4990
	$http_method = !empty($modSettings['force_ssl']) && $modSettings['force_ssl'] == 2 ? 'https://secure' : 'http://www';
4991
4992
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
4993
}
4994
4995
/**
4996
 * Get a list of timezoned.
4997
 *
4998
 * @return array An array of timezone info.
4999
 */
5000
function smf_list_timezones()
5001
{
5002
	return array(
5003
		'' => '(Forum Default)',
5004
		'UTC' => '[UTC] UTC',
5005
		'Pacific/Midway' => '[UTC-11:00] Midway Island, Samoa',
5006
		'America/Adak' => '[UTC-10:00] Hawaii-Aleutian',
5007
		'Pacific/Honolulu' => '[UTC-10:00] Hawaii',
5008
		'Pacific/Marquesas' => '[UTC-09:30] Marquesas Islands',
5009
		'Pacific/Gambier' => '[UTC-09:00] Gambier Islands',
5010
		'America/Anchorage' => '[UTC-09:00] Alaska',
5011
		'America/Ensenada' => '[UTC-08:00] Tijuana, Baja California',
5012
		'Pacific/Pitcairn' => '[UTC-08:00] Pitcairn Islands',
5013
		'America/Los_Angeles' => '[UTC-08:00] Pacific Time (USA, Canada)',
5014
		'America/Denver' => '[UTC-07:00] Mountain Time (USA, Canada)',
5015
		'America/Phoenix' => '[UTC-07:00] Arizona',
5016
		'America/Chihuahua' => '[UTC-07:00] Chihuahua, Mazatlan',
5017
		'America/Belize' => '[UTC-06:00] Saskatchewan, Central America',
5018
		'America/Cancun' => '[UTC-06:00] Guadalajara, Mexico City, Monterrey',
5019
		'Chile/EasterIsland' => '[UTC-06:00] Easter Island',
5020
		'America/Chicago' => '[UTC-06:00] Central Time (USA, Canada)',
5021
		'America/New_York' => '[UTC-05:00] Eastern Time (USA, Canada)',
5022
		'America/Havana' => '[UTC-05:00] Cuba',
5023
		'America/Bogota' => '[UTC-05:00] Bogota, Lima, Quito',
5024
		'America/Caracas' => '[UTC-04:30] Caracas',
5025
		'America/Santiago' => '[UTC-04:00] Santiago',
5026
		'America/La_Paz' => '[UTC-04:00] La Paz, San Juan, Manaus',
5027
		'Atlantic/Stanley' => '[UTC-04:00] Falkland Islands',
5028
		'America/Cuiaba' => '[UTC-04:00] Cuiaba',
5029
		'America/Goose_Bay' => '[UTC-04:00] Atlantic Time (Goose Bay)',
5030
		'America/Glace_Bay' => '[UTC-04:00] Atlantic Time (Canada)',
5031
		'America/St_Johns' => '[UTC-03:30] Newfoundland',
5032
		'America/Araguaina' => '[UTC-03:00] Araguaina',
5033
		'America/Montevideo' => '[UTC-03:00] Montevideo',
5034
		'America/Miquelon' => '[UTC-03:00] Saint Pierre and Miquelon',
5035
		'America/Argentina/Buenos_Aires' => '[UTC-03:00] Buenos Aires',
5036
		'America/Sao_Paulo' => '[UTC-03:00] Brasilia',
5037
		'America/Godthab' => '[UTC-02:00] Greenland',
5038
		'America/Noronha' => '[UTC-02:00] Fernando de Noronha',
5039
		'Atlantic/Cape_Verde' => '[UTC-01:00] Cape Verde',
5040
		'Atlantic/Azores' => '[UTC-01:00] Azores',
5041
		'Africa/Abidjan' => '[UTC] Monrovia, Reykjavik',
5042
		'Europe/London' => '[UTC] London, Edinburgh, Dublin, Lisbon (Greenwich Mean Time)',
5043
		'Europe/Brussels' => '[UTC+01:00] Central European Time',
5044
		'Africa/Algiers' => '[UTC+01:00] West Central Africa',
5045
		'Africa/Windhoek' => '[UTC+01:00] Windhoek',
5046
		'Africa/Cairo' => '[UTC+02:00] Cairo',
5047
		'Africa/Blantyre' => '[UTC+02:00] Harare, Maputo, Pretoria',
5048
		'Asia/Jerusalem' => '[UTC+02:00] Jerusalem',
5049
		'Europe/Minsk' => '[UTC+02:00] Minsk',
5050
		'Asia/Damascus' => '[UTC+02:00] Damascus, Nicosia, Gaza, Beirut',
5051
		'Africa/Addis_Ababa' => '[UTC+03:00] Addis Ababa, Nairobi',
5052
		'Asia/Tehran' => '[UTC+03:30] Tehran',
5053
		'Europe/Moscow' => '[UTC+04:00] Moscow, St. Petersburg, Volgograd',
5054
		'Asia/Dubai' => '[UTC+04:00] Abu Dhabi, Muscat',
5055
		'Asia/Baku' => '[UTC+04:00] Baku',
5056
		'Asia/Yerevan' => '[UTC+04:00] Yerevan',
5057
		'Asia/Kabul' => '[UTC+04:30] Kabul',
5058
		'Asia/Tashkent' => '[UTC+05:00] Tashkent',
5059
		'Asia/Kolkata' => '[UTC+05:30] Chennai, Kolkata, Mumbai, New Delhi',
5060
		'Asia/Katmandu' => '[UTC+05:45] Kathmandu',
5061
		'Asia/Yekaterinburg' => '[UTC+06:00] Yekaterinburg, Tyumen',
5062
		'Asia/Dhaka' => '[UTC+06:00] Astana, Thimphu, Dhaka',
5063
		'Asia/Novosibirsk' => '[UTC+06:00] Omsk, Novosibirsk',
5064
		'Asia/Rangoon' => '[UTC+06:30] Yangon Rangoon',
5065
		'Asia/Bangkok' => '[UTC+07:00] Bangkok, Hanoi, Jakarta',
5066
		'Asia/Krasnoyarsk' => '[UTC+08:00] Krasnoyarsk',
5067
		'Asia/Hong_Kong' => '[UTC+08:00] Beijing, Chongqing, Hong Kong, Urumqi',
5068
		'Asia/Ulaanbaatar' => '[UTC+08:00] Ulaan Bataar',
5069
		'Asia/Irkutsk' => '[UTC+09:00] Irkutsk',
5070
		'Australia/Perth' => '[UTC+08:00] Perth',
5071
		'Australia/Eucla' => '[UTC+08:45] Eucla',
5072
		'Asia/Tokyo' => '[UTC+09:00] Tokyo, Osaka, Sapporo',
5073
		'Asia/Seoul' => '[UTC+09:00] Seoul',
5074
		'Australia/Adelaide' => '[UTC+09:30] Adelaide',
5075
		'Australia/Darwin' => '[UTC+09:30] Darwin',
5076
		'Australia/Brisbane' => '[UTC+10:00] Brisbane, Guam',
5077
		'Australia/Sydney' => '[UTC+10:00] Sydney, Hobart',
5078
		'Asia/Yakutsk' => '[UTC+10:00] Yakutsk',
5079
		'Australia/Lord_Howe' => '[UTC+10:30] Lord Howe Island',
5080
		'Asia/Vladivostok' => '[UTC+11:00] Vladivostok',
5081
		'Pacific/Noumea' => '[UTC+11:00] Solomon Islands, New Caledonia',
5082
		'Pacific/Norfolk' => '[UTC+11:30] Norfolk Island',
5083
		'Pacific/Auckland' => '[UTC+12:00] Auckland, Wellington',
5084
		'Asia/Magadan' => '[UTC+12:00] Magadan, Kamchatka, Anadyr',
5085
		'Pacific/Fiji' => '[UTC+12:00] Fiji',
5086
		'Pacific/Majuro' => '[UTC+12:00] Marshall Islands',
5087
		'Pacific/Chatham' => '[UTC+12:45] Chatham Islands',
5088
		'Pacific/Tongatapu' => '[UTC+13:00] Nuku\'alofa',
5089
		'Pacific/Kiritimati' => '[UTC+14:00] Kiritimati',
5090
	);
5091
}
5092
5093
/**
5094
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
5095
 * @return binary The IP address in binary or false
5096
 */
5097
function inet_ptod($ip_address)
5098
{
5099
	if (!isValidIP($ip_address))
5100
		return $ip_address;
5101
5102
	$bin = inet_pton($ip_address);
5103
	return $bin;
5104
}
5105
5106
/**
5107
 * @param binary $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
5108
 * @return string The IP address in presentation format or false on error
5109
 */
5110
function inet_dtop($bin)
5111
{
5112
	if(empty($bin))
5113
		return '';
5114
5115
	global $db_type;
5116
5117
	if ($db_type == 'postgresql')
5118
		return $bin;
5119
5120
	$ip_address = inet_ntop($bin);
5121
5122
	return $ip_address;
5123
}
5124
5125
/**
5126
 * Safe serialize() and unserialize() replacements
5127
 *
5128
 * @license Public Domain
5129
 *
5130
 * @author anthon (dot) pang (at) gmail (dot) com
5131
 */
5132
5133
/**
5134
 * Safe serialize() replacement. Recursive
5135
 * - output a strict subset of PHP's native serialized representation
5136
 * - does not serialize objects
5137
 *
5138
 * @param mixed $value
5139
 * @return string
5140
 */
5141
function _safe_serialize($value)
5142
{
5143
	if(is_null($value))
5144
		return 'N;';
5145
5146
	if(is_bool($value))
5147
		return 'b:'. (int) $value .';';
5148
5149
	if(is_int($value))
5150
		return 'i:'. $value .';';
5151
5152
	if(is_float($value))
5153
		return 'd:'. str_replace(',', '.', $value) .';';
5154
5155
	if(is_string($value))
5156
		return 's:'. strlen($value) .':"'. $value .'";';
5157
5158
	if(is_array($value))
5159
	{
5160
		$out = '';
5161
		foreach($value as $k => $v)
5162
			$out .= _safe_serialize($k) . _safe_serialize($v);
5163
5164
		return 'a:'. count($value) .':{'. $out .'}';
5165
	}
5166
5167
	// safe_serialize cannot serialize resources or objects.
5168
	return false;
5169
}
5170
/**
5171
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
5172
 *
5173
 * @param mixed $value
5174
 * @return string
5175
 */
5176 View Code Duplication
function safe_serialize($value)
0 ignored issues
show
Duplication introduced by
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
5177
{
5178
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5179
	if (function_exists('mb_internal_encoding') &&
5180
		(((int) ini_get('mbstring.func_overload')) & 2))
5181
	{
5182
		$mbIntEnc = mb_internal_encoding();
5183
		mb_internal_encoding('ASCII');
5184
	}
5185
5186
	$out = _safe_serialize($value);
5187
5188
	if (isset($mbIntEnc))
5189
		mb_internal_encoding($mbIntEnc);
5190
5191
	return $out;
5192
}
5193
5194
/**
5195
 * Safe unserialize() replacement
5196
 * - accepts a strict subset of PHP's native serialized representation
5197
 * - does not unserialize objects
5198
 *
5199
 * @param string $str
5200
 * @return mixed
5201
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
5202
 */
5203
function _safe_unserialize($str)
5204
{
5205
	// Input  is not a string.
5206
	if(empty($str) || !is_string($str))
5207
		return false;
5208
5209
	$stack = array();
5210
	$expected = array();
5211
5212
	/*
5213
	 * states:
5214
	 *   0 - initial state, expecting a single value or array
5215
	 *   1 - terminal state
5216
	 *   2 - in array, expecting end of array or a key
5217
	 *   3 - in array, expecting value or another array
5218
	 */
5219
	$state = 0;
5220
	while($state != 1)
5221
	{
5222
		$type = isset($str[0]) ? $str[0] : '';
5223
		if($type == '}')
5224
			$str = substr($str, 1);
5225
5226
		else if($type == 'N' && $str[1] == ';')
5227
		{
5228
			$value = null;
5229
			$str = substr($str, 2);
5230
		}
5231
		else if($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
5232
		{
5233
			$value = $matches[1] == '1' ? true : false;
5234
			$str = substr($str, 4);
5235
		}
5236
		else if($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
5237
		{
5238
			$value = (int)$matches[1];
5239
			$str = $matches[2];
5240
		}
5241
		else if($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
5242
		{
5243
			$value = (float)$matches[1];
5244
			$str = $matches[3];
5245
		}
5246
		else if($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int)$matches[1], 2) == '";')
5247
		{
5248
			$value = substr($matches[2], 0, (int)$matches[1]);
5249
			$str = substr($matches[2], (int)$matches[1] + 2);
5250
		}
5251
		else if($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
5252
		{
5253
			$expectedLength = (int)$matches[1];
5254
			$str = $matches[2];
5255
		}
5256
5257
		// Object or unknown/malformed type.
5258
		else
5259
			return false;
5260
5261
		switch($state)
5262
		{
5263
			case 3: // In array, expecting value or another array.
5264
				if($type == 'a')
5265
				{
5266
					$stack[] = &$list;
0 ignored issues
show
Bug introduced by
The variable $list does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
5267
					$list[$key] = array();
0 ignored issues
show
Bug introduced by
The variable $key does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
5268
					$list = &$list[$key];
5269
					$expected[] = $expectedLength;
0 ignored issues
show
Bug introduced by
The variable $expectedLength does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
5270
					$state = 2;
5271
					break;
5272
				}
5273
				if($type != '}')
5274
				{
5275
					$list[$key] = $value;
0 ignored issues
show
Bug introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
5276
					$state = 2;
5277
					break;
5278
				}
5279
5280
				// Missing array value.
5281
				return false;
5282
5283
			case 2: // in array, expecting end of array or a key
5284
				if($type == '}')
5285
				{
5286
					// Array size is less than expected.
5287
					if(count($list) < end($expected))
5288
						return false;
5289
5290
					unset($list);
5291
					$list = &$stack[count($stack)-1];
5292
					array_pop($stack);
5293
5294
					// Go to terminal state if we're at the end of the root array.
5295
					array_pop($expected);
5296
5297
					if(count($expected) == 0)
5298
						$state = 1;
5299
5300
					break;
5301
				}
5302
5303
				if($type == 'i' || $type == 's')
5304
				{
5305
					// Array size exceeds expected length.
5306
					if(count($list) >= end($expected))
5307
						return false;
5308
5309
					$key = $value;
5310
					$state = 3;
5311
					break;
5312
				}
5313
5314
				// Illegal array index type.
5315
				return false;
5316
5317
			// Expecting array or value.
5318
			case 0:
5319
				if($type == 'a')
5320
				{
5321
					$data = array();
5322
					$list = &$data;
5323
					$expected[] = $expectedLength;
5324
					$state = 2;
5325
					break;
5326
				}
5327
5328
				if($type != '}')
5329
				{
5330
					$data = $value;
5331
					$state = 1;
5332
					break;
5333
				}
5334
5335
				// Not in array.
5336
				return false;
5337
		}
5338
	}
5339
5340
	// Trailing data in input.
5341
	if(!empty($str))
5342
		return false;
5343
5344
	return $data;
0 ignored issues
show
Bug introduced by
The variable $data does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
5345
}
5346
5347
/**
5348
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
5349
 *
5350
 * @param string $str
5351
 * @return mixed
5352
 */
5353 View Code Duplication
function safe_unserialize($str)
0 ignored issues
show
Duplication introduced by
This function seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
5354
{
5355
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5356
	if (function_exists('mb_internal_encoding') &&
5357
		(((int) ini_get('mbstring.func_overload')) & 0x02))
5358
	{
5359
		$mbIntEnc = mb_internal_encoding();
5360
		mb_internal_encoding('ASCII');
5361
	}
5362
5363
	$out = _safe_unserialize($str);
5364
5365
	if (isset($mbIntEnc))
5366
		mb_internal_encoding($mbIntEnc);
5367
5368
	return $out;
5369
}
5370
5371
/**
5372
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
5373
5374
 * @param string $file The file/dir full path.
5375
 * @param int $value Not needed, added for legacy reasons.
5376
 * @return boolean  true if the file/dir is already writable or the function was able to make it writable, false if the function couldn't make the file/dir writable.
5377
 */
5378
function smf_chmod($file, $value = 0)
0 ignored issues
show
Unused Code introduced by
The parameter $value 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...
5379
{
5380
	// No file? no checks!
5381
	if (empty($file))
5382
		return false;
5383
5384
	// Already writable?
5385
	if (is_writable($file))
5386
		return true;
5387
5388
	// Do we have a file or a dir?
5389
	$isDir = is_dir($file);
5390
	$isWritable = false;
5391
5392
	// Set different modes.
5393
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
5394
5395
	foreach($chmodValues as $val)
5396
	{
5397
		// If it's writable, break out of the loop.
5398
		if (is_writable($file))
5399
		{
5400
			$isWritable = true;
5401
			break;
5402
		}
5403
5404
		else
5405
			@chmod($file, $val);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
5406
	}
5407
5408
	return $isWritable;
5409
}
5410
5411
/**
5412
 * Wrapper function for json_decode() with error handling.
5413
5414
 * @param string $json The string to decode.
5415
 * @param bool $returnAsArray To return the decoded string as an array or an object, SMF only uses Arrays but to keep on compatibility with json_decode its set to false as default.
5416
 * @param bool $logIt To specify if the error will be logged if theres any.
5417
 * @return array Either an empty array or the decoded data as an array.
5418
 */
5419
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
5420
{
5421
	global $txt;
5422
5423
	// Come on...
5424
	if (empty($json) || !is_string($json))
5425
		return array();
5426
5427
	$returnArray = array();
0 ignored issues
show
Unused Code introduced by
$returnArray is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
5428
	$jsonError = false;
0 ignored issues
show
Unused Code introduced by
$jsonError is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
5429
5430
	$returnArray = @json_decode($json, $returnAsArray);
5431
5432
	// PHP 5.3 so no json_last_error_msg()
5433
	switch(json_last_error())
5434
	{
5435
		case JSON_ERROR_NONE:
5436
			$jsonError = false;
5437
			break;
5438
		case JSON_ERROR_DEPTH:
5439
			$jsonError =  'JSON_ERROR_DEPTH';
5440
			break;
5441
		case JSON_ERROR_STATE_MISMATCH:
5442
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
5443
			break;
5444
		case JSON_ERROR_CTRL_CHAR:
5445
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
5446
			break;
5447
		case JSON_ERROR_SYNTAX:
5448
			$jsonError = 'JSON_ERROR_SYNTAX';
5449
			break;
5450
		case JSON_ERROR_UTF8:
5451
			$jsonError = 'JSON_ERROR_UTF8';
5452
			break;
5453
		default:
5454
			$jsonError = 'unknown';
5455
			break;
5456
	}
5457
5458
	// Something went wrong!
5459
	if (!empty($jsonError) && $logIt)
5460
	{
5461
		// Being a wrapper means we lost our smf_error_handler() privileges :(
5462
		$jsonDebug = debug_backtrace();
5463
		$jsonDebug = $jsonDebug[0];
5464
		loadLanguage('Errors');
5465
5466
		if (!empty($jsonDebug))
5467
			log_error($txt['json_'. $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
5468
5469
		else
5470
			log_error($txt['json_'. $jsonError], 'critical');
5471
5472
		// Everyone expects an array.
5473
		return array();
5474
	}
5475
5476
	return $returnArray;
5477
}
5478
5479
/**
5480
 * Check the given String if he is a valid IPv4 or IPv6
5481
 * return true or false
5482
 */
5483
function isValidIP($IPString)
5484
{
5485
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
5486
}
5487
5488
/**
5489
 * Outputs a response.
5490
 * It assumes the data is already a string.
5491
 * @param string $data The data to print
5492
 * @param string $type The content type. Defaults to Json.
5493
 * @return void
5494
 */
5495
function smf_serverResponse($data = '', $type = 'Content-Type: application/json')
5496
{
5497
	global $db_show_debug, $modSettings;
5498
5499
	// Defensive programming anyone?
5500
	if (empty($data))
5501
		return false;
5502
5503
	// Don't need extra stuff...
5504
	$db_show_debug = false;
5505
5506
	// Kill anything else.
5507
	ob_end_clean();
5508
5509
	if (!empty($modSettings['CompressedOutput']))
5510
		@ob_start('ob_gzhandler');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
5511
5512
	else
5513
		ob_start();
5514
5515
	// Set the header.
5516
	header($type);
5517
5518
	// Echo!
5519
	echo $data;
5520
5521
	// Done.
5522
	obExit(false);
5523
}
5524
5525
/**
5526
 * Creates an optimized regex to match all known top level domains.
5527
 *
5528
 * The optimized regex is stored in $modSettings['tld_regex'].
5529
 *
5530
 * To update the stored version of the regex to use the latest list of valid TLDs from iana.org, set
5531
 * the $update parameter to true. Updating can take some time, based on network connectivity, so it
5532
 * should normally only be done by calling this function from a background or scheduled task.
5533
 *
5534
 * If $update is not true, but the regex is missing or invalid, the regex will be regenerated from a
5535
 * hard-coded list of TLDs. This regenerated regex will be overwritten on the next scheduled update.
5536
 *
5537
 * @param bool $update If true, fetch and process the latest offical list of TLDs from iana.org.
5538
 */
5539
function set_tld_regex($update = false)
5540
{
5541
	global $sourcedir, $smcFunc;
5542
	static $done = false;
5543
5544
	// If we don't need to do anything, don't
5545
	if (!$update && $done)
5546
		return;
5547
5548
	// Should we get a new copy of the official list of TLDs?
5549
	if ($update)
5550
	{
5551
		require_once($sourcedir . '/Subs-Package.php');
5552
		$tlds = fetch_web_data('http://data.iana.org/TLD/tlds-alpha-by-domain.txt');
5553
	}
5554
	// If we aren't updating and the regex is valid, we're done
5555
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
0 ignored issues
show
Bug introduced by
The variable $modSettings seems to never exist, and therefore empty should always return true. Did you maybe rename this variable?

This check looks for calls to isset(...) or empty() on variables that are yet undefined. These calls will always produce the same result and can be removed.

This is most likely caused by the renaming of a variable or the removal of a function/method parameter.

Loading history...
5556
	{
5557
		$done = true;
5558
		return;
5559
	}
5560
5561
	// If we successfully got an update, process the list into an array
5562
	if (!empty($tlds))
5563
	{
5564
		// Clean $tlds and convert it to an array
5565
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line) {
5566
			$line = trim($line);
5567
			if (empty($line) || strpos($line, '#') !== false || strpos($line, ' ') !== false)
5568
				return false;
5569
			else
5570
				return true;
5571
		});
5572
5573
		// Convert Punycode to Unicode
5574
		$tlds = array_map(function ($input) {
5575
			$prefix = 'xn--';
5576
			$safe_char = 0xFFFC;
5577
			$base = 36;
5578
			$tmin = 1;
5579
			$tmax = 26;
5580
			$skew = 38;
5581
			$damp = 700;
5582
			$output_parts = array();
5583
5584
			$input = str_replace(strtoupper($prefix), $prefix, $input);
5585
5586
			$enco_parts = (array) explode('.', $input);
5587
5588
			foreach ($enco_parts as $encoded)
5589
			{
5590
				if (strpos($encoded,$prefix) !== 0 || strlen(trim(str_replace($prefix,'',$encoded))) == 0)
5591
				{
5592
					$output_parts[] = $encoded;
5593
					continue;
5594
				}
5595
5596
				$is_first = true;
5597
				$bias = 72;
5598
				$idx = 0;
5599
				$char = 0x80;
5600
				$decoded = array();
5601
				$output='';
5602
				$delim_pos = strrpos($encoded, '-');
5603
5604
				if ($delim_pos > strlen($prefix))
5605
				{
5606
					for ($k = strlen($prefix); $k < $delim_pos; ++$k)
5607
					{
5608
						$decoded[] = ord($encoded{$k});
5609
					}
5610
				}
5611
5612
				$deco_len = count($decoded);
5613
				$enco_len = strlen($encoded);
5614
5615
				for ($enco_idx = $delim_pos ? ($delim_pos + 1) : 0; $enco_idx < $enco_len; ++$deco_len)
5616
				{
5617
					for ($old_idx = $idx, $w = 1, $k = $base; 1 ; $k += $base)
5618
					{
5619
						$cp = ord($encoded{$enco_idx++});
5620
						$digit = ($cp - 48 < 10) ? $cp - 22 : (($cp - 65 < 26) ? $cp - 65 : (($cp - 97 < 26) ? $cp - 97 : $base));
5621
						$idx += $digit * $w;
5622
						$t = ($k <= $bias) ? $tmin : (($k >= $bias + $tmax) ? $tmax : ($k - $bias));
5623
5624
						if ($digit < $t)
5625
							break;
5626
5627
						$w = (int) ($w * ($base - $t));
5628
					}
5629
5630
					$delta = $idx - $old_idx;
5631
					$delta = intval($is_first ? ($delta / $damp) : ($delta / 2));
5632
					$delta += intval($delta / ($deco_len + 1));
5633
5634
					for ($k = 0; $delta > (($base - $tmin) * $tmax) / 2; $k += $base)
5635
						$delta = intval($delta / ($base - $tmin));
5636
5637
					$bias = intval($k + ($base - $tmin + 1) * $delta / ($delta + $skew));
5638
					$is_first = false;
5639
					$char += (int) ($idx / ($deco_len + 1));
5640
					$idx %= ($deco_len + 1);
5641
5642
					if ($deco_len > 0)
5643
					{
5644
						for ($i = $deco_len; $i > $idx; $i--)
5645
							$decoded[$i] = $decoded[($i - 1)];
5646
					}
5647
					$decoded[$idx++] = $char;
5648
				}
5649
5650
				foreach ($decoded as $k => $v)
5651
				{
5652
					// 7bit are transferred literally
5653
					if ($v < 128)
5654
						$output .= chr($v);
5655
5656
					// 2 bytes
5657
					elseif ($v < (1 << 11))
5658
						$output .= chr(192+($v >> 6)) . chr(128+($v & 63));
5659
5660
					// 3 bytes
5661
					elseif ($v < (1 << 16))
5662
						$output .= chr(224+($v >> 12)) . chr(128+(($v >> 6) & 63)) . chr(128+($v & 63));
5663
5664
					// 4 bytes
5665
					elseif ($v < (1 << 21))
5666
						$output .= chr(240+($v >> 18)) . chr(128+(($v >> 12) & 63)) . chr(128+(($v >> 6) & 63)) . chr(128+($v & 63));
5667
5668
					//  'Conversion from UCS-4 to UTF-8 failed: malformed input at byte '.$k
5669
					else
5670
						$output .= $safe_char;
5671
				}
5672
5673
				$output_parts[] = $output;
5674
			}
5675
5676
			return implode('.', $output_parts);
5677
		}, $tlds);
5678
5679
		$schedule_update = false;
5680
	}
5681
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
5682
	else
5683
	{
5684
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz', 'cat',
5685
			'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post', 'pro', 'tel',
5686
			'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq',
5687
			'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh',
5688
			'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc',
5689
			'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv',
5690
			'cx', 'cy', 'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', 'eh',
5691
			'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge',
5692
			'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw',
5693
			'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq',
5694
			'ir', 'is', 'it', 'ja', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn',
5695
			'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu',
5696
			'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp',
5697
			'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf',
5698
			'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph',
5699
			'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru',
5700
			'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
5701
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th',
5702
			'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug',
5703
			'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye',
5704
			'yt', 'yu', 'za', 'zm', 'zw');
5705
5706
		$schedule_update = true;
5707
	}
5708
5709
	// build_regex() returns an array. We only need the first item.
5710
	$tld_regex = array_shift(build_regex($tlds));
0 ignored issues
show
Bug introduced by
build_regex($tlds) cannot be passed to array_shift() as the parameter $array expects a reference.
Loading history...
5711
5712
	// Remember the new regex in $modSettings
5713
	updateSettings(array('tld_regex' => $tld_regex));
5714
5715
	// Schedule a background update if we need one
5716
	if (!empty($schedule_update))
5717
	{
5718
		$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
5719
			array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
5720
			array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
5721
		);
5722
	}
5723
5724
	// Redundant repetition is redundant
5725
	$done = true;
5726
}
5727
5728
/**
5729
 * Creates optimized regular expressions from an array of strings.
5730
 *
5731
 * An optimized regex built using this function will be much faster than a simple regex built using
5732
 * `implode('|', $strings)` --- anywhere from several times to several orders of magnitude faster.
5733
 *
5734
 * However, the time required to build the optimized regex is approximately equal to the time it
5735
 * takes to execute the simple regex. Therefore, it is only worth calling this function if the
5736
 * resulting regex will be used more than once.
5737
 *
5738
 * Because PHP places an upper limit on the allowed length of a regex, very large arrays may be
5739
 * split and returned as multiple regexes. In such cases, you will need to iterate through all
5740
 * elements of the returned array in order to test all possible matches. (Note: if your array of
5741
 * alternative strings is large enough to require multiple regexes to accomodate it all, it is
5742
 * probably time to reconsider your coding choices. There is almost certainly a better way to do
5743
 * whatever you are trying to do with these giant regexes.)
5744
 *
5745
 * @param array $strings An array of strings to make a regex for.
5746
 * @param string $delim An optional delimiter character to pass to preg_quote().
5747
 * @return array An array of one or more regular expressions to match any of the input strings.
5748
 */
5749
function build_regex($strings, $delim = null)
5750
{
5751
	global $smcFunc;
5752
5753
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
5754
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
5755
	{
5756
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
5757
		{
5758
			$current_encoding = mb_internal_encoding();
5759
			mb_internal_encoding($string_encoding);
5760
		}
5761
5762
		$strlen = 'mb_strlen';
5763
		$substr = 'mb_substr';
5764
	}
5765
	else
5766
	{
5767
		$strlen = $smcFunc['strlen'];
5768
		$substr = $smcFunc['substr'];
5769
	}
5770
5771
	// This recursive function creates the index array from the strings
5772
	$add_string_to_index = function ($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
5773
	{
5774
		static $depth = 0;
5775
		$depth++;
5776
5777
		$first = $substr($string, 0, 1);
5778
5779
		if (empty($index[$first]))
5780
			$index[$first] = array();
5781
5782
		if ($strlen($string) > 1)
5783
		{
5784
			// Sanity check on recursion
5785
			if ($depth > 99)
5786
				$index[$first][$substr($string, 1)] = '';
5787
5788
			else
5789
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
5790
		}
5791
		else
5792
			$index[$first][''] = '';
5793
5794
		$depth--;
5795
		return $index;
5796
	};
5797
5798
	// This recursive function turns the index array into a regular expression
5799
	$index_to_regex = function (&$index, $delim) use (&$strlen, &$index_to_regex)
5800
	{
5801
		static $depth = 0;
5802
		$depth++;
5803
5804
		// Absolute max length for a regex is 32768, but we might need wiggle room
5805
		$max_length = 30000;
5806
5807
		$regex = array();
5808
		$length = 0;
5809
5810
		foreach ($index as $key => $value)
5811
		{
5812
			$key_regex = preg_quote($key, $delim);
5813
			$new_key = $key;
5814
5815
			if (empty($value))
5816
				$sub_regex = '';
5817
			else
5818
			{
5819
				$sub_regex = $index_to_regex($value, $delim);
5820
5821
				if (count(array_keys($value)) == 1)
5822
					$new_key .= explode('(?'.'>', $sub_regex)[0];
5823
				else
5824
					$sub_regex = '(?'.'>' . $sub_regex . ')';
5825
			}
5826
5827
			if ($depth > 1)
5828
				$regex[$new_key] = $key_regex . $sub_regex;
5829
			else
5830
			{
5831
				if (($length += strlen($key_regex) + 1) < $max_length || empty($regex))
5832
				{
5833
					$regex[$new_key] = $key_regex . $sub_regex;
5834
					unset($index[$key]);
5835
				}
5836
				else
5837
					break;
5838
			}
5839
		}
5840
5841
		// Sort by key length and then alphabetically
5842
		uksort($regex, function($k1, $k2) use (&$strlen) {
5843
			$l1 = $strlen($k1);
5844
			$l2 = $strlen($k2);
5845
5846
			if ($l1 == $l2)
5847
				return strcmp($k1, $k2) > 0 ? 1 : -1;
5848
			else
5849
				return $l1 > $l2 ? -1 : 1;
5850
		});
5851
5852
		$depth--;
5853
		return implode('|', $regex);
5854
	};
5855
5856
	// Now that the functions are defined, let's do this thing
5857
	$index = array();
5858
	$regexes = array();
5859
5860
	foreach ($strings as $string)
5861
		$index = $add_string_to_index($string, $index);
5862
5863
	while (!empty($index))
5864
		$regexes[] = '(?'.'>' . $index_to_regex($index, $delim) . ')';
5865
5866
	// Restore PHP's internal character encoding to whatever it was originally
5867
	if (!empty($current_encoding))
5868
		mb_internal_encoding($current_encoding);
5869
5870
	return $regexes;
5871
}
5872
5873
?>
0 ignored issues
show
Best Practice introduced by
It is not recommended to use PHP's closing tag ?> in files other than templates.

Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore.

A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.

Loading history...