Completed
Push — release-2.1 ( 001348...ca7b71 )
by Mathias
18:55
created

Subs.php ➔ fix_tz_abbrev()   B

Complexity

Conditions 7
Paths 13

Size

Total Lines 55
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 37
nc 13
nop 2
dl 0
loc 55
rs 7.8235
c 0
b 0
f 0

How to fix   Long Method   

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 2017 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 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...
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)
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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)
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 = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1123
						$title = !empty($params['{title}']) ? ' title="' . $params['{title}'] . '"' : '';
1124
1125
						$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1126
						$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1127
1128
						if (empty($width) && empty($height))
1129
						{
1130
							$width = ' width="' . $currentAttachment['width'] . '"';
1131
							$height = ' height="' . $currentAttachment['height'] . '"';
1132
						}
1133
1134
						if ($currentAttachment['thumbnail']['has_thumb'] && empty($params['{width}']) && empty($params['{height}']))
1135
							$returnContext .= '<a href="'. $currentAttachment['href']. ';image" id="link_'. $currentAttachment['id']. '" onclick="'. $currentAttachment['thumbnail']['javascript']. '"><img src="'. $currentAttachment['thumbnail']['href']. '"' . $alt . $title . ' id="thumb_'. $currentAttachment['id']. '" class="atc_img"></a>';
1136
						else
1137
							$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img"/>';
1138
					}
1139
1140
					// No image. Show a link.
1141
					else
1142
						$returnContext .= $currentAttachment['link'];
1143
1144
					// Gotta append what we just did.
1145
					$data = $returnContext;
1146
				},
1147
			),
1148
			array(
1149
				'tag' => 'b',
1150
				'before' => '<b>',
1151
				'after' => '</b>',
1152
			),
1153
			array(
1154
				'tag' => 'center',
1155
				'before' => '<div class="centertext">',
1156
				'after' => '</div>',
1157
				'block_level' => true,
1158
			),
1159
			array(
1160
				'tag' => 'code',
1161
				'type' => 'unparsed_content',
1162
				'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>',
1163
				// @todo Maybe this can be simplified?
1164
				'validate' => isset($disabled['code']) ? null : function (&$tag, &$data, $disabled) use ($context)
1165
				{
1166
					if (!isset($disabled['code']))
1167
					{
1168
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1169
1170 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...
1171
						{
1172
							// Do PHP code coloring?
1173
							if ($php_parts[$php_i] != '&lt;?php')
1174
								continue;
1175
1176
							$php_string = '';
1177
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1178
							{
1179
								$php_string .= $php_parts[$php_i];
1180
								$php_parts[$php_i++] = '';
1181
							}
1182
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1183
						}
1184
1185
						// Fix the PHP code stuff...
1186
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1187
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1188
1189
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1190
						if ($context['browser']['is_opera'])
1191
							$data .= '&nbsp;';
1192
					}
1193
				},
1194
				'block_level' => true,
1195
			),
1196
			array(
1197
				'tag' => 'code',
1198
				'type' => 'unparsed_equals_content',
1199
				'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>',
1200
				// @todo Maybe this can be simplified?
1201
				'validate' => isset($disabled['code']) ? null : function (&$tag, &$data, $disabled) use ($context)
1202
				{
1203
					if (!isset($disabled['code']))
1204
					{
1205
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1206
1207 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...
1208
						{
1209
							// Do PHP code coloring?
1210
							if ($php_parts[$php_i] != '&lt;?php')
1211
								continue;
1212
1213
							$php_string = '';
1214
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1215
							{
1216
								$php_string .= $php_parts[$php_i];
1217
								$php_parts[$php_i++] = '';
1218
							}
1219
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1220
						}
1221
1222
						// Fix the PHP code stuff...
1223
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1224
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1225
1226
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1227
						if ($context['browser']['is_opera'])
1228
							$data[0] .= '&nbsp;';
1229
					}
1230
				},
1231
				'block_level' => true,
1232
			),
1233
			array(
1234
				'tag' => 'color',
1235
				'type' => 'unparsed_equals',
1236
				'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]?)\))\]',
1237
				'before' => '<span style="color: $1;" class="bbc_color">',
1238
				'after' => '</span>',
1239
			),
1240
			array(
1241
				'tag' => 'email',
1242
				'type' => 'unparsed_content',
1243
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1244
				// @todo Should this respect guest_hideContacts?
1245
				'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...
1246
				{
1247
					$data = strtr($data, array('<br>' => ''));
1248
				},
1249
			),
1250
			array(
1251
				'tag' => 'email',
1252
				'type' => 'unparsed_equals',
1253
				'before' => '<a href="mailto:$1" class="bbc_email">',
1254
				'after' => '</a>',
1255
				// @todo Should this respect guest_hideContacts?
1256
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1257
				'disabled_after' => ' ($1)',
1258
			),
1259
			array(
1260
				'tag' => 'flash',
1261
				'type' => 'unparsed_commas_content',
1262
				'test' => '\d+,\d+\]',
1263
				'content' => '<embed type="application/x-shockwave-flash" src="$1" width="$2" height="$3" play="true" loop="true" quality="high" AllowScriptAccess="never">',
1264
				'validate' => function (&$tag, &$data, $disabled)
1265
				{
1266
					if (isset($disabled['url']))
1267
						$tag['content'] = '$1';
1268
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1269
					if (empty($scheme))
1270
						$data[0] = '//' . ltrim($data[0], ':/');
1271
				},
1272
				'disabled_content' => '<a href="$1" target="_blank" class="new_win">$1</a>',
1273
			),
1274
			array(
1275
				'tag' => 'float',
1276
				'type' => 'unparsed_equals',
1277
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1278
				'before' => '<div $1>',
1279
				'after' => '</div>',
1280
				'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...
1281
				{
1282
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1283
1284
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1285
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1286
					else
1287
						$css = '';
1288
1289
					$data = $class . $css;
1290
				},
1291
				'trim' => 'outside',
1292
				'block_level' => true,
1293
			),
1294
			array(
1295
				'tag' => 'font',
1296
				'type' => 'unparsed_equals',
1297
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1298
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1299
				'after' => '</span>',
1300
			),
1301
			array(
1302
				'tag' => 'html',
1303
				'type' => 'unparsed_content',
1304
				'content' => '<div>$1</div>',
1305
				'block_level' => true,
1306
				'disabled_content' => '$1',
1307
			),
1308
			array(
1309
				'tag' => 'hr',
1310
				'type' => 'closed',
1311
				'content' => '<hr>',
1312
				'block_level' => true,
1313
			),
1314
			array(
1315
				'tag' => 'i',
1316
				'before' => '<i>',
1317
				'after' => '</i>',
1318
			),
1319
			array(
1320
				'tag' => 'img',
1321
				'type' => 'unparsed_content',
1322
				'parameters' => array(
1323
					'alt' => array('optional' => true),
1324
					'title' => array('optional' => true),
1325
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1326
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1327
				),
1328
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized">',
1329 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...
1330
				{
1331
					global $image_proxy_enabled, $image_proxy_secret, $boardurl;
1332
1333
					$data = strtr($data, array('<br>' => ''));
1334
					$scheme = parse_url($data, PHP_URL_SCHEME);
1335
					if ($image_proxy_enabled)
1336
					{
1337
						if (empty($scheme))
1338
							$data = 'http://' . ltrim($data, ':/');
1339
1340
						if ($scheme != 'https')
1341
							$data = $boardurl . '/proxy.php?request=' . urlencode($data) . '&hash=' . md5($data . $image_proxy_secret);
1342
					}
1343
					elseif (empty($scheme))
1344
						$data = '//' . ltrim($data, ':/');
1345
				},
1346
				'disabled_content' => '($1)',
1347
			),
1348
			array(
1349
				'tag' => 'img',
1350
				'type' => 'unparsed_content',
1351
				'content' => '<img src="$1" alt="" class="bbc_img">',
1352 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...
1353
				{
1354
					global $image_proxy_enabled, $image_proxy_secret, $boardurl;
1355
1356
					$data = strtr($data, array('<br>' => ''));
1357
					$scheme = parse_url($data, PHP_URL_SCHEME);
1358
					if ($image_proxy_enabled)
1359
					{
1360
						if (empty($scheme))
1361
							$data = 'http://' . ltrim($data, ':/');
1362
1363
						if ($scheme != 'https')
1364
							$data = $boardurl . '/proxy.php?request=' . urlencode($data) . '&hash=' . md5($data . $image_proxy_secret);
1365
					}
1366
					elseif (empty($scheme))
1367
						$data = '//' . ltrim($data, ':/');
1368
				},
1369
				'disabled_content' => '($1)',
1370
			),
1371
			array(
1372
				'tag' => 'iurl',
1373
				'type' => 'unparsed_content',
1374
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1375 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...
1376
				{
1377
					$data = strtr($data, array('<br>' => ''));
1378
					$scheme = parse_url($data, PHP_URL_SCHEME);
1379
					if (empty($scheme))
1380
						$data = '//' . ltrim($data, ':/');
1381
				},
1382
			),
1383
			array(
1384
				'tag' => 'iurl',
1385
				'type' => 'unparsed_equals',
1386
				'quoted' => 'optional',
1387
				'before' => '<a href="$1" class="bbc_link">',
1388
				'after' => '</a>',
1389
				'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...
1390
				{
1391
					if (substr($data, 0, 1) == '#')
1392
						$data = '#post_' . substr($data, 1);
1393
					else
1394
					{
1395
						$scheme = parse_url($data, PHP_URL_SCHEME);
1396
						if (empty($scheme))
1397
							$data = '//' . ltrim($data, ':/');
1398
					}
1399
				},
1400
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1401
				'disabled_after' => ' ($1)',
1402
			),
1403
			array(
1404
				'tag' => 'left',
1405
				'before' => '<div style="text-align: left;">',
1406
				'after' => '</div>',
1407
				'block_level' => true,
1408
			),
1409
			array(
1410
				'tag' => 'li',
1411
				'before' => '<li>',
1412
				'after' => '</li>',
1413
				'trim' => 'outside',
1414
				'require_parents' => array('list'),
1415
				'block_level' => true,
1416
				'disabled_before' => '',
1417
				'disabled_after' => '<br>',
1418
			),
1419
			array(
1420
				'tag' => 'list',
1421
				'before' => '<ul class="bbc_list">',
1422
				'after' => '</ul>',
1423
				'trim' => 'inside',
1424
				'require_children' => array('li', 'list'),
1425
				'block_level' => true,
1426
			),
1427
			array(
1428
				'tag' => 'list',
1429
				'parameters' => array(
1430
					'type' => array('match' => '(none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|upper-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'),
1431
				),
1432
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1433
				'after' => '</ul>',
1434
				'trim' => 'inside',
1435
				'require_children' => array('li'),
1436
				'block_level' => true,
1437
			),
1438
			array(
1439
				'tag' => 'ltr',
1440
				'before' => '<bdo dir="ltr">',
1441
				'after' => '</bdo>',
1442
				'block_level' => true,
1443
			),
1444
			array(
1445
				'tag' => 'me',
1446
				'type' => 'unparsed_equals',
1447
				'before' => '<div class="meaction">* $1 ',
1448
				'after' => '</div>',
1449
				'quoted' => 'optional',
1450
				'block_level' => true,
1451
				'disabled_before' => '/me ',
1452
				'disabled_after' => '<br>',
1453
			),
1454
			array(
1455
				'tag' => 'member',
1456
				'type' => 'unparsed_equals',
1457
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1458
				'after' => '</a>',
1459
			),
1460
			array(
1461
				'tag' => 'nobbc',
1462
				'type' => 'unparsed_content',
1463
				'content' => '$1',
1464
			),
1465
			array(
1466
				'tag' => 'php',
1467
				'type' => 'unparsed_content',
1468
				'content' => '<span class="phpcode">$1</span>',
1469
				'validate' => isset($disabled['php']) ? null : function (&$tag, &$data, $disabled)
1470
				{
1471
					if (!isset($disabled['php']))
1472
					{
1473
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1474
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1475
						if ($add_begin)
1476
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1477
					}
1478
				},
1479
				'block_level' => false,
1480
				'disabled_content' => '$1',
1481
			),
1482
			array(
1483
				'tag' => 'pre',
1484
				'before' => '<pre>',
1485
				'after' => '</pre>',
1486
			),
1487
			array(
1488
				'tag' => 'quote',
1489
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1490
				'after' => '</blockquote>',
1491
				'trim' => 'both',
1492
				'block_level' => true,
1493
			),
1494
			array(
1495
				'tag' => 'quote',
1496
				'parameters' => array(
1497
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1498
				),
1499
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1500
				'after' => '</blockquote>',
1501
				'trim' => 'both',
1502
				'block_level' => true,
1503
			),
1504
			array(
1505
				'tag' => 'quote',
1506
				'type' => 'parsed_equals',
1507
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1508
				'after' => '</blockquote>',
1509
				'trim' => 'both',
1510
				'quoted' => 'optional',
1511
				// Don't allow everything to be embedded with the author name.
1512
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1513
				'block_level' => true,
1514
			),
1515
			array(
1516
				'tag' => 'quote',
1517
				'parameters' => array(
1518
					'author' => array('match' => '([^<>]{1,192}?)'),
1519
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1520
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1521
				),
1522
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1523
				'after' => '</blockquote>',
1524
				'trim' => 'both',
1525
				'block_level' => true,
1526
			),
1527
			array(
1528
				'tag' => 'quote',
1529
				'parameters' => array(
1530
					'author' => array('match' => '(.{1,192}?)'),
1531
				),
1532
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1533
				'after' => '</blockquote>',
1534
				'trim' => 'both',
1535
				'block_level' => true,
1536
			),
1537
			array(
1538
				'tag' => 'right',
1539
				'before' => '<div style="text-align: right;">',
1540
				'after' => '</div>',
1541
				'block_level' => true,
1542
			),
1543
			array(
1544
				'tag' => 'rtl',
1545
				'before' => '<bdo dir="rtl">',
1546
				'after' => '</bdo>',
1547
				'block_level' => true,
1548
			),
1549
			array(
1550
				'tag' => 's',
1551
				'before' => '<s>',
1552
				'after' => '</s>',
1553
			),
1554
			array(
1555
				'tag' => 'size',
1556
				'type' => 'unparsed_equals',
1557
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
1558
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1559
				'after' => '</span>',
1560
			),
1561
			array(
1562
				'tag' => 'size',
1563
				'type' => 'unparsed_equals',
1564
				'test' => '[1-7]\]',
1565
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1566
				'after' => '</span>',
1567
				'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...
1568
				{
1569
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
1570
					$data = $sizes[$data] . 'em';
1571
				},
1572
			),
1573
			array(
1574
				'tag' => 'sub',
1575
				'before' => '<sub>',
1576
				'after' => '</sub>',
1577
			),
1578
			array(
1579
				'tag' => 'sup',
1580
				'before' => '<sup>',
1581
				'after' => '</sup>',
1582
			),
1583
			array(
1584
				'tag' => 'table',
1585
				'before' => '<table class="bbc_table">',
1586
				'after' => '</table>',
1587
				'trim' => 'inside',
1588
				'require_children' => array('tr'),
1589
				'block_level' => true,
1590
			),
1591
			array(
1592
				'tag' => 'td',
1593
				'before' => '<td>',
1594
				'after' => '</td>',
1595
				'require_parents' => array('tr'),
1596
				'trim' => 'outside',
1597
				'block_level' => true,
1598
				'disabled_before' => '',
1599
				'disabled_after' => '',
1600
			),
1601
			array(
1602
				'tag' => 'time',
1603
				'type' => 'unparsed_content',
1604
				'content' => '$1',
1605
				'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...
1606
				{
1607
					if (is_numeric($data))
1608
						$data = timeformat($data);
1609
					else
1610
						$tag['content'] = '[time]$1[/time]';
1611
				},
1612
			),
1613
			array(
1614
				'tag' => 'tr',
1615
				'before' => '<tr>',
1616
				'after' => '</tr>',
1617
				'require_parents' => array('table'),
1618
				'require_children' => array('td'),
1619
				'trim' => 'both',
1620
				'block_level' => true,
1621
				'disabled_before' => '',
1622
				'disabled_after' => '',
1623
			),
1624
			array(
1625
				'tag' => 'u',
1626
				'before' => '<u>',
1627
				'after' => '</u>',
1628
			),
1629
			array(
1630
				'tag' => 'url',
1631
				'type' => 'unparsed_content',
1632
				'content' => '<a href="$1" class="bbc_link" target="_blank">$1</a>',
1633 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...
1634
				{
1635
					$data = strtr($data, array('<br>' => ''));
1636
					$scheme = parse_url($data, PHP_URL_SCHEME);
1637
					if (empty($scheme))
1638
						$data = '//' . ltrim($data, ':/');
1639
				},
1640
			),
1641
			array(
1642
				'tag' => 'url',
1643
				'type' => 'unparsed_equals',
1644
				'quoted' => 'optional',
1645
				'before' => '<a href="$1" class="bbc_link" target="_blank">',
1646
				'after' => '</a>',
1647
				'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...
1648
				{
1649
					$scheme = parse_url($data, PHP_URL_SCHEME);
1650
					if (empty($scheme))
1651
						$data = '//' . ltrim($data, ':/');
1652
				},
1653
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1654
				'disabled_after' => ' ($1)',
1655
			),
1656
		);
1657
1658
		// Inside these tags autolink is not recommendable.
1659
		$no_autolink_tags = array(
1660
			'url',
1661
			'iurl',
1662
			'email',
1663
		);
1664
1665
		// Let mods add new BBC without hassle.
1666
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
1667
1668
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
1669
		if ($message === false)
1670
		{
1671
			if (isset($temp_bbc))
1672
				$bbc_codes = $temp_bbc;
1673
			usort($codes, function ($a, $b) {
1674
				return strcmp($a['tag'], $b['tag']);
1675
			});
1676
			return $codes;
1677
		}
1678
1679
		// So the parser won't skip them.
1680
		$itemcodes = array(
1681
			'*' => 'disc',
1682
			'@' => 'disc',
1683
			'+' => 'square',
1684
			'x' => 'square',
1685
			'#' => 'square',
1686
			'o' => 'circle',
1687
			'O' => 'circle',
1688
			'0' => 'circle',
1689
		);
1690
		if (!isset($disabled['li']) && !isset($disabled['list']))
1691
		{
1692
			foreach ($itemcodes as $c => $dummy)
1693
				$bbc_codes[$c] = array();
1694
		}
1695
1696
		// Shhhh!
1697
		if (!isset($disabled['color']))
1698
		{
1699
			$codes[] = array(
1700
				'tag' => 'chrissy',
1701
				'before' => '<span style="color: #cc0099;">',
1702
				'after' => ' :-*</span>',
1703
			);
1704
			$codes[] = array(
1705
				'tag' => 'kissy',
1706
				'before' => '<span style="color: #cc0099;">',
1707
				'after' => ' :-*</span>',
1708
			);
1709
		}
1710
1711
		foreach ($codes as $code)
1712
		{
1713
			// Make it easier to process parameters later
1714
			if (!empty($code['parameters']))
1715
				ksort($code['parameters'], SORT_STRING);
1716
1717
			// If we are not doing every tag only do ones we are interested in.
1718
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
1719
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
1720
		}
1721
		$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...
1722
	}
1723
1724
	// Shall we take the time to cache this?
1725
	if ($cache_id != '' && !empty($modSettings['cache_enable']) && (($modSettings['cache_enable'] >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
1726
	{
1727
		// It's likely this will change if the message is modified.
1728
		$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']);
1729
1730
		if (($temp = cache_get_data($cache_key, 240)) != null)
1731
			return $temp;
1732
1733
		$cache_t = microtime();
1734
	}
1735
1736
	if ($smileys === 'print')
1737
	{
1738
		// [glow], [shadow], and [move] can't really be printed.
1739
		$disabled['glow'] = true;
1740
		$disabled['shadow'] = true;
1741
		$disabled['move'] = true;
1742
1743
		// Colors can't well be displayed... supposed to be black and white.
1744
		$disabled['color'] = true;
1745
		$disabled['black'] = true;
1746
		$disabled['blue'] = true;
1747
		$disabled['white'] = true;
1748
		$disabled['red'] = true;
1749
		$disabled['green'] = true;
1750
		$disabled['me'] = true;
1751
1752
		// Color coding doesn't make sense.
1753
		$disabled['php'] = true;
1754
1755
		// Links are useless on paper... just show the link.
1756
		$disabled['ftp'] = true;
1757
		$disabled['url'] = true;
1758
		$disabled['iurl'] = true;
1759
		$disabled['email'] = true;
1760
		$disabled['flash'] = true;
1761
1762
		// @todo Change maybe?
1763
		if (!isset($_GET['images']))
1764
			$disabled['img'] = true;
1765
1766
		// @todo Interface/setting to add more?
1767
	}
1768
1769
	$open_tags = array();
1770
	$message = strtr($message, array("\n" => '<br>'));
1771
1772
	$alltags = array();
1773
	foreach ($bbc_codes as $section) {
1774
		foreach ($section as $code) {
1775
			$alltags[] = $code['tag'];
1776
		}
1777
	}
1778
	$alltags_regex = '\b' . implode("\b|\b", array_unique($alltags)) . '\b';
1779
1780
	$pos = -1;
1781
	while ($pos !== false)
1782
	{
1783
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
1784
		preg_match('~\[/?(?=' . $alltags_regex . ')~', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
1785
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
1786
1787
		// Failsafe.
1788
		if ($pos === false || $last_pos > $pos)
1789
			$pos = strlen($message) + 1;
1790
1791
		// Can't have a one letter smiley, URL, or email! (sorry.)
1792
		if ($last_pos < $pos - 1)
1793
		{
1794
			// Make sure the $last_pos is not negative.
1795
			$last_pos = max($last_pos, 0);
1796
1797
			// Pick a block of data to do some raw fixing on.
1798
			$data = substr($message, $last_pos, $pos - $last_pos);
1799
1800
			// Take care of some HTML!
1801
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
1802
			{
1803
				$data = preg_replace('~&lt;a\s+href=((?:&quot;)?)((?:https?://|ftps?://|mailto:)\S+?)\\1&gt;~i', '[url=&quot;$2&quot;]', $data);
1804
				$data = preg_replace('~&lt;/a&gt;~i', '[/url]', $data);
1805
1806
				// <br> should be empty.
1807
				$empty_tags = array('br', 'hr');
1808
				foreach ($empty_tags as $tag)
1809
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '[' . $tag . ' /]', $data);
1810
1811
				// b, u, i, s, pre... basic tags.
1812
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote');
1813
				foreach ($closable_tags as $tag)
1814
				{
1815
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
1816
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
1817
1818
					if ($diff > 0)
1819
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
1820
				}
1821
1822
				// Do <img ...> - with security... action= -> action-.
1823
				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);
1824
				if (!empty($matches[0]))
1825
				{
1826
					$replaces = array();
1827
					foreach ($matches[2] as $match => $imgtag)
1828
					{
1829
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
1830
1831
						// Remove action= from the URL - no funny business, now.
1832
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
1833
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
1834
1835
						// Check if the image is larger than allowed.
1836
						if (!empty($modSettings['max_image_width']) && !empty($modSettings['max_image_height']))
1837
						{
1838
							list ($width, $height) = url_image_size($imgtag);
1839
1840 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...
1841
							{
1842
								$height = (int) (($modSettings['max_image_width'] * $height) / $width);
1843
								$width = $modSettings['max_image_width'];
1844
							}
1845
1846 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...
1847
							{
1848
								$width = (int) (($modSettings['max_image_height'] * $width) / $height);
1849
								$height = $modSettings['max_image_height'];
1850
							}
1851
1852
							// Set the new image tag.
1853
							$replaces[$matches[0][$match]] = '[img width=' . $width . ' height=' . $height . $alt . ']' . $imgtag . '[/img]';
1854
						}
1855
						else
1856
							$replaces[$matches[0][$match]] = '[img' . $alt . ']' . $imgtag . '[/img]';
1857
					}
1858
1859
					$data = strtr($data, $replaces);
1860
				}
1861
			}
1862
1863
			if (!empty($modSettings['autoLinkUrls']))
1864
			{
1865
				// Are we inside tags that should be auto linked?
1866
				$no_autolink_area = false;
1867
				if (!empty($open_tags))
1868
				{
1869
					foreach ($open_tags as $open_tag)
1870
						if (in_array($open_tag['tag'], $no_autolink_tags))
1871
							$no_autolink_area = true;
1872
				}
1873
1874
				// Don't go backwards.
1875
				// @todo Don't think is the real solution....
1876
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
1877
				if ($pos < $lastAutoPos)
1878
					$no_autolink_area = true;
1879
				$lastAutoPos = $pos;
1880
1881
				if (!$no_autolink_area)
1882
				{
1883
					// Parse any URLs
1884
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
1885
					{
1886
						$url_regex = '
1887
						(?:
1888
							# IRIs with a scheme (or at least an opening "//")
1889
							(?:
1890
								# URI scheme (or lack thereof for schemeless URLs)
1891
								(?:
1892
									# URL scheme and colon
1893
									\b[a-z][\w\-]+:
1894
									| # or
1895
									# A boundary followed by two slashes for schemeless URLs
1896
									(?<=^|\W)(?=//)
1897
								)
1898
1899
								# IRI "authority" chunk
1900
								(?:
1901
									# 2 slashes for IRIs with an "authority"
1902
									//
1903
									# then a domain name
1904
									(?:
1905
										# Either the reserved "localhost" domain name
1906
										localhost
1907
										| # or
1908
										# a run of Unicode domain name characters and a dot
1909
										[\p{L}\p{M}\p{N}\-.:@]+\.
1910
										# and then a TLD valid in the DNS or the reserved "local" TLD
1911
										(?:'. $modSettings['tld_regex'] .'|local)
1912
									)
1913
									# followed by a non-domain character or end of line
1914
									(?=[^\p{L}\p{N}\-.]|$)
1915
1916
									| # Or, if there is no "authority" per se (e.g. mailto: URLs) ...
1917
1918
									# a run of IRI characters
1919
									[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]
1920
									# and then a dot and a closing IRI label
1921
									\.[\p{L}\p{M}\p{N}\-]+
1922
								)
1923
							)
1924
1925
							| # or
1926
1927
							# Naked domains (e.g. "example.com" in "Go to example.com for an example.")
1928
							(?:
1929
								# Preceded by start of line or a non-domain character
1930
								(?<=^|[^\p{L}\p{M}\p{N}\-:@])
1931
1932
								# A run of Unicode domain name characters (excluding [:@])
1933
								[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]
1934
								# and then a dot and a valid TLD
1935
								\.' . $modSettings['tld_regex'] . '
1936
1937
								# Followed by either:
1938
								(?=
1939
									# end of line or a non-domain character (excluding [.:@])
1940
									$|[^\p{L}\p{N}\-]
1941
									| # or
1942
									# a dot followed by end of line or a non-domain character (excluding [.:@])
1943
									\.(?=$|[^\p{L}\p{N}\-])
1944
								)
1945
							)
1946
						)
1947
1948
						# IRI path, query, and fragment (if present)
1949
						(?:
1950
							# If any of these parts exist, must start with a single /
1951
							/
1952
1953
							# And then optionally:
1954
							(?:
1955
								# One or more of:
1956
								(?:
1957
									# a run of non-space, non-()<>
1958
									[^\s()<>]+
1959
									| # or
1960
									# balanced parens, up to 2 levels
1961
									\(([^\s()<>]+|(\([^\s()<>]+\)))*\)
1962
								)+
1963
1964
								# End with:
1965
								(?:
1966
									# balanced parens, up to 2 levels
1967
									\(([^\s()<>]+|(\([^\s()<>]+\)))*\)
1968
									| # or
1969
									# not a space or one of these punct char
1970
									[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]
1971
									| # or
1972
									# a trailing slash (but not two in a row)
1973
									(?<!/)/
1974
								)
1975
							)?
1976
						)?
1977
						';
1978
1979
						$data = preg_replace_callback('~' . $url_regex . '~xi' . ($context['utf8'] ? 'u' : ''), function ($matches) {
1980
							$url = array_shift($matches);
1981
1982
							$scheme = parse_url($url, PHP_URL_SCHEME);
1983
1984
							if ($scheme == 'mailto')
1985
							{
1986
								$email_address = str_replace('mailto:', '', $url);
1987
								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...
1988
									return '[email=' . $email_address . ']' . $url . '[/email]';
1989
								else
1990
									return $url;
1991
							}
1992
1993
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
1994
							if (empty($scheme))
1995
								$fullUrl = '//' . ltrim($url, ':/');
1996
							else
1997
								$fullUrl = $url;
1998
1999
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2000
						}, $data);
2001
					}
2002
2003
					// Next, emails...
2004
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false)
2005
					{
2006
						$email_regex = '
2007
						# Preceded by a non-domain character or start of line
2008
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
2009
2010
						# An email address
2011
						[\p{L}\p{M}\p{N}_\-.]{1,80}
2012
						@
2013
						[\p{L}\p{M}\p{N}\-.]+
2014
						\.
2015
						'. $modSettings['tld_regex'] . '
2016
2017
						# Followed by either:
2018
						(?=
2019
							# end of line or a non-domain character (excluding the dot)
2020
							$|[^\p{L}\p{M}\p{N}\-]
2021
							| # or
2022
							# a dot followed by end of line or a non-domain character
2023
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2024
						)';
2025
2026
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2027
					}
2028
				}
2029
			}
2030
2031
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2032
2033
			// If it wasn't changed, no copying or other boring stuff has to happen!
2034
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2035
			{
2036
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2037
2038
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2039
				$old_pos = strlen($data) + $last_pos;
2040
				$pos = strpos($message, '[', $last_pos);
2041
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2042
			}
2043
		}
2044
2045
		// Are we there yet?  Are we there yet?
2046
		if ($pos >= strlen($message) - 1)
2047
			break;
2048
2049
		$tags = strtolower($message[$pos + 1]);
2050
2051
		if ($tags == '/' && !empty($open_tags))
2052
		{
2053
			$pos2 = strpos($message, ']', $pos + 1);
2054
			if ($pos2 == $pos + 2)
2055
				continue;
2056
2057
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2058
2059
			$to_close = array();
2060
			$block_level = null;
2061
2062
			do
2063
			{
2064
				$tag = array_pop($open_tags);
2065
				if (!$tag)
2066
					break;
2067
2068
				if (!empty($tag['block_level']))
2069
				{
2070
					// Only find out if we need to.
2071
					if ($block_level === false)
2072
					{
2073
						array_push($open_tags, $tag);
2074
						break;
2075
					}
2076
2077
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2078 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...
2079
					{
2080
						foreach ($bbc_codes[$look_for[0]] as $temp)
2081
							if ($temp['tag'] == $look_for)
2082
							{
2083
								$block_level = !empty($temp['block_level']);
2084
								break;
2085
							}
2086
					}
2087
2088
					if ($block_level !== true)
2089
					{
2090
						$block_level = false;
2091
						array_push($open_tags, $tag);
2092
						break;
2093
					}
2094
				}
2095
2096
				$to_close[] = $tag;
2097
			}
2098
			while ($tag['tag'] != $look_for);
2099
2100
			// Did we just eat through everything and not find it?
2101
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2102
			{
2103
				$open_tags = $to_close;
2104
				continue;
2105
			}
2106
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2107
			{
2108 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...
2109
				{
2110
					foreach ($bbc_codes[$look_for[0]] as $temp)
2111
						if ($temp['tag'] == $look_for)
2112
						{
2113
							$block_level = !empty($temp['block_level']);
2114
							break;
2115
						}
2116
				}
2117
2118
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2119
				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...
2120
				{
2121
					foreach ($to_close as $tag)
2122
						array_push($open_tags, $tag);
2123
					continue;
2124
				}
2125
			}
2126
2127
			foreach ($to_close as $tag)
2128
			{
2129
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2130
				$pos += strlen($tag['after']) + 2;
2131
				$pos2 = $pos - 1;
2132
2133
				// See the comment at the end of the big loop - just eating whitespace ;).
2134
				$whitespace_regex = '';
2135
				if (!empty($tag['block_level']))
2136
					$whitespace_regex .= '(&nbsp;|\s)*<br>';
2137
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2138 View Code Duplication
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
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...
2139
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2140
2141 View Code Duplication
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', 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...
2142
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2143
			}
2144
2145
			if (!empty($to_close))
2146
			{
2147
				$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...
2148
				$pos--;
2149
			}
2150
2151
			continue;
2152
		}
2153
2154
		// No tags for this character, so just keep going (fastest possible course.)
2155
		if (!isset($bbc_codes[$tags]))
2156
			continue;
2157
2158
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2159
		$tag = null;
2160
		foreach ($bbc_codes[$tags] as $possible)
2161
		{
2162
			$pt_strlen = strlen($possible['tag']);
2163
2164
			// Not a match?
2165
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2166
				continue;
2167
2168
			$next_c = $message[$pos + 1 + $pt_strlen];
2169
2170
			// A test validation?
2171
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2172
				continue;
2173
			// Do we want parameters?
2174
			elseif (!empty($possible['parameters']))
2175
			{
2176
				if ($next_c != ' ')
2177
					continue;
2178
			}
2179
			elseif (isset($possible['type']))
2180
			{
2181
				// Do we need an equal sign?
2182
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2183
					continue;
2184
				// Maybe we just want a /...
2185
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2186
					continue;
2187
				// An immediate ]?
2188
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2189
					continue;
2190
			}
2191
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2192
			elseif ($next_c != ']')
2193
				continue;
2194
2195
			// Check allowed tree?
2196
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2197
				continue;
2198
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2199
				continue;
2200
			// If this is in the list of disallowed child tags, don't parse it.
2201
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2202
				continue;
2203
2204
			$pos1 = $pos + 1 + $pt_strlen + 1;
2205
2206
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2207
			if ($possible['tag'] == 'quote')
2208
			{
2209
				// Start with standard
2210
				$quote_alt = false;
2211
				foreach ($open_tags as $open_quote)
2212
				{
2213
					// Every parent quote this quote has flips the styling
2214
					if ($open_quote['tag'] == 'quote')
2215
						$quote_alt = !$quote_alt;
2216
				}
2217
				// Add a class to the quote to style alternating blockquotes
2218
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2219
			}
2220
2221
			// This is long, but it makes things much easier and cleaner.
2222
			if (!empty($possible['parameters']))
2223
			{
2224
				// Build a regular expression for each parameter for the current tag.
2225
				$preg = array();
2226
				foreach ($possible['parameters'] as $p => $info)
2227
					$preg[] = '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
2228
2229
				// Extract the string that potentially holds our parameters.
2230
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
2231
				$blobs = preg_split('~\]~i', $blob[1]);
2232
2233
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
2234
2235
				// Progressively append more blobs until we find our parameters or run out of blobs
2236
				$blob_counter = 1;
2237
				while ($blob_counter <= count($blobs))
2238
				{
2239
2240
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
2241
2242
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2243
					sort($given_params, SORT_STRING);
2244
2245
					$match = preg_match('~^' . implode('', $preg) . '$~i', implode(' ', $given_params), $matches) !== 0;
2246
2247
					if ($match)
2248
						$blob_counter = count($blobs) + 1;
2249
				}
2250
2251
				// Didn't match our parameter list, try the next possible.
2252
				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...
2253
					continue;
2254
2255
				$params = array();
2256
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2257
				{
2258
					$key = strtok(ltrim($matches[$i]), '=');
2259
					if (isset($possible['parameters'][$key]['value']))
2260
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2261
					elseif (isset($possible['parameters'][$key]['validate']))
2262
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2263
					else
2264
						$params['{' . $key . '}'] = $matches[$i + 1];
2265
2266
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2267
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2268
				}
2269
2270
				foreach ($possible['parameters'] as $p => $info)
2271
				{
2272
					if (!isset($params['{' . $p . '}']))
2273
						$params['{' . $p . '}'] = '';
2274
				}
2275
2276
				$tag = $possible;
2277
2278
				// Put the parameters into the string.
2279
				if (isset($tag['before']))
2280
					$tag['before'] = strtr($tag['before'], $params);
2281
				if (isset($tag['after']))
2282
					$tag['after'] = strtr($tag['after'], $params);
2283
				if (isset($tag['content']))
2284
					$tag['content'] = strtr($tag['content'], $params);
2285
2286
				$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...
2287
			}
2288
			else
2289
			{
2290
				$tag = $possible;
2291
				$params = array();
2292
			}
2293
			break;
2294
		}
2295
2296
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2297
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2298
		{
2299
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2300
				continue;
2301
2302
			$tag = $itemcodes[$message[$pos + 1]];
2303
2304
			// First let's set up the tree: it needs to be in a list, or after an li.
2305
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2306
			{
2307
				$open_tags[] = array(
2308
					'tag' => 'list',
2309
					'after' => '</ul>',
2310
					'block_level' => true,
2311
					'require_children' => array('li'),
2312
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2313
				);
2314
				$code = '<ul class="bbc_list">';
2315
			}
2316
			// We're in a list item already: another itemcode?  Close it first.
2317
			elseif ($inside['tag'] == 'li')
2318
			{
2319
				array_pop($open_tags);
2320
				$code = '</li>';
2321
			}
2322
			else
2323
				$code = '';
2324
2325
			// Now we open a new tag.
2326
			$open_tags[] = array(
2327
				'tag' => 'li',
2328
				'after' => '</li>',
2329
				'trim' => 'outside',
2330
				'block_level' => true,
2331
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2332
			);
2333
2334
			// First, open the tag...
2335
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2336
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2337
			$pos += strlen($code) - 1 + 2;
2338
2339
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2340
			$pos2 = strpos($message, '<br>', $pos);
2341
			$pos3 = strpos($message, '[/', $pos);
2342
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2343
			{
2344
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2345
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2346
2347
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2348
			}
2349
			// Tell the [list] that it needs to close specially.
2350
			else
2351
			{
2352
				// Move the li over, because we're not sure what we'll hit.
2353
				$open_tags[count($open_tags) - 1]['after'] = '';
2354
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2355
			}
2356
2357
			continue;
2358
		}
2359
2360
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2361
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2362
		{
2363
			array_pop($open_tags);
2364
2365
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2366
			$pos += strlen($inside['after']) - 1 + 2;
2367
		}
2368
2369
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2370
		if ($tag === null)
2371
			continue;
2372
2373
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2374
		if (isset($inside['disallow_children']))
2375
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2376
2377
		// Is this tag disabled?
2378
		if (isset($disabled[$tag['tag']]))
2379
		{
2380
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2381
			{
2382
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2383
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2384
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2385
			}
2386
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2387
			{
2388
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2389
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2390
			}
2391
			else
2392
				$tag['content'] = $tag['disabled_content'];
2393
		}
2394
2395
		// we use this a lot
2396
		$tag_strlen = strlen($tag['tag']);
2397
2398
		// The only special case is 'html', which doesn't need to close things.
2399
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2400
		{
2401
			$n = count($open_tags) - 1;
2402
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2403
				$n--;
2404
2405
			// Close all the non block level tags so this tag isn't surrounded by them.
2406
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2407
			{
2408
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2409
				$ot_strlen = strlen($open_tags[$i]['after']);
2410
				$pos += $ot_strlen + 2;
2411
				$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...
2412
2413
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2414
				$whitespace_regex = '';
2415
				if (!empty($tag['block_level']))
2416
					$whitespace_regex .= '(&nbsp;|\s)*<br>';
2417 View Code Duplication
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
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...
2418
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2419 View Code Duplication
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', 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...
2420
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2421
2422
				array_pop($open_tags);
2423
			}
2424
		}
2425
2426
		// No type means 'parsed_content'.
2427
		if (!isset($tag['type']))
2428
		{
2429
			// @todo Check for end tag first, so people can say "I like that [i] tag"?
2430
			$open_tags[] = $tag;
2431
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2432
			$pos += strlen($tag['before']) - 1 + 2;
2433
		}
2434
		// Don't parse the content, just skip it.
2435
		elseif ($tag['type'] == 'unparsed_content')
2436
		{
2437
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2438
			if ($pos2 === false)
2439
				continue;
2440
2441
			$data = substr($message, $pos1, $pos2 - $pos1);
2442
2443
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2444
				$data = substr($data, 4);
2445
2446
			if (isset($tag['validate']))
2447
				$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...
2448
2449
			$code = strtr($tag['content'], array('$1' => $data));
2450
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2451
2452
			$pos += strlen($code) - 1 + 2;
2453
			$last_pos = $pos + 1;
2454
2455
		}
2456
		// Don't parse the content, just skip it.
2457
		elseif ($tag['type'] == 'unparsed_equals_content')
2458
		{
2459
			// The value may be quoted for some tags - check.
2460 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...
2461
			{
2462
				$quoted = substr($message, $pos1, 6) == '&quot;';
2463
				if ($tag['quoted'] != 'optional' && !$quoted)
2464
					continue;
2465
2466
				if ($quoted)
2467
					$pos1 += 6;
2468
			}
2469
			else
2470
				$quoted = false;
2471
2472
			$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...
2473
			if ($pos2 === false)
2474
				continue;
2475
2476
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2477
			if ($pos3 === false)
2478
				continue;
2479
2480
			$data = array(
2481
				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...
2482
				substr($message, $pos1, $pos2 - $pos1)
2483
			);
2484
2485
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
2486
				$data[0] = substr($data[0], 4);
2487
2488
			// Validation for my parking, please!
2489
			if (isset($tag['validate']))
2490
				$tag['validate']($tag, $data, $disabled, $params);
2491
2492
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
2493
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2494
			$pos += strlen($code) - 1 + 2;
2495
		}
2496
		// A closed tag, with no content or value.
2497
		elseif ($tag['type'] == 'closed')
2498
		{
2499
			$pos2 = strpos($message, ']', $pos);
2500
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
2501
			$pos += strlen($tag['content']) - 1 + 2;
2502
		}
2503
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
2504
		elseif ($tag['type'] == 'unparsed_commas_content')
2505
		{
2506
			$pos2 = strpos($message, ']', $pos1);
2507
			if ($pos2 === false)
2508
				continue;
2509
2510
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2511
			if ($pos3 === false)
2512
				continue;
2513
2514
			// We want $1 to be the content, and the rest to be csv.
2515
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
2516
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
2517
2518
			if (isset($tag['validate']))
2519
				$tag['validate']($tag, $data, $disabled, $params);
2520
2521
			$code = $tag['content'];
2522 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...
2523
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2524
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2525
			$pos += strlen($code) - 1 + 2;
2526
		}
2527
		// This has parsed content, and a csv value which is unparsed.
2528
		elseif ($tag['type'] == 'unparsed_commas')
2529
		{
2530
			$pos2 = strpos($message, ']', $pos1);
2531
			if ($pos2 === false)
2532
				continue;
2533
2534
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
2535
2536
			if (isset($tag['validate']))
2537
				$tag['validate']($tag, $data, $disabled, $params);
2538
2539
			// Fix after, for disabled code mainly.
2540 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...
2541
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
2542
2543
			$open_tags[] = $tag;
2544
2545
			// Replace them out, $1, $2, $3, $4, etc.
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2546
			$code = $tag['before'];
2547 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...
2548
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2549
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
2550
			$pos += strlen($code) - 1 + 2;
2551
		}
2552
		// A tag set to a value, parsed or not.
2553
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
2554
		{
2555
			// The value may be quoted for some tags - check.
2556 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...
2557
			{
2558
				$quoted = substr($message, $pos1, 6) == '&quot;';
2559
				if ($tag['quoted'] != 'optional' && !$quoted)
2560
					continue;
2561
2562
				if ($quoted)
2563
					$pos1 += 6;
2564
			}
2565
			else
2566
				$quoted = false;
2567
2568
			$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...
2569
			if ($pos2 === false)
2570
				continue;
2571
2572
			$data = substr($message, $pos1, $pos2 - $pos1);
2573
2574
			// Validation for my parking, please!
2575
			if (isset($tag['validate']))
2576
				$tag['validate']($tag, $data, $disabled, $params);
2577
2578
			// For parsed content, we must recurse to avoid security problems.
2579
			if ($tag['type'] != 'unparsed_equals')
2580
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
2581
2582
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
2583
2584
			$open_tags[] = $tag;
2585
2586
			$code = strtr($tag['before'], array('$1' => $data));
2587
			$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...
2588
			$pos += strlen($code) - 1 + 2;
2589
		}
2590
2591
		// If this is block level, eat any breaks after it.
2592
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
2593
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
2594
2595
		// Are we trimming outside this tag?
2596
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
2597
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
2598
	}
2599
2600
	// Close any remaining tags.
2601
	while ($tag = array_pop($open_tags))
2602
		$message .= "\n" . $tag['after'] . "\n";
2603
2604
	// Parse the smileys within the parts where it can be done safely.
2605
	if ($smileys === true)
2606
	{
2607
		$message_parts = explode("\n", $message);
2608
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
2609
			parsesmileys($message_parts[$i]);
2610
2611
		$message = implode('', $message_parts);
2612
	}
2613
2614
	// No smileys, just get rid of the markers.
2615
	else
2616
		$message = strtr($message, array("\n" => ''));
2617
2618
	if ($message !== '' && $message[0] === ' ')
2619
		$message = '&nbsp;' . substr($message, 1);
2620
2621
	// Cleanup whitespace.
2622
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
2623
2624
	// Allow mods access to what parse_bbc created
2625
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
2626
2627
	// Cache the output if it took some time...
2628
	if (isset($cache_key, $cache_t) && array_sum(explode(' ', microtime())) - array_sum(explode(' ', $cache_t)) > 0.05)
2629
		cache_put_data($cache_key, $message, 240);
2630
2631
	// If this was a force parse revert if needed.
2632
	if (!empty($parse_tags))
2633
	{
2634
		if (empty($temp_bbc))
2635
			$bbc_codes = array();
2636
		else
2637
		{
2638
			$bbc_codes = $temp_bbc;
2639
			unset($temp_bbc);
2640
		}
2641
	}
2642
2643
	return $message;
2644
}
2645
2646
/**
2647
 * Parse smileys in the passed message.
2648
 *
2649
 * The smiley parsing function which makes pretty faces appear :).
2650
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
2651
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
2652
 * Caches the smileys from the database or array in memory.
2653
 * Doesn't return anything, but rather modifies message directly.
2654
 *
2655
 * @param string &$message The message to parse smileys in
2656
 */
2657
function parsesmileys(&$message)
2658
{
2659
	global $modSettings, $txt, $user_info, $context, $smcFunc;
2660
	static $smileyPregSearch = null, $smileyPregReplacements = array();
2661
2662
	// No smiley set at all?!
2663
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
2664
		return;
2665
2666
	// If smileyPregSearch hasn't been set, do it now.
2667
	if (empty($smileyPregSearch))
2668
	{
2669
		// Use the default smileys if it is disabled. (better for "portability" of smileys.)
2670
		if (empty($modSettings['smiley_enable']))
2671
		{
2672
			$smileysfrom = array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', '0:)');
2673
			$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');
2674
			$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'], '', '', '', '');
2675
		}
2676
		else
2677
		{
2678
			// Load the smileys in reverse order by length so they don't get parsed wrong.
2679
			if (($temp = cache_get_data('parsing_smileys', 480)) == null)
2680
			{
2681
				$result = $smcFunc['db_query']('', '
2682
					SELECT code, filename, description
2683
					FROM {db_prefix}smileys
2684
					ORDER BY LENGTH(code) DESC',
2685
					array(
2686
					)
2687
				);
2688
				$smileysfrom = array();
2689
				$smileysto = array();
2690
				$smileysdescs = array();
2691
				while ($row = $smcFunc['db_fetch_assoc']($result))
2692
				{
2693
					$smileysfrom[] = $row['code'];
2694
					$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
2695
					$smileysdescs[] = $row['description'];
2696
				}
2697
				$smcFunc['db_free_result']($result);
2698
2699
				cache_put_data('parsing_smileys', array($smileysfrom, $smileysto, $smileysdescs), 480);
2700
			}
2701
			else
2702
				list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
2703
		}
2704
2705
		// The non-breaking-space is a complex thing...
2706
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
2707
2708
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
2709
		$smileyPregReplacements = array();
2710
		$searchParts = array();
2711
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
2712
2713
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
2714
		{
2715
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
2716
			$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">';
2717
2718
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
2719
2720
			$searchParts[] = preg_quote($smileysfrom[$i], '~');
2721
			if ($smileysfrom[$i] != $specialChars)
2722
			{
2723
				$smileyPregReplacements[$specialChars] = $smileyCode;
2724
				$searchParts[] = preg_quote($specialChars, '~');
2725
			}
2726
		}
2727
2728
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . implode('|', $searchParts) . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
2729
	}
2730
2731
	// Replace away!
2732
	$message = preg_replace_callback($smileyPregSearch,
2733
		function ($matches) use ($smileyPregReplacements)
2734
		{
2735
			return $smileyPregReplacements[$matches[1]];
2736
		}, $message);
2737
}
2738
2739
/**
2740
 * Highlight any code.
2741
 *
2742
 * Uses PHP's highlight_string() to highlight PHP syntax
2743
 * does special handling to keep the tabs in the code available.
2744
 * used to parse PHP code from inside [code] and [php] tags.
2745
 *
2746
 * @param string $code The code
2747
 * @return string The code with highlighted HTML.
2748
 */
2749
function highlight_php_code($code)
2750
{
2751
	// Remove special characters.
2752
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
2753
2754
	$oldlevel = error_reporting(0);
2755
2756
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
2757
2758
	error_reporting($oldlevel);
2759
2760
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
2761
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
2762
2763
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
2764
}
2765
2766
/**
2767
 * Make sure the browser doesn't come back and repost the form data.
2768
 * Should be used whenever anything is posted.
2769
 *
2770
 * @param string $setLocation The URL to redirect them to
2771
 * @param bool $refresh Whether to use a meta refresh instead
2772
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
2773
 */
2774
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
2775
{
2776
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
2777
2778
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
2779
	if (!empty($context['flush_mail']))
2780
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
2781
		AddMailQueue(true);
2782
2783
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
2784
2785
	if ($add)
2786
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
2787
2788
	// Put the session ID in.
2789
	if (defined('SID') && SID != '')
2790
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
2791
	// Keep that debug in their for template debugging!
2792 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...
2793
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
2794
2795
	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'])))
2796
	{
2797
		if (defined('SID') && SID != '')
2798
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '/') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
2799
				function ($m) use ($scripturl)
2800
				{
2801
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID. (isset($m[2]) ? "$m[2]" : "");
2802
				}, $setLocation);
2803 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...
2804
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '/') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
2805
				function ($m) use ($scripturl)
2806
				{
2807
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
2808
				}, $setLocation);
2809
	}
2810
2811
	// Maybe integrations want to change where we are heading?
2812
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
2813
2814
	// Set the header.
2815
	header('Location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
2816
2817
	// Debugging.
2818
	if (isset($db_show_debug) && $db_show_debug === true)
2819
		$_SESSION['debug_redirect'] = $db_cache;
2820
2821
	obExit(false);
2822
}
2823
2824
/**
2825
 * Ends execution.  Takes care of template loading and remembering the previous URL.
2826
 * @param bool $header Whether to do the header
2827
 * @param bool $do_footer Whether to do the footer
2828
 * @param bool $from_index Whether we're coming from the board index
2829
 * @param bool $from_fatal_error Whether we're coming from a fatal error
2830
 */
2831
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
2832
{
2833
	global $context, $settings, $modSettings, $txt, $smcFunc;
2834
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
2835
2836
	// Attempt to prevent a recursive loop.
2837
	++$level;
2838
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
2839
		exit;
2840
	if ($from_fatal_error)
2841
		$has_fatal_error = true;
2842
2843
	// Clear out the stat cache.
2844
	trackStats();
2845
2846
	// If we have mail to send, send it.
2847
	if (!empty($context['flush_mail']))
2848
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
2849
		AddMailQueue(true);
2850
2851
	$do_header = $header === null ? !$header_done : $header;
2852
	if ($do_footer === null)
2853
		$do_footer = $do_header;
2854
2855
	// Has the template/header been done yet?
2856
	if ($do_header)
2857
	{
2858
		// Was the page title set last minute? Also update the HTML safe one.
2859
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
2860
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
2861
2862
		// Start up the session URL fixer.
2863
		ob_start('ob_sessrewrite');
2864
2865
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
2866
			$buffers = explode(',', $settings['output_buffers']);
2867
		elseif (!empty($settings['output_buffers']))
2868
			$buffers = $settings['output_buffers'];
2869
		else
2870
			$buffers = array();
2871
2872
		if (isset($modSettings['integrate_buffer']))
2873
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
2874
2875
		if (!empty($buffers))
2876
			foreach ($buffers as $function)
2877
			{
2878
				$call = call_helper($function, true);
2879
2880
				// Is it valid?
2881
				if (!empty($call))
2882
					ob_start($call);
2883
			}
2884
2885
		// Display the screen in the logical order.
2886
		template_header();
2887
		$header_done = true;
2888
	}
2889
	if ($do_footer)
2890
	{
2891
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
2892
2893
		// Anything special to put out?
2894
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
2895
			echo $context['insert_after_template'];
2896
2897
		// Just so we don't get caught in an endless loop of errors from the footer...
2898
		if (!$footer_done)
2899
		{
2900
			$footer_done = true;
2901
			template_footer();
2902
2903
			// (since this is just debugging... it's okay that it's after </html>.)
2904
			if (!isset($_REQUEST['xml']))
2905
				displayDebug();
2906
		}
2907
	}
2908
2909
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
2910
	if (strpos($_SERVER['REQUEST_URL'], 'action=dlattach') === false && strpos($_SERVER['REQUEST_URL'], 'action=viewsmfile') === false)
2911
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
2912
2913
	// For session check verification.... don't switch browsers...
2914
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
2915
2916
	// Hand off the output to the portal, etc. we're integrated with.
2917
	call_integration_hook('integrate_exit', array($do_footer));
2918
2919
	// Don't exit if we're coming from index.php; that will pass through normally.
2920
	if (!$from_index)
2921
		exit;
2922
}
2923
2924
/**
2925
 * Get the size of a specified image with better error handling.
2926
 * @todo see if it's better in Subs-Graphics, but one step at the time.
2927
 * Uses getimagesize() to determine the size of a file.
2928
 * Attempts to connect to the server first so it won't time out.
2929
 *
2930
 * @param string $url The URL of the image
2931
 * @return array|false The image size as array (width, height), or false on failure
2932
 */
2933
function url_image_size($url)
2934
{
2935
	global $sourcedir;
2936
2937
	// Make sure it is a proper URL.
2938
	$url = str_replace(' ', '%20', $url);
2939
2940
	// Can we pull this from the cache... please please?
2941
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
2942
		return $temp;
2943
	$t = microtime();
2944
2945
	// Get the host to pester...
2946
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
2947
2948
	// Can't figure it out, just try the image size.
2949
	if ($url == '' || $url == 'http://' || $url == 'https://')
2950
	{
2951
		return false;
2952
	}
2953
	elseif (!isset($match[1]))
2954
	{
2955
		$size = @getimagesize($url);
2956
	}
2957
	else
2958
	{
2959
		// Try to connect to the server... give it half a second.
2960
		$temp = 0;
2961
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
2962
2963
		// Successful?  Continue...
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
2964
		if ($fp != false)
2965
		{
2966
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
2967
			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");
2968
2969
			// Read in the HTTP/1.1 or whatever.
2970
			$test = substr(fgets($fp, 11), -1);
2971
			fclose($fp);
2972
2973
			// See if it returned a 404/403 or something.
2974
			if ($test < 4)
2975
			{
2976
				$size = @getimagesize($url);
2977
2978
				// This probably means allow_url_fopen is off, let's try GD.
2979
				if ($size === false && function_exists('imagecreatefromstring'))
2980
				{
2981
					include_once($sourcedir . '/Subs-Package.php');
2982
2983
					// It's going to hate us for doing this, but another request...
2984
					$image = @imagecreatefromstring(fetch_web_data($url));
2985
					if ($image !== false)
2986
					{
2987
						$size = array(imagesx($image), imagesy($image));
2988
						imagedestroy($image);
2989
					}
2990
				}
2991
			}
2992
		}
2993
	}
2994
2995
	// If we didn't get it, we failed.
2996
	if (!isset($size))
2997
		$size = false;
2998
2999
	// If this took a long time, we may never have to do it again, but then again we might...
3000 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...
3001
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3002
3003
	// Didn't work.
3004
	return $size;
3005
}
3006
3007
/**
3008
 * Sets up the basic theme context stuff.
3009
 * @param bool $forceload Whether to load the theme even if it's already loaded
3010
 */
3011
function setupThemeContext($forceload = false)
3012
{
3013
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3014
	global $smcFunc;
3015
	static $loaded = false;
3016
3017
	// Under SSI this function can be called more then once.  That can cause some problems.
3018
	//   So only run the function once unless we are forced to run it again.
3019
	if ($loaded && !$forceload)
3020
		return;
3021
3022
	$loaded = true;
3023
3024
	$context['in_maintenance'] = !empty($maintenance);
3025
	$context['current_time'] = timeformat(time(), false);
3026
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3027
3028
	// Get some news...
3029
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3030
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3031
	{
3032
		if (trim($context['news_lines'][$i]) == '')
3033
			continue;
3034
3035
		// Clean it up for presentation ;).
3036
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3037
	}
3038
	if (!empty($context['news_lines']))
3039
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3040
3041
	if (!$user_info['is_guest'])
3042
	{
3043
		$context['user']['messages'] = &$user_info['messages'];
3044
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3045
		$context['user']['alerts'] = &$user_info['alerts'];
3046
3047
		// Personal message popup...
3048
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3049
			$context['user']['popup_messages'] = true;
3050
		else
3051
			$context['user']['popup_messages'] = false;
3052
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3053
3054
		if (allowedTo('moderate_forum'))
3055
			$context['unapproved_members'] = (!empty($modSettings['registration_method']) && ($modSettings['registration_method'] == 2 || (!empty($modSettings['coppaType']) && $modSettings['coppaType'] == 2))) || !empty($modSettings['approveAccountDeletion']) ? $modSettings['unapprovedMembers'] : 0;
3056
3057
		$context['user']['avatar'] = array();
3058
3059
		// Check for gravatar first since we might be forcing them...
3060
		if (($modSettings['gravatarEnabled'] && substr($user_info['avatar']['url'], 0, 11) == 'gravatar://') || !empty($modSettings['gravatarOverride']))
3061
		{
3062
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3063
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3064
			else
3065
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3066
		}
3067
		// Uploaded?
3068
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3069
			$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';
3070
		// Full URL?
3071
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3072
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3073
		// Otherwise we assume it's server stored.
3074
		elseif ($user_info['avatar']['url'] != '')
3075
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3076
		// No avatar at all? Fine, we have a big fat default avatar ;)
3077
		else
3078
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3079
3080
		if (!empty($context['user']['avatar']))
3081
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3082
3083
		// Figure out how long they've been logged in.
3084
		$context['user']['total_time_logged_in'] = array(
3085
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3086
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3087
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3088
		);
3089
	}
3090
	else
3091
	{
3092
		$context['user']['messages'] = 0;
3093
		$context['user']['unread_messages'] = 0;
3094
		$context['user']['avatar'] = array();
3095
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3096
		$context['user']['popup_messages'] = false;
3097
3098
		if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
3099
			$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
3100
3101
		// If we've upgraded recently, go easy on the passwords.
3102
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3103
			$context['disable_login_hashing'] = true;
3104
	}
3105
3106
	// Setup the main menu items.
3107
	setupMenuContext();
3108
3109
	// This is here because old index templates might still use it.
3110
	$context['show_news'] = !empty($settings['enable_news']);
3111
3112
	// This is done to allow theme authors to customize it as they want.
3113
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3114
3115
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3116
	if ($context['show_pm_popup'])
3117
		addInlineJavaScript('
3118
		jQuery(document).ready(function($) {
3119
			new smc_Popup({
3120
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3121
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3122
				icon_class: \'generic_icons mail_new\'
3123
			});
3124
		});');
3125
3126
	// Add a generic "Are you sure?" confirmation message.
3127
	addInlineJavaScript('
3128
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) .';');
3129
3130
	// Now add the capping code for avatars.
3131
	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')
3132
		addInlineCss('
3133
img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3134
3135
	// This looks weird, but it's because BoardIndex.php references the variable.
3136
	$context['common_stats']['latest_member'] = array(
3137
		'id' => $modSettings['latestMember'],
3138
		'name' => $modSettings['latestRealName'],
3139
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3140
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3141
	);
3142
	$context['common_stats'] = array(
3143
		'total_posts' => comma_format($modSettings['totalMessages']),
3144
		'total_topics' => comma_format($modSettings['totalTopics']),
3145
		'total_members' => comma_format($modSettings['totalMembers']),
3146
		'latest_member' => $context['common_stats']['latest_member'],
3147
	);
3148
	$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']);
3149
3150
	if (empty($settings['theme_version']))
3151
		addJavaScriptVar('smf_scripturl', $scripturl);
3152
3153
	if (!isset($context['page_title']))
3154
		$context['page_title'] = '';
3155
3156
	// Set some specific vars.
3157
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3158
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3159
3160
	// Content related meta tags, including Open Graph
3161
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3162
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3163
3164
	if (!empty($context['meta_keywords']))
3165
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3166
3167
	if (!empty($context['canonical_url']))
3168
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3169
3170
	if (!empty($settings['og_image']))
3171
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3172
3173
	if (!empty($context['meta_description']))
3174
	{
3175
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3176
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3177
	}
3178
	else
3179
	{
3180
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3181
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3182
	}
3183
3184
	call_integration_hook('integrate_theme_context');
3185
}
3186
3187
/**
3188
 * Helper function to set the system memory to a needed value
3189
 * - If the needed memory is greater than current, will attempt to get more
3190
 * - if in_use is set to true, will also try to take the current memory usage in to account
3191
 *
3192
 * @param string $needed The amount of memory to request, if needed, like 256M
3193
 * @param bool $in_use Set to true to account for current memory usage of the script
3194
 * @return boolean True if we have at least the needed memory
3195
 */
3196
function setMemoryLimit($needed, $in_use = false)
3197
{
3198
	// everything in bytes
3199
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3200
	$memory_needed = memoryReturnBytes($needed);
3201
3202
	// should we account for how much is currently being used?
3203
	if ($in_use)
3204
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3205
3206
	// if more is needed, request it
3207
	if ($memory_current < $memory_needed)
3208
	{
3209
		@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...
3210
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3211
	}
3212
3213
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
3214
3215
	// return success or not
3216
	return (bool) ($memory_current >= $memory_needed);
3217
}
3218
3219
/**
3220
 * Helper function to convert memory string settings to bytes
3221
 *
3222
 * @param string $val The byte string, like 256M or 1G
3223
 * @return integer The string converted to a proper integer in bytes
3224
 */
3225
function memoryReturnBytes($val)
3226
{
3227
	if (is_integer($val))
3228
		return $val;
3229
3230
	// Separate the number from the designator
3231
	$val = trim($val);
3232
	$num = intval(substr($val, 0, strlen($val) - 1));
3233
	$last = strtolower(substr($val, -1));
3234
3235
	// convert to bytes
3236
	switch ($last)
3237
	{
3238
		case 'g':
3239
			$num *= 1024;
3240
		case 'm':
3241
			$num *= 1024;
3242
		case 'k':
3243
			$num *= 1024;
3244
	}
3245
	return $num;
3246
}
3247
3248
/**
3249
 * The header template
3250
 */
3251
function template_header()
3252
{
3253
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir;
3254
3255
	setupThemeContext();
3256
3257
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
3258
	if (empty($context['no_last_modified']))
3259
	{
3260
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
3261
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
3262
3263
		// Are we debugging the template/html content?
3264
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
3265
			header('Content-Type: application/xhtml+xml');
3266 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...
3267
			header('Content-Type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3268
	}
3269
3270
	header('Content-Type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3271
3272
	// We need to splice this in after the body layer, or after the main layer for older stuff.
3273
	if ($context['in_maintenance'] && $context['user']['is_admin'])
3274
	{
3275
		$position = array_search('body', $context['template_layers']);
3276
		if ($position === false)
3277
			$position = array_search('main', $context['template_layers']);
3278
3279
		if ($position !== false)
3280
		{
3281
			$before = array_slice($context['template_layers'], 0, $position + 1);
3282
			$after = array_slice($context['template_layers'], $position + 1);
3283
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
3284
		}
3285
	}
3286
3287
	$checked_securityFiles = false;
3288
	$showed_banned = false;
3289
	foreach ($context['template_layers'] as $layer)
3290
	{
3291
		loadSubTemplate($layer . '_above', true);
3292
3293
		// May seem contrived, but this is done in case the body and main layer aren't there...
3294
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
3295
		{
3296
			$checked_securityFiles = true;
3297
3298
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
3299
3300
			// Add your own files.
3301
			call_integration_hook('integrate_security_files', array(&$securityFiles));
3302
3303
			foreach ($securityFiles as $i => $securityFile)
3304
			{
3305
				if (!file_exists($boarddir . '/' . $securityFile))
3306
					unset($securityFiles[$i]);
3307
			}
3308
3309
			// We are already checking so many files...just few more doesn't make any difference! :P
3310
			if (!empty($modSettings['currentAttachmentUploadDir']))
3311
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
3312
3313
			else
3314
				$path = $modSettings['attachmentUploadDir'];
3315
3316
			secureDirectory($path, true);
3317
			secureDirectory($cachedir);
3318
3319
			// If agreement is enabled, at least the english version shall exists
3320
			if ($modSettings['requireAgreement'])
3321
				$agreement = !file_exists($boarddir . '/agreement.txt');
3322
3323
			if (!empty($securityFiles) || (!empty($modSettings['cache_enable']) && !is_writable($cachedir)) || !empty($agreement))
3324
			{
3325
				echo '
3326
		<div class="errorbox">
3327
			<p class="alert">!!</p>
3328
			<h3>', empty($securityFiles) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
3329
			<p>';
3330
3331
				foreach ($securityFiles as $securityFile)
3332
				{
3333
					echo '
3334
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
3335
3336
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
3337
						echo '
3338
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
3339
				}
3340
3341
				if (!empty($modSettings['cache_enable']) && !is_writable($cachedir))
3342
					echo '
3343
				<strong>', $txt['cache_writable'], '</strong><br>';
3344
3345
				if (!empty($agreement))
3346
					echo '
3347
				<strong>', $txt['agreement_missing'], '</strong><br>';
3348
3349
				echo '
3350
			</p>
3351
		</div>';
3352
			}
3353
		}
3354
		// If the user is banned from posting inform them of it.
3355
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
3356
		{
3357
			$showed_banned = true;
3358
			echo '
3359
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
3360
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
3361
3362
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
3363
				echo '
3364
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
3365
3366
			if (!empty($_SESSION['ban']['expire_time']))
3367
				echo '
3368
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
3369
			else
3370
				echo '
3371
					<div>', $txt['your_ban_expires_never'], '</div>';
3372
3373
			echo '
3374
				</div>';
3375
		}
3376
	}
3377
}
3378
3379
/**
3380
 * Show the copyright.
3381
 */
3382
function theme_copyright()
3383
{
3384
	global $forum_copyright, $software_year, $forum_version;
3385
3386
	// Don't display copyright for things like SSI.
3387
	if (!isset($forum_version) || !isset($software_year))
3388
		return;
3389
3390
	// Put in the version...
3391
	printf($forum_copyright, $forum_version, $software_year);
3392
}
3393
3394
/**
3395
 * The template footer
3396
 */
3397
function template_footer()
3398
{
3399
	global $context, $modSettings, $time_start, $db_count;
3400
3401
	// Show the load time?  (only makes sense for the footer.)
3402
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
3403
	$context['load_time'] = comma_format(round(array_sum(explode(' ', microtime())) - array_sum(explode(' ', $time_start)), 3));
3404
	$context['load_queries'] = $db_count;
3405
3406
	foreach (array_reverse($context['template_layers']) as $layer)
3407
		loadSubTemplate($layer . '_below', true);
3408
}
3409
3410
/**
3411
 * Output the Javascript files
3412
 * 	- tabbing in this function is to make the HTML source look good proper
3413
 *  - if defered is set function will output all JS (source & inline) set to load at page end
3414
 *
3415
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
3416
 */
3417
function template_javascript($do_deferred = false)
3418
{
3419
	global $context, $modSettings, $settings;
3420
3421
	// Use this hook to minify/optimize Javascript files and vars
3422
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
3423
3424
	$toMinify = array();
3425
	$toMinifyDefer = array();
3426
3427
	// Ouput the declared Javascript variables.
3428
	if (!empty($context['javascript_vars']) && !$do_deferred)
3429
	{
3430
		echo '
3431
	<script>';
3432
3433
		foreach ($context['javascript_vars'] as $key => $value)
3434
		{
3435
			if (empty($value))
3436
			{
3437
				echo '
3438
		var ', $key, ';';
3439
			}
3440
			else
3441
			{
3442
				echo '
3443
		var ', $key, ' = ', $value, ';';
3444
			}
3445
		}
3446
3447
		echo '
3448
	</script>';
3449
	}
3450
3451
	// While we have JavaScript files to place in the template.
3452
	foreach ($context['javascript_files'] as $id => $js_file)
3453
	{
3454
		// Last minute call! allow theme authors to disable single files.
3455
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
3456
			continue;
3457
3458
		// By default all files don't get minimized unless the file explicitly says so!
3459
		if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
3460
		{
3461
			if ($do_deferred && !empty($js_file['options']['defer']))
3462
				$toMinifyDefer[] = $js_file;
3463
3464
			elseif (!$do_deferred && empty($js_file['options']['defer']))
3465
				$toMinify[] = $js_file;
3466
3467
			// Grab a random seed.
3468
			if (!isset($minSeed))
3469
				$minSeed = $js_file['options']['seed'];
3470
		}
3471
3472
		elseif ((!$do_deferred && empty($js_file['options']['defer'])) || ($do_deferred && !empty($js_file['options']['defer'])))
3473
			echo '
3474
	<script src="', $js_file['fileUrl'], '"', !empty($js_file['options']['async']) ? ' async="async"' : '', '></script>';
3475
	}
3476
3477
	if ((!$do_deferred && !empty($toMinify)) || ($do_deferred && !empty($toMinifyDefer)))
3478
	{
3479
		$result = custMinify(($do_deferred ? $toMinifyDefer : $toMinify), 'js', $do_deferred);
3480
3481
		// Minify process couldn't work, print each individual files.
3482
		if (!empty($result) && is_array($result))
3483
			foreach ($result as $minFailedFile)
3484
				echo '
3485
	<script src="', $minFailedFile['fileUrl'], '"', !empty($minFailedFile['options']['async']) ? ' async="async"' : '', '></script>';
3486
3487
		else
3488
			echo '
3489
	<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...
3490
	}
3491
3492
	// Inline JavaScript - Actually useful some times!
3493
	if (!empty($context['javascript_inline']))
3494
	{
3495 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...
3496
		{
3497
			echo '
3498
<script>';
3499
3500
			foreach ($context['javascript_inline']['defer'] as $js_code)
3501
				echo $js_code;
3502
3503
			echo '
3504
</script>';
3505
		}
3506
3507 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...
3508
		{
3509
			echo '
3510
	<script>';
3511
3512
			foreach ($context['javascript_inline']['standard'] as $js_code)
3513
				echo $js_code;
3514
3515
			echo '
3516
	</script>';
3517
		}
3518
	}
3519
}
3520
3521
/**
3522
 * Output the CSS files
3523
 *
3524
 */
3525
function template_css()
3526
{
3527
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
3528
3529
	// Use this hook to minify/optimize CSS files
3530
	call_integration_hook('integrate_pre_css_output');
3531
3532
	$toMinify = array();
3533
	$normal = array();
3534
3535
	foreach ($context['css_files'] as $id => $file)
3536
	{
3537
		// Last minute call! allow theme authors to disable single files.
3538
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
3539
			continue;
3540
3541
		// By default all files don't get minimized unless the file explicitly says so!
3542
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']))
3543
		{
3544
			$toMinify[] = $file;
3545
3546
			// Grab a random seed.
3547
			if (!isset($minSeed))
3548
				$minSeed = $file['options']['seed'];
3549
		}
3550
3551
		else
3552
			$normal[] = $file['fileUrl'];
3553
	}
3554
3555
	if (!empty($toMinify))
3556
	{
3557
		$result = custMinify($toMinify, 'css');
3558
3559
		// Minify process couldn't work, print each individual files.
3560
		if (!empty($result) && is_array($result))
3561
			foreach ($result as $minFailedFile)
3562
				echo '
3563
	<link rel="stylesheet" href="', $minFailedFile['fileUrl'], '">';
3564
3565
		else
3566
			echo '
3567
	<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...
3568
	}
3569
3570
	// Print the rest after the minified files.
3571
	if (!empty($normal))
3572
		foreach ($normal as $nf)
3573
			echo '
3574
	<link rel="stylesheet" href="', $nf ,'">';
3575
3576
	if ($db_show_debug === true)
3577
	{
3578
		// Try to keep only what's useful.
3579
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
3580
		foreach ($context['css_files'] as $file)
3581
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
3582
	}
3583
3584
	if (!empty($context['css_header']))
3585
	{
3586
		echo '
3587
	<style>';
3588
3589
		foreach ($context['css_header'] as $css)
3590
			echo $css .'
3591
	';
3592
3593
		echo'
3594
	</style>';
3595
	}
3596
}
3597
3598
/**
3599
 * Get an array of previously defined files and adds them to our main minified file.
3600
 * Sets a one day cache to avoid re-creating a file on every request.
3601
 *
3602
 * @param array $data The files to minify.
3603
 * @param string $type either css or js.
3604
 * @param bool $do_deferred use for type js to indicate if the minified file will be deferred, IE, put at the closing </body> tag.
3605
 * @return bool|array If an array the minify process failed and the data is returned intact.
3606
 */
3607
function custMinify($data, $type, $do_deferred = false)
3608
{
3609
	global $sourcedir, $settings, $txt;
3610
3611
	$types = array('css', 'js');
3612
	$type = !empty($type) && in_array($type, $types) ? $type : false;
3613
	$data = !empty($data) ? $data : false;
3614
3615
	if (empty($type) || empty($data))
3616
		return false;
3617
3618
	// Did we already did this?
3619
	$toCache = cache_get_data('minimized_'. $settings['theme_id'] .'_'. $type, 86400);
3620
3621
	// Already done?
3622
	if (!empty($toCache))
3623
		return true;
3624
3625
	// No namespaces, sorry!
3626
	$classType = 'MatthiasMullie\\Minify\\'. strtoupper($type);
3627
3628
	// Temp path.
3629
	$cTempPath = $settings['theme_dir'] .'/'. ($type == 'css' ? 'css' : 'scripts') .'/';
3630
3631
	// What kind of file are we going to create?
3632
	$toCreate = $cTempPath .'minified'. ($do_deferred ? '_deferred' : '') .'.'. $type;
3633
3634
	// File has to exists, if it isn't try to create it.
3635
	if ((!file_exists($toCreate) && @fopen($toCreate, 'w') === false) || !smf_chmod($toCreate))
3636
	{
3637
		loadLanguage('Errors');
3638
		log_error(sprintf($txt['file_not_created'], $toCreate), 'general');
3639
		cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, null);
3640
3641
		// The process failed so roll back to print each individual file.
3642
		return $data;
3643
	}
3644
3645
	$minifier = new $classType();
3646
3647
	foreach ($data as $file)
3648
	{
3649
		$tempFile = str_replace($file['options']['seed'], '', $file['filePath']);
3650
		$toAdd = file_exists($tempFile) ? $tempFile : false;
3651
3652
		// The file couldn't be located so it won't be added, log this error.
3653
		if (empty($toAdd))
3654
		{
3655
			loadLanguage('Errors');
3656
			log_error(sprintf($txt['file_minimize_fail'], $file['fileName']), 'general');
3657
			continue;
3658
		}
3659
3660
		// Add this file to the list.
3661
		$minifier->add($toAdd);
3662
	}
3663
3664
	// Create the file.
3665
	$minifier->minify($toCreate);
3666
	unset($minifier);
3667
	clearstatcache();
3668
3669
	// Minify process failed.
3670
	if (!filesize($toCreate))
3671
	{
3672
		loadLanguage('Errors');
3673
		log_error(sprintf($txt['file_not_created'], $toCreate), 'general');
3674
		cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, null);
3675
3676
		// The process failed so roll back to print each individual file.
3677
		return $data;
3678
	}
3679
3680
	// And create a long lived cache entry.
3681
	cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, $toCreate, 86400);
3682
3683
	return true;
3684
}
3685
3686
/**
3687
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
3688
 * @todo this currently returns the hash if new, and the full filename otherwise.
3689
 * Something messy like that.
3690
 * @todo and of course everything relies on this behavior and work around it. :P.
3691
 * Converters included.
3692
 *
3693
 * @param string $filename The name of the file
3694
 * @param int $attachment_id The ID of the attachment
3695
 * @param string $dir Which directory it should be in (null to use current one)
3696
 * @param bool $new Whether this is a new attachment
3697
 * @param string $file_hash The file hash
3698
 * @return string The path to the file
3699
 */
3700
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
3701
{
3702
	global $modSettings, $smcFunc;
3703
3704
	// Just make up a nice hash...
3705
	if ($new)
3706
		return sha1(md5($filename . time()) . mt_rand());
3707
3708
	// Just make sure that attachment id is only a int
3709
	$attachment_id = (int) $attachment_id;
3710
3711
	// Grab the file hash if it wasn't added.
3712
	// Left this for legacy.
3713
	if ($file_hash === '')
3714
	{
3715
		$request = $smcFunc['db_query']('', '
3716
			SELECT file_hash
3717
			FROM {db_prefix}attachments
3718
			WHERE id_attach = {int:id_attach}',
3719
			array(
3720
				'id_attach' => $attachment_id,
3721
			));
3722
3723
		if ($smcFunc['db_num_rows']($request) === 0)
3724
			return false;
3725
3726
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
3727
		$smcFunc['db_free_result']($request);
3728
	}
3729
3730
	// Still no hash? mmm...
3731
	if (empty($file_hash))
3732
		$file_hash = sha1(md5($filename . time()) . mt_rand());
3733
3734
	// Are we using multiple directories?
3735
	if (is_array($modSettings['attachmentUploadDir']))
3736
		$path = $modSettings['attachmentUploadDir'][$dir];
3737
3738
	else
3739
		$path = $modSettings['attachmentUploadDir'];
3740
3741
	return $path . '/' . $attachment_id . '_' . $file_hash .'.dat';
3742
}
3743
3744
/**
3745
 * Convert a single IP to a ranged IP.
3746
 * internal function used to convert a user-readable format to a format suitable for the database.
3747
 *
3748
 * @param string $fullip The full IP
3749
 * @return array An array of IP parts
3750
 */
3751
function ip2range($fullip)
3752
{
3753
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
3754
	if ($fullip == 'unknown')
3755
		$fullip = '255.255.255.255';
3756
3757
	$ip_parts = explode('-', $fullip);
3758
	$ip_array = array();
3759
3760
	// if ip 22.12.31.21
3761
	if (count($ip_parts) == 1 && isValidIP($fullip))
3762
	{
3763
		$ip_array['low'] = $fullip;
3764
		$ip_array['high'] = $fullip;
3765
		return $ip_array;
3766
	} // if ip 22.12.* -> 22.12.* - 22.12.*
3767
	elseif (count($ip_parts) == 1)
3768
	{
3769
		$ip_parts[0] = $fullip;
3770
		$ip_parts[1] = $fullip;
3771
	}
3772
3773
	// if ip 22.12.31.21-12.21.31.21
3774
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
3775
	{
3776
		$ip_array['low'] = $ip_parts[0];
3777
		$ip_array['high'] = $ip_parts[1];
3778
		return $ip_array;
3779
	}
3780
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
3781
	{
3782
		$valid_low = isValidIP($ip_parts[0]);
3783
		$valid_high = isValidIP($ip_parts[1]);
3784
		$count = 0;
3785
		$mode = (preg_match('/:/',$ip_parts[0]) > 0 ? ':' : '.');
3786
		$max = ($mode == ':' ? 'ffff' : '255');
3787
		$min = 0;
3788 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...
3789
		{
3790
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
3791
			$valid_low = isValidIP($ip_parts[0]);
3792
			while (!$valid_low)
3793
			{
3794
				$ip_parts[0] .= $mode . $min;
3795
				$valid_low = isValidIP($ip_parts[0]);
3796
				$count++;
3797
				if ($count > 9) break;
3798
			}
3799
		}
3800
3801
		$count = 0;
3802 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...
3803
		{
3804
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
3805
			$valid_high = isValidIP($ip_parts[1]);
3806
			while (!$valid_high)
3807
			{
3808
				$ip_parts[1] .= $mode . $max;
3809
				$valid_high = isValidIP($ip_parts[1]);
3810
				$count++;
3811
				if ($count > 9) break;
3812
			}
3813
		}
3814
3815
		if($valid_high && $valid_low)
3816
		{
3817
			$ip_array['low'] = $ip_parts[0];
3818
			$ip_array['high'] = $ip_parts[1];
3819
		}
3820
3821
	}
3822
3823
	return $ip_array;
3824
}
3825
3826
/**
3827
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
3828
 *
3829
 * @param string $ip The IP to get the hostname from
3830
 * @return string The hostname
3831
 */
3832
function host_from_ip($ip)
3833
{
3834
	global $modSettings;
3835
3836
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
3837
		return $host;
3838
	$t = microtime();
3839
3840
	// Try the Linux host command, perhaps?
3841
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
3842
	{
3843
		if (!isset($modSettings['host_to_dis']))
3844
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
3845
		else
3846
			$test = @shell_exec('host ' . @escapeshellarg($ip));
3847
3848
		// Did host say it didn't find anything?
3849
		if (strpos($test, 'not found') !== false)
3850
			$host = '';
3851
		// Invalid server option?
3852
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
3853
			updateSettings(array('host_to_dis' => 1));
3854
		// Maybe it found something, after all?
3855
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
3856
			$host = $match[1];
3857
	}
3858
3859
	// This is nslookup; usually only Windows, but possibly some Unix?
3860
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
3861
	{
3862
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
3863
		if (strpos($test, 'Non-existent domain') !== false)
3864
			$host = '';
3865
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
3866
			$host = $match[1];
3867
	}
3868
3869
	// This is the last try :/.
3870
	if (!isset($host) || $host === false)
3871
		$host = @gethostbyaddr($ip);
3872
3873
	// It took a long time, so let's cache it!
3874 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...
3875
		cache_put_data('hostlookup-' . $ip, $host, 600);
3876
3877
	return $host;
3878
}
3879
3880
/**
3881
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
3882
 *
3883
 * @param string $text The text to split into words
3884
 * @param int $max_chars The maximum number of characters per word
3885
 * @param bool $encrypt Whether to encrypt the results
3886
 * @return array An array of ints or words depending on $encrypt
3887
 */
3888
function text2words($text, $max_chars = 20, $encrypt = false)
3889
{
3890
	global $smcFunc, $context;
3891
3892
	// Step 1: Remove entities/things we don't consider words:
3893
	$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>' => ' ')));
3894
3895
	// Step 2: Entities we left to letters, where applicable, lowercase.
3896
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
3897
3898
	// Step 3: Ready to split apart and index!
3899
	$words = explode(' ', $words);
3900
3901
	if ($encrypt)
3902
	{
3903
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
3904
		$returned_ints = array();
3905
		foreach ($words as $word)
3906
		{
3907
			if (($word = trim($word, '-_\'')) !== '')
3908
			{
3909
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
3910
				$total = 0;
3911
				for ($i = 0; $i < $max_chars; $i++)
3912
					$total += $possible_chars[ord($encrypted{$i})] * pow(63, $i);
3913
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
3914
			}
3915
		}
3916
		return array_unique($returned_ints);
3917
	}
3918
	else
3919
	{
3920
		// Trim characters before and after and add slashes for database insertion.
3921
		$returned_words = array();
3922
		foreach ($words as $word)
3923
			if (($word = trim($word, '-_\'')) !== '')
3924
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
3925
3926
		// Filter out all words that occur more than once.
3927
		return array_unique($returned_words);
3928
	}
3929
}
3930
3931
/**
3932
 * Creates an image/text button
3933
 *
3934
 * @param string $name The name of the button (should be a generic_icons class or the name of an image)
3935
 * @param string $alt The alt text
3936
 * @param string $label The $txt string to use as the label
3937
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
3938
 * @param boolean $force_use Whether to force use of this when template_create_button is available
3939
 * @return string The HTML to display the button
3940
 */
3941
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
3942
{
3943
	global $settings, $txt;
3944
3945
	// Does the current loaded theme have this and we are not forcing the usage of this function?
3946
	if (function_exists('template_create_button') && !$force_use)
3947
		return template_create_button($name, $alt, $label = '', $custom = '');
3948
3949
	if (!$settings['use_image_buttons'])
3950
		return $txt[$alt];
3951
	elseif (!empty($settings['use_buttons']))
3952
		return '<span class="generic_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
3953
	else
3954
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
3955
}
3956
3957
/**
3958
 * Sets up all of the top menu buttons
3959
 * Saves them in the cache if it is available and on
3960
 * Places the results in $context
3961
 *
3962
 */
3963
function setupMenuContext()
3964
{
3965
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings;
3966
3967
	// Set up the menu privileges.
3968
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
3969
	$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'));
3970
3971
	$context['allow_memberlist'] = allowedTo('view_mlist');
3972
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
3973
	$context['allow_moderation_center'] = $context['user']['can_mod'];
3974
	$context['allow_pm'] = allowedTo('pm_read');
3975
3976
	$cacheTime = $modSettings['lastActive'] * 60;
3977
3978
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
3979
	if (!isset($context['allow_calendar_event']))
3980
	{
3981
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
3982
3983
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
3984 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...
3985
		{
3986
			$boards_can_post = boardsAllowedTo('post_new');
3987
			$context['allow_calendar_event'] &= !empty($boards_can_post);
3988
		}
3989
	}
3990
3991
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
3992
	if (!$context['user']['is_guest'])
3993
	{
3994
		addInlineJavaScript('
3995
	var user_menus = new smc_PopupMenu();
3996
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
3997
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u='. $context['user']['id'] .'");', true);
3998
		if ($context['allow_pm'])
3999
			addInlineJavaScript('
4000
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
4001
4002
		if (!empty($modSettings['enable_ajax_alerts']))
4003
		{
4004
			require_once($sourcedir . '/Subs-Notify.php');
4005
4006
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
4007
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
4008
4009
			addInlineJavaScript('
4010
	var new_alert_title = "' . $context['forum_name'] . '";
4011
	var alert_timeout = ' . $timeout . ';');
4012
			loadJavaScriptFile('alerts.js', array(), 'smf_alerts');
4013
		}
4014
	}
4015
4016
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
4017
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
4018
	{
4019
		$buttons = array(
4020
			'home' => array(
4021
				'title' => $txt['home'],
4022
				'href' => $scripturl,
4023
				'show' => true,
4024
				'sub_buttons' => array(
4025
				),
4026
				'is_last' => $context['right_to_left'],
4027
			),
4028
			'search' => array(
4029
				'title' => $txt['search'],
4030
				'href' => $scripturl . '?action=search',
4031
				'show' => $context['allow_search'],
4032
				'sub_buttons' => array(
4033
				),
4034
			),
4035
			'admin' => array(
4036
				'title' => $txt['admin'],
4037
				'href' => $scripturl . '?action=admin',
4038
				'show' => $context['allow_admin'],
4039
				'sub_buttons' => array(
4040
					'featuresettings' => array(
4041
						'title' => $txt['modSettings_title'],
4042
						'href' => $scripturl . '?action=admin;area=featuresettings',
4043
						'show' => allowedTo('admin_forum'),
4044
					),
4045
					'packages' => array(
4046
						'title' => $txt['package'],
4047
						'href' => $scripturl . '?action=admin;area=packages',
4048
						'show' => allowedTo('admin_forum'),
4049
					),
4050
					'errorlog' => array(
4051
						'title' => $txt['errlog'],
4052
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
4053
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
4054
					),
4055
					'permissions' => array(
4056
						'title' => $txt['edit_permissions'],
4057
						'href' => $scripturl . '?action=admin;area=permissions',
4058
						'show' => allowedTo('manage_permissions'),
4059
					),
4060
					'memberapprove' => array(
4061
						'title' => $txt['approve_members_waiting'],
4062
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
4063
						'show' => !empty($context['unapproved_members']),
4064
						'is_last' => true,
4065
					),
4066
				),
4067
			),
4068
			'moderate' => array(
4069
				'title' => $txt['moderate'],
4070
				'href' => $scripturl . '?action=moderate',
4071
				'show' => $context['allow_moderation_center'],
4072
				'sub_buttons' => array(
4073
					'modlog' => array(
4074
						'title' => $txt['modlog_view'],
4075
						'href' => $scripturl . '?action=moderate;area=modlog',
4076
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4077
					),
4078
					'poststopics' => array(
4079
						'title' => $txt['mc_unapproved_poststopics'],
4080
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
4081
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4082
					),
4083
					'attachments' => array(
4084
						'title' => $txt['mc_unapproved_attachments'],
4085
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
4086
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4087
					),
4088
					'reports' => array(
4089
						'title' => $txt['mc_reported_posts'],
4090
						'href' => $scripturl . '?action=moderate;area=reportedposts',
4091
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4092
					),
4093
					'reported_members' => array(
4094
						'title' => $txt['mc_reported_members'],
4095
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
4096
						'show' => allowedTo('moderate_forum'),
4097
						'is_last' => true,
4098
					)
4099
				),
4100
			),
4101
			'calendar' => array(
4102
				'title' => $txt['calendar'],
4103
				'href' => $scripturl . '?action=calendar',
4104
				'show' => $context['allow_calendar'],
4105
				'sub_buttons' => array(
4106
					'view' => array(
4107
						'title' => $txt['calendar_menu'],
4108
						'href' => $scripturl . '?action=calendar',
4109
						'show' => $context['allow_calendar_event'],
4110
					),
4111
					'post' => array(
4112
						'title' => $txt['calendar_post_event'],
4113
						'href' => $scripturl . '?action=calendar;sa=post',
4114
						'show' => $context['allow_calendar_event'],
4115
						'is_last' => true,
4116
					),
4117
				),
4118
			),
4119
			'mlist' => array(
4120
				'title' => $txt['members_title'],
4121
				'href' => $scripturl . '?action=mlist',
4122
				'show' => $context['allow_memberlist'],
4123
				'sub_buttons' => array(
4124
					'mlist_view' => array(
4125
						'title' => $txt['mlist_menu_view'],
4126
						'href' => $scripturl . '?action=mlist',
4127
						'show' => true,
4128
					),
4129
					'mlist_search' => array(
4130
						'title' => $txt['mlist_search'],
4131
						'href' => $scripturl . '?action=mlist;sa=search',
4132
						'show' => true,
4133
						'is_last' => true,
4134
					),
4135
				),
4136
			),
4137
			'signup' => array(
4138
				'title' => $txt['register'],
4139
				'href' => $scripturl . '?action=signup',
4140
				'show' => $user_info['is_guest'] && $context['can_register'],
4141
				'sub_buttons' => array(
4142
				),
4143
				'is_last' => !$context['right_to_left'],
4144
			),
4145
			'logout' => array(
4146
				'title' => $txt['logout'],
4147
				'href' => $scripturl . '?action=logout;%1$s=%2$s',
4148
				'show' => !$user_info['is_guest'],
4149
				'sub_buttons' => array(
4150
				),
4151
				'is_last' => !$context['right_to_left'],
4152
			),
4153
		);
4154
4155
		// Allow editing menu buttons easily.
4156
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
4157
4158
		// Now we put the buttons in the context so the theme can use them.
4159
		$menu_buttons = array();
4160
		foreach ($buttons as $act => $button)
4161
			if (!empty($button['show']))
4162
			{
4163
				$button['active_button'] = false;
4164
4165
				// This button needs some action.
4166
				if (isset($button['action_hook']))
4167
					$needs_action_hook = true;
4168
4169
				// Make sure the last button truly is the last button.
4170
				if (!empty($button['is_last']))
4171
				{
4172
					if (isset($last_button))
4173
						unset($menu_buttons[$last_button]['is_last']);
4174
					$last_button = $act;
4175
				}
4176
4177
				// Go through the sub buttons if there are any.
4178
				if (!empty($button['sub_buttons']))
4179
					foreach ($button['sub_buttons'] as $key => $subbutton)
4180
					{
4181
						if (empty($subbutton['show']))
4182
							unset($button['sub_buttons'][$key]);
4183
4184
						// 2nd level sub buttons next...
4185
						if (!empty($subbutton['sub_buttons']))
4186
						{
4187
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
4188
							{
4189
								if (empty($sub_button2['show']))
4190
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
4191
							}
4192
						}
4193
					}
4194
4195
				// Does this button have its own icon?
4196
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
4197
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
4198
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
4199
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
4200
				elseif (isset($button['icon']))
4201
					$button['icon'] = '<span class="generic_icons ' . $button['icon'] . '"></span>';
4202
				else
4203
					$button['icon'] = '<span class="generic_icons ' . $act . '"></span>';
4204
4205
				$menu_buttons[$act] = $button;
4206
			}
4207
4208
		if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2)
4209
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
4210
	}
4211
4212
	$context['menu_buttons'] = $menu_buttons;
4213
4214
	// Logging out requires the session id in the url.
4215
	if (isset($context['menu_buttons']['logout']))
4216
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
4217
4218
	// Figure out which action we are doing so we can set the active tab.
4219
	// Default to home.
4220
	$current_action = 'home';
4221
4222
	if (isset($context['menu_buttons'][$context['current_action']]))
4223
		$current_action = $context['current_action'];
4224
	elseif ($context['current_action'] == 'search2')
4225
		$current_action = 'search';
4226
	elseif ($context['current_action'] == 'theme')
4227
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
4228
	elseif ($context['current_action'] == 'register2')
4229
		$current_action = 'register';
4230
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
4231
		$current_action = 'login';
4232
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
4233
		$current_action = 'moderate';
4234
4235
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
4236
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
4237
	{
4238
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
4239
		$context[$current_action] = true;
4240
	}
4241
	elseif ($context['current_action'] == 'pm')
4242
	{
4243
		$current_action = 'self_pm';
4244
		$context['self_pm'] = true;
4245
	}
4246
4247
	$total_mod_reports = 0;
4248
4249
	if (!empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1' && !empty($context['open_mod_reports']))
4250
	{
4251
		$total_mod_reports = $context['open_mod_reports'];
4252
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['title'] .= ' <span class="amt">' . $context['open_mod_reports'] . '</span>';
4253
	}
4254
4255
	// Show how many errors there are
4256 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...
4257
	{
4258
		$context['menu_buttons']['admin']['title'] .= ' <span class="amt">' . $context['num_errors'] . '</span>';
4259
		$context['menu_buttons']['admin']['sub_buttons']['errorlog']['title'] .= ' <span class="amt">' . $context['num_errors'] . '</span>';
4260
	}
4261
4262
	/**
4263
	 * @todo For some reason, $context['open_member_reports'] isn't getting set
4264
	 */
4265
	if (!empty($context['open_member_reports']) && allowedTo('moderate_forum'))
4266
	{
4267
		$total_mod_reports += $context['open_member_reports'];
4268
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['title'] .= ' <span class="amt">' . $context['open_member_reports'] . '</span>';
4269
	}
4270
4271 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...
4272
	{
4273
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['title'] .= ' <span class="amt">' . $context['unapproved_members'] . '</span>';
4274
		$context['menu_buttons']['admin']['title'] .= ' <span class="amt">' . $context['unapproved_members'] . '</span>';
4275
	}
4276
4277
	// Do we have any open reports?
4278
	if ($total_mod_reports > 0)
4279
	{
4280
		$context['menu_buttons']['moderate']['title'] .= ' <span class="amt">' . $total_mod_reports . '</span>';
4281
	}
4282
4283
	// Not all actions are simple.
4284
	if (!empty($needs_action_hook))
4285
		call_integration_hook('integrate_current_action', array(&$current_action));
4286
4287
	if (isset($context['menu_buttons'][$current_action]))
4288
		$context['menu_buttons'][$current_action]['active_button'] = true;
4289
}
4290
4291
/**
4292
 * Generate a random seed and ensure it's stored in settings.
4293
 */
4294
function smf_seed_generator()
4295
{
4296
	updateSettings(array('rand_seed' => microtime() * 1000000));
4297
}
4298
4299
/**
4300
 * Process functions of an integration hook.
4301
 * calls all functions of the given hook.
4302
 * supports static class method calls.
4303
 *
4304
 * @param string $hook The hook name
4305
 * @param array $parameters An array of parameters this hook implements
4306
 * @return array The results of the functions
4307
 */
4308
function call_integration_hook($hook, $parameters = array())
4309
{
4310
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
4311
	global $context, $txt;
4312
4313
	if ($db_show_debug === true)
4314
		$context['debug']['hooks'][] = $hook;
4315
4316
	// Need to have some control.
4317
	if (!isset($context['instances']))
4318
		$context['instances'] = array();
4319
4320
	$results = array();
4321
	if (empty($modSettings[$hook]))
4322
		return $results;
4323
4324
	// Define some needed vars.
4325
	$function = false;
4326
4327
	$functions = explode(',', $modSettings[$hook]);
4328
	// Loop through each function.
4329
	foreach ($functions as $function)
4330
	{
4331
		// Hook has been marked as "disabled". Skip it!
4332
		if (strpos($function, '!') !== false)
4333
			continue;
4334
4335
		$call = call_helper($function, true);
4336
4337
		// Is it valid?
4338
		if (!empty($call))
4339
			$results[$function] = call_user_func_array($call, $parameters);
4340
4341
		// Whatever it was suppose to call, it failed :(
4342
		elseif (!empty($function))
4343
		{
4344
			loadLanguage('Errors');
4345
4346
			// Get a full path to show on error.
4347
			if (strpos($function, '|') !== false)
4348
			{
4349
				list ($file, $string) = explode('|', $function);
4350
				$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'])));
4351
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
4352
			}
4353
4354
			// "Assume" the file resides on $boarddir somewhere...
4355
			else
4356
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
4357
		}
4358
	}
4359
4360
	return $results;
4361
}
4362
4363
/**
4364
 * Add a function for integration hook.
4365
 * does nothing if the function is already added.
4366
 *
4367
 * @param string $hook The complete hook name.
4368
 * @param string $function The function name. Can be a call to a method via Class::method.
4369
 * @param bool $permanent If true, updates the value in settings table.
4370
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4371
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4372
 */
4373
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
4374
{
4375
	global $smcFunc, $modSettings;
4376
4377
	// Any objects?
4378
	if ($object)
4379
		$function = $function . '#';
4380
4381
	// Any files  to load?
4382
	if (!empty($file) && is_string($file))
4383
		$function = $file . (!empty($function) ? '|' . $function : '');
4384
4385
	// Get the correct string.
4386
	$integration_call = $function;
4387
4388
	// Is it going to be permanent?
4389
	if ($permanent)
4390
	{
4391
		$request = $smcFunc['db_query']('', '
4392
			SELECT value
4393
			FROM {db_prefix}settings
4394
			WHERE variable = {string:variable}',
4395
			array(
4396
				'variable' => $hook,
4397
			)
4398
		);
4399
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
4400
		$smcFunc['db_free_result']($request);
4401
4402
		if (!empty($current_functions))
4403
		{
4404
			$current_functions = explode(',', $current_functions);
4405
			if (in_array($integration_call, $current_functions))
4406
				return;
4407
4408
			$permanent_functions = array_merge($current_functions, array($integration_call));
4409
		}
4410
		else
4411
			$permanent_functions = array($integration_call);
4412
4413
		updateSettings(array($hook => implode(',', $permanent_functions)));
4414
	}
4415
4416
	// Make current function list usable.
4417
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4418
4419
	// Do nothing, if it's already there.
4420
	if (in_array($integration_call, $functions))
4421
		return;
4422
4423
	$functions[] = $integration_call;
4424
	$modSettings[$hook] = implode(',', $functions);
4425
}
4426
4427
/**
4428
 * Remove an integration hook function.
4429
 * Removes the given function from the given hook.
4430
 * Does nothing if the function is not available.
4431
 *
4432
 * @param string $hook The complete hook name.
4433
 * @param string $function The function name. Can be a call to a method via Class::method.
4434
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
4435
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4436
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4437
 * @see add_integration_function
4438
 */
4439
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...
4440
{
4441
	global $smcFunc, $modSettings;
4442
4443
	// Any objects?
4444
	if ($object)
4445
		$function = $function . '#';
4446
4447
	// Any files  to load?
4448
	if (!empty($file) && is_string($file))
4449
		$function = $file . '|' . $function;
4450
4451
	// Get the correct string.
4452
	$integration_call = $function;
4453
4454
	// Get the permanent functions.
4455
	$request = $smcFunc['db_query']('', '
4456
		SELECT value
4457
		FROM {db_prefix}settings
4458
		WHERE variable = {string:variable}',
4459
		array(
4460
			'variable' => $hook,
4461
		)
4462
	);
4463
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
4464
	$smcFunc['db_free_result']($request);
4465
4466
	if (!empty($current_functions))
4467
	{
4468
		$current_functions = explode(',', $current_functions);
4469
4470
		if (in_array($integration_call, $current_functions))
4471
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
4472
	}
4473
4474
	// Turn the function list into something usable.
4475
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4476
4477
	// You can only remove it if it's available.
4478
	if (!in_array($integration_call, $functions))
4479
		return;
4480
4481
	$functions = array_diff($functions, array($integration_call));
4482
	$modSettings[$hook] = implode(',', $functions);
4483
}
4484
4485
/**
4486
 * Receives a string and tries to figure it out if its a method or a function.
4487
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
4488
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
4489
 * Prepare and returns a callable depending on the type of method/function found.
4490
 *
4491
 * @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)
4492
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
4493
 * @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.
4494
 */
4495
function call_helper($string, $return = false)
4496
{
4497
	global $context, $smcFunc, $txt, $db_show_debug;
4498
4499
	// Really?
4500
	if (empty($string))
4501
		return false;
4502
4503
	// An array? should be a "callable" array IE array(object/class, valid_callable).
4504
	// A closure? should be a callable one.
4505
	if (is_array($string) || $string instanceof Closure)
4506
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
4507
4508
	// No full objects, sorry! pass a method or a property instead!
4509
	if (is_object($string))
4510
		return false;
4511
4512
	// Stay vitaminized my friends...
4513
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
4514
4515
	// Is there a file to load?
4516
	$string = load_file($string);
4517
4518
	// Loaded file failed
4519
	if (empty($string))
4520
		return false;
4521
4522
	// Found a method.
4523
	if (strpos($string, '::') !== false)
4524
	{
4525
		list ($class, $method) = explode('::', $string);
4526
4527
		// Check if a new object will be created.
4528
		if (strpos($method, '#') !== false)
4529
		{
4530
			// Need to remove the # thing.
4531
			$method = str_replace('#', '', $method);
4532
4533
			// Don't need to create a new instance for every method.
4534
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
4535
			{
4536
				$context['instances'][$class] = new $class;
4537
4538
				// Add another one to the list.
4539
				if ($db_show_debug === true)
4540
				{
4541
					if (!isset($context['debug']['instances']))
4542
						$context['debug']['instances'] = array();
4543
4544
					$context['debug']['instances'][$class] = $class;
4545
				}
4546
			}
4547
4548
			$func = array($context['instances'][$class], $method);
4549
		}
4550
4551
		// Right then. This is a call to a static method.
4552
		else
4553
			$func = array($class, $method);
4554
	}
4555
4556
	// Nope! just a plain regular function.
4557
	else
4558
		$func = $string;
4559
4560
	// Right, we got what we need, time to do some checks.
4561
	if (!is_callable($func, false, $callable_name))
4562
	{
4563
		loadLanguage('Errors');
4564
		log_error(sprintf($txt['subAction_fail'], $callable_name), 'general');
4565
4566
		// Gotta tell everybody.
4567
		return false;
4568
	}
4569
4570
	// Everything went better than expected.
4571
	else
4572
	{
4573
		// What are we gonna do about it?
4574
		if ($return)
4575
			return $func;
4576
4577
		// If this is a plain function, avoid the heat of calling call_user_func().
4578
		else
4579
		{
4580
			if (is_array($func))
4581
				call_user_func($func);
4582
4583
			else
4584
				$func();
4585
		}
4586
	}
4587
}
4588
4589
/**
4590
 * Receives a string and tries to figure it out if it contains info to load a file.
4591
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
4592
 * 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.
4593
 *
4594
 * @param string $string The string containing a valid format.
4595
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
4596
 */
4597
function load_file($string)
4598
{
4599
	global $sourcedir, $txt, $boarddir, $settings;
4600
4601
	if (empty($string))
4602
		return false;
4603
4604
	if (strpos($string, '|') !== false)
4605
	{
4606
		list ($file, $string) = explode('|', $string);
4607
4608
		// Match the wildcards to their regular vars.
4609
		if (empty($settings['theme_dir']))
4610
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
4611
4612
		else
4613
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
4614
4615
		// Load the file if it can be loaded.
4616
		if (file_exists($absPath))
4617
			require_once($absPath);
4618
4619
		// No? try a fallback to $sourcedir
4620
		else
4621
		{
4622
			$absPath = $sourcedir .'/'. $file;
4623
4624
			if (file_exists($absPath))
4625
				require_once($absPath);
4626
4627
			// Sorry, can't do much for you at this point.
4628
			else
4629
			{
4630
				loadLanguage('Errors');
4631
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
4632
4633
				// File couldn't be loaded.
4634
				return false;
4635
			}
4636
		}
4637
	}
4638
4639
	return $string;
4640
}
4641
4642
/**
4643
 * Prepares an array of "likes" info for the topic specified by $topic
4644
 * @param integer $topic The topic ID to fetch the info from.
4645
 * @return array An array of IDs of messages in the specified topic that the current user likes
4646
 */
4647
function prepareLikesContext($topic)
4648
{
4649
	global $user_info, $smcFunc;
4650
4651
	// Make sure we have something to work with.
4652
	if (empty($topic))
4653
		return array();
4654
4655
4656
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
4657
	$user = $user_info['id'];
4658
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
4659
	$ttl = 180;
4660
4661
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
4662
	{
4663
		$temp = array();
4664
		$request = $smcFunc['db_query']('', '
4665
			SELECT content_id
4666
			FROM {db_prefix}user_likes AS l
4667
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
4668
			WHERE l.id_member = {int:current_user}
4669
				AND l.content_type = {literal:msg}
4670
				AND m.id_topic = {int:topic}',
4671
			array(
4672
				'current_user' => $user,
4673
				'topic' => $topic,
4674
			)
4675
		);
4676
		while ($row = $smcFunc['db_fetch_assoc']($request))
4677
			$temp[] = (int) $row['content_id'];
4678
4679
		cache_put_data($cache_key, $temp, $ttl);
4680
	}
4681
4682
	return $temp;
4683
}
4684
4685
/**
4686
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
4687
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
4688
 * that are not normally displayable.  This converts the popular ones that
4689
 * appear from a cut and paste from windows.
4690
 *
4691
 * @param string $string The string
4692
 * @return string The sanitized string
4693
 */
4694
function sanitizeMSCutPaste($string)
4695
{
4696
	global $context;
4697
4698
	if (empty($string))
4699
		return $string;
4700
4701
	// UTF-8 occurences of MS special characters
4702
	$findchars_utf8 = array(
4703
		"\xe2\x80\x9a",	// single low-9 quotation mark
4704
		"\xe2\x80\x9e",	// double low-9 quotation mark
4705
		"\xe2\x80\xa6",	// horizontal ellipsis
4706
		"\xe2\x80\x98",	// left single curly quote
4707
		"\xe2\x80\x99",	// right single curly quote
4708
		"\xe2\x80\x9c",	// left double curly quote
4709
		"\xe2\x80\x9d",	// right double curly quote
4710
		"\xe2\x80\x93",	// en dash
4711
		"\xe2\x80\x94",	// em dash
4712
	);
4713
4714
	// windows 1252 / iso equivalents
4715
	$findchars_iso = array(
4716
		chr(130),
4717
		chr(132),
4718
		chr(133),
4719
		chr(145),
4720
		chr(146),
4721
		chr(147),
4722
		chr(148),
4723
		chr(150),
4724
		chr(151),
4725
	);
4726
4727
	// safe replacements
4728
	$replacechars = array(
4729
		',',	// &sbquo;
4730
		',,',	// &bdquo;
4731
		'...',	// &hellip;
4732
		"'",	// &lsquo;
4733
		"'",	// &rsquo;
4734
		'"',	// &ldquo;
4735
		'"',	// &rdquo;
4736
		'-',	// &ndash;
4737
		'--',	// &mdash;
4738
	);
4739
4740
	if ($context['utf8'])
4741
		$string = str_replace($findchars_utf8, $replacechars, $string);
4742
	else
4743
		$string = str_replace($findchars_iso, $replacechars, $string);
4744
4745
	return $string;
4746
}
4747
4748
/**
4749
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
4750
 *
4751
 * Callback function for preg_replace_callback in subs-members
4752
 * Uses capture group 2 in the supplied array
4753
 * Does basic scan to ensure characters are inside a valid range
4754
 *
4755
 * @param array $matches An array of matches (relevant info should be the 3rd item)
4756
 * @return string A fixed string
4757
 */
4758
function replaceEntities__callback($matches)
4759
{
4760
	global $context;
4761
4762
	if (!isset($matches[2]))
4763
		return '';
4764
4765
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
4766
4767
	// remove left to right / right to left overrides
4768
	if ($num === 0x202D || $num === 0x202E)
4769
		return '';
4770
4771
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
4772
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
4773
		return '&#' . $num . ';';
4774
4775
	if (empty($context['utf8']))
4776
	{
4777
		// no control characters
4778
		if ($num < 0x20)
4779
			return '';
4780
		// text is text
4781
		elseif ($num < 0x80)
4782
			return chr($num);
4783
		// all others get html-ised
4784
		else
4785
			return '&#' . $matches[2] . ';';
4786
	}
4787
	else
4788
	{
4789
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
4790
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
4791
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
4792
			return '';
4793
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
4794
		elseif ($num < 0x80)
4795
			return chr($num);
4796
		// <0x800 (2048)
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
4797 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...
4798
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
4799
		// < 0x10000 (65536)
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
4800 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...
4801
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4802
		// <= 0x10FFFF (1114111)
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
4803 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...
4804
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4805
	}
4806
}
4807
4808
/**
4809
 * Converts html entities to utf8 equivalents
4810
 *
4811
 * Callback function for preg_replace_callback
4812
 * Uses capture group 1 in the supplied array
4813
 * Does basic checks to keep characters inside a viewable range.
4814
 *
4815
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
4816
 * @return string The fixed string
4817
 */
4818
function fixchar__callback($matches)
4819
{
4820
	if (!isset($matches[1]))
4821
		return '';
4822
4823
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
4824
4825
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
4826
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
4827
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
4828
		return '';
4829
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
4830
	elseif ($num < 0x80)
4831
		return chr($num);
4832
	// <0x800 (2048)
0 ignored issues
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
4833 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...
4834
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
4835
	// < 0x10000 (65536)
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
4836 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...
4837
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4838
	// <= 0x10FFFF (1114111)
0 ignored issues
show
Unused Code Comprehensibility introduced by
45% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
4839 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...
4840
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4841
}
4842
4843
/**
4844
 * Strips out invalid html entities, replaces others with html style &#123; codes
4845
 *
4846
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
4847
 * strpos, strlen, substr etc
4848
 *
4849
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
4850
 * @return string The fixed string
4851
 */
4852
function entity_fix__callback($matches)
4853
{
4854
	if (!isset($matches[2]))
4855
		return '';
4856
4857
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
4858
4859
	// we don't allow control characters, characters out of range, byte markers, etc
4860
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
4861
		return '';
4862
	else
4863
		return '&#' . $num . ';';
4864
}
4865
4866
/**
4867
 * Return a Gravatar URL based on
4868
 * - the supplied email address,
4869
 * - the global maximum rating,
4870
 * - the global default fallback,
4871
 * - maximum sizes as set in the admin panel.
4872
 *
4873
 * It is SSL aware, and caches most of the parameters.
4874
 *
4875
 * @param string $email_address The user's email address
4876
 * @return string The gravatar URL
4877
 */
4878
function get_gravatar_url($email_address)
4879
{
4880
	global $modSettings, $smcFunc;
4881
	static $url_params = null;
4882
4883
	if ($url_params === null)
4884
	{
4885
		$ratings = array('G', 'PG', 'R', 'X');
4886
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
4887
		$url_params = array();
4888 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...
4889
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
4890 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...
4891
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
4892
		if (!empty($modSettings['avatar_max_width_external']))
4893
			$size_string = (int) $modSettings['avatar_max_width_external'];
4894
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
4895
			if ((int) $modSettings['avatar_max_height_external'] < $size_string)
4896
				$size_string = $modSettings['avatar_max_height_external'];
4897
4898
		if (!empty($size_string))
4899
			$url_params[] = 's=' . $size_string;
4900
	}
4901
	$http_method = !empty($modSettings['force_ssl']) && $modSettings['force_ssl'] == 2 ? 'https://secure' : 'http://www';
4902
4903
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
4904
}
4905
4906
/**
4907
 * Get a list of timezones.
4908
 *
4909
 * @param string $when An optional date or time for which to calculate the timezone offset values. May be a Unix timestamp or any string that strtotime() can understand. Defaults to 'now'.
4910
 * @return array An array of timezone info.
4911
 */
4912
function smf_list_timezones($when = 'now')
4913
{
4914
	global $modSettings;
4915
	static $timezones = null, $lastwhen = null;
4916
4917
	// No point doing this over if we already did it once
4918
	if (!empty($timezones) && $when == $lastwhen)
4919
		return $timezones;
4920
	else
4921
		$lastwhen = $when;
4922
4923
	// Parseable datetime string?
4924
	if (is_int($timestamp = strtotime($when)))
4925
		$when = $timestamp;
4926
4927
	// A Unix timestamp?
4928
	elseif (is_numeric($when))
4929
		$when = intval($when);
4930
4931
	// Invalid value? Just get current Unix timestamp.
4932
	else
4933
		$when = time();
4934
4935
	// We'll need these too
4936
	$date_when = date_create('@' . $when);
4937
	$later = (int) date_format(date_add($date_when, date_interval_create_from_date_string('1 year')), 'U');
4938
4939
	// Prefer and give custom descriptions for these time zones
4940
	// If the description is left empty, it will be filled in with the names of matching cities
4941
	$timezone_descriptions = array(
4942
		'America/Adak' => 'Aleutian Islands',
4943
		'Pacific/Marquesas' => 'Marquesas Islands',
4944
		'Pacific/Gambier' => 'Gambier Islands',
4945
		'America/Anchorage' => 'Alaska',
4946
		'Pacific/Pitcairn' => 'Pitcairn Islands',
4947
		'America/Los_Angeles' => 'Pacific Time (USA, Canada)',
4948
		'America/Denver' => 'Mountain Time (USA, Canada)',
4949
		'America/Phoenix' => 'Mountain Time (no DST)',
4950
		'America/Chicago' => 'Central Time (USA, Canada)',
4951
		'America/Belize' => 'Central Time (no DST)',
4952
		'America/New_York' => 'Eastern Time (USA, Canada)',
4953
		'America/Atikokan' => 'Eastern Time (no DST)',
4954
		'America/Halifax' => 'Atlantic Time (Canada)',
4955
		'America/Anguilla' => 'Atlantic Time (no DST)',
4956
		'America/St_Johns' => 'Newfoundland',
4957
		'America/Chihuahua' => 'Chihuahua, Mazatlan',
4958
		'Pacific/Easter' => 'Easter Island',
4959
		'Atlantic/Stanley' => 'Falkland Islands',
4960
		'America/Miquelon' => 'Saint Pierre and Miquelon',
4961
		'America/Argentina/Buenos_Aires' => 'Buenos Aires',
4962
		'America/Sao_Paulo' => 'Brasilia Time',
4963
		'America/Araguaina' => 'Brasilia Time (no DST)',
4964
		'America/Godthab' => 'Greenland',
4965
		'America/Noronha' => 'Fernando de Noronha',
4966
		'Atlantic/Reykjavik' => 'Greenwich Mean Time (no DST)',
4967
		'Europe/London' => '',
4968
		'Europe/Berlin' => 'Central European Time',
4969
		'Europe/Helsinki' => 'Eastern European Time',
4970
		'Africa/Brazzaville' => 'Brazzaville, Lagos, Porto-Novo',
4971
		'Asia/Jerusalem' => 'Jerusalem',
4972
		'Europe/Moscow' => '',
4973
		'Africa/Khartoum' => 'Eastern Africa Time',
4974
		'Asia/Riyadh' => 'Arabia Time',
4975
		'Asia/Kolkata' => 'India, Sri Lanka',
4976
		'Asia/Yekaterinburg' => 'Yekaterinburg, Tyumen',
4977
		'Asia/Dhaka' => 'Astana, Dhaka',
4978
		'Asia/Rangoon' => 'Yangon/Rangoon',
4979
		'Indian/Christmas' => 'Christmas Island',
4980
		'Antarctica/DumontDUrville' => 'Dumont D\'Urville Station',
4981
		'Antarctica/Vostok' => 'Vostok Station',
4982
		'Australia/Lord_Howe' => 'Lord Howe Island',
4983
		'Pacific/Guadalcanal' => 'Solomon Islands',
4984
		'Pacific/Norfolk' => 'Norfolk Island',
4985
		'Pacific/Noumea' => 'New Caledonia',
4986
		'Pacific/Auckland' => 'Auckland, McMurdo Station',
4987
		'Pacific/Kwajalein' => 'Marshall Islands',
4988
		'Pacific/Chatham' => 'Chatham Islands',
4989
	);
4990
4991
	// Should we put time zones from certain countries at the top of the list?
4992
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
4993
	$priority_tzids = array();
4994
	foreach ($priority_countries as $country)
4995
	{
4996
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
4997
		if (!empty($country_tzids))
4998
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
4999
	}
5000
5001
	// Process the preferred timezones first, then the rest.
5002
	$tzids = array_keys($timezone_descriptions) + array_diff(timezone_identifiers_list(), array_keys($timezone_descriptions));
5003
5004
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5005
	foreach ($tzids as $tzid)
5006
	{
5007
		// We don't want UTC right now
5008
		if ($tzid == 'UTC')
5009
			continue;
5010
5011
		$tz = timezone_open($tzid);
5012
5013
		// First, get the set of transition rules for this tzid
5014
		$tzinfo = timezone_transitions_get($tz, $when, $later);
5015
5016
		$tzinfo[0]['abbr'] = fix_tz_abbrev($tzid, $tzinfo[0]['abbr']);
5017
5018
		$tzkey = serialize($tzinfo);
5019
5020
		// Next, get the geographic info for this tzid
5021
		$tzgeo = timezone_location_get($tz);
5022
5023
		// Don't overwrite our preferred tzids
5024
		if (empty($zones[$tzkey]['tzid']))
5025
			$zones[$tzkey]['tzid'] = $tzid;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$zones was never initialized. Although not strictly required by PHP, it is generally a good practice to add $zones = 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...
5026
5027
		// A time zone from a prioritized country?
5028
		if (in_array($tzid, $priority_tzids))
5029
			$priority_zones[$tzkey] = true;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$priority_zones was never initialized. Although not strictly required by PHP, it is generally a good practice to add $priority_zones = 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...
5030
5031
		// Keep track of the location and offset for this tzid
5032
		$tzid_parts = explode('/', $tzid);
5033
		$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
0 ignored issues
show
Bug introduced by
The variable $zones 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...
5034
		$offsets[$tzkey] = $tzinfo[0]['offset'];
0 ignored issues
show
Coding Style Comprehensibility introduced by
$offsets was never initialized. Although not strictly required by PHP, it is generally a good practice to add $offsets = 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...
5035
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
0 ignored issues
show
Bug introduced by
The variable $longitudes 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...
5036
	}
5037
5038
	// Sort by offset then longitude
5039
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $longitudes, SORT_ASC, SORT_NUMERIC, $zones);
5040
5041
	// Build the final array of formatted values
5042
	$priority_timezones = array();
5043
	$timezones = array();
5044
	foreach ($zones as $tzkey => $tzvalue)
5045
	{
5046
		$tzinfo = unserialize($tzkey);
5047
5048
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
5049
5050
		if (!empty($timezone_descriptions[$tzvalue['tzid']]))
5051
			$desc = $timezone_descriptions[$tzvalue['tzid']];
5052
		else
5053
			$desc = implode(', ', array_unique($tzvalue['locations']));
5054
5055
		if (isset($priority_zones[$tzkey]))
5056
			$priority_timezones[$tzvalue['tzid']] = $tzinfo[0]['abbr'] . ' - ' . $desc . ' [UTC' . date_format($date_when, 'P') . ']';
5057
		else
5058
			$timezones[$tzvalue['tzid']] = $tzinfo[0]['abbr'] . ' - ' . $desc . ' [UTC' . date_format($date_when, 'P') . ']';
5059
	}
5060
5061
	$timezones = array_merge(
5062
		$priority_timezones,
5063
		array('' => '(Forum Default)', 'UTC' => 'UTC - Coordinated Universal Time'),
5064
		$timezones
5065
	);
5066
5067
	return $timezones;
5068
}
5069
5070
/**
5071
 * Reformats certain time zone abbreviations to look better.
5072
 *
5073
 * Some of PHP's time zone abbreviations are just numerical offsets from UTC, e.g. '+04'
5074
 * These look weird and are kind of useless, so we make them look better.
5075
 *
5076
 * @param string $tzid The Olsen time zome identifier for a time zone.
5077
 * @param string $tz_abbrev The abbreviation PHP provided for this time zone.
5078
 * @return string The fixed version of $tz_abbrev.
5079
 */
5080
function fix_tz_abbrev($tzid, $tz_abbrev)
5081
{
5082
	// Is this abbreviation just a numerical offset?
5083
	if (strspn($tz_abbrev, '+-') > 0)
5084
	{
5085
		// To get on this list, a time zone must be historically stable and must not observe daylight saving time
5086
		$missing_tz_abbrs = array(
5087
			'Antarctica/Casey' => 'CAST',
5088
			'Antarctica/Davis' => 'DAVT',
5089
			'Antarctica/DumontDUrville' => 'DDUT',
5090
			'Antarctica/Mawson' => 'MAWT',
5091
			'Antarctica/Rothera' => 'ART',
5092
			'Antarctica/Syowa' => 'SYOT',
5093
			'Antarctica/Vostok' => 'VOST',
5094
			'Asia/Almaty' => 'ALMT',
5095
			'Asia/Aqtau' => 'ORAT',
5096
			'Asia/Aqtobe' => 'AQTT',
5097
			'Asia/Ashgabat' => 'TMT',
5098
			'Asia/Bishkek' => 'KGT',
5099
			'Asia/Colombo' => 'IST',
5100
			'Asia/Dushanbe' => 'TJT',
5101
			'Asia/Oral' => 'ORAT',
5102
			'Asia/Qyzylorda' => 'QYZT',
5103
			'Asia/Samarkand' => 'UZT',
5104
			'Asia/Tashkent' => 'UZT',
5105
			'Asia/Tbilisi' => 'GET',
5106
			'Asia/Yerevan' => 'AMT',
5107
			'Europe/Istanbul' => 'TRT',
5108
			'Europe/Minsk' => 'MSK',
5109
			'Indian/Kerguelen' => 'TFT',
5110
		);
5111
5112
		if (!empty($missing_tz_abbrs[$tzid]))
5113
			$tz_abbrev = $missing_tz_abbrs[$tzid];
5114
		else
5115
		{
5116
			// Russia likes to experiment with time zones often, and names them as offsets from Moscow
5117
			$tz_location = timezone_location_get(timezone_open($tzid));
5118
			if ($tz_location['country_code'] == 'RU')
5119
			{
5120
				$msk_offset = intval($tz_abbrev) - 3;
5121
				$tz_abbrev = 'MSK' . (!empty($msk_offset) ? sprintf('%+0d', $msk_offset) : '');
5122
			}
5123
		}
5124
5125
		// Still no good? We'll just mark it as a UTC offset
5126
		if (strspn($tz_abbrev, '+-') > 0)
5127
		{
5128
			$tz_abbrev = intval($tz_abbrev);
5129
			$tz_abbrev = 'UTC' . (!empty($tz_abbrev) ? sprintf('%+0d', $tz_abbrev) : '');
5130
		}
5131
	}
5132
5133
	return $tz_abbrev;
5134
}
5135
5136
/**
5137
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
5138
 * @return binary The IP address in binary or false
5139
 */
5140
function inet_ptod($ip_address)
5141
{
5142
	if (!isValidIP($ip_address))
5143
		return $ip_address;
5144
5145
	$bin = inet_pton($ip_address);
5146
	return $bin;
5147
}
5148
5149
/**
5150
 * @param binary $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
5151
 * @return string The IP address in presentation format or false on error
5152
 */
5153
function inet_dtop($bin)
5154
{
5155
	if(empty($bin))
5156
		return '';
5157
5158
	global $db_type;
5159
5160
	if ($db_type == 'postgresql')
5161
		return $bin;
5162
5163
	$ip_address = inet_ntop($bin);
5164
5165
	return $ip_address;
5166
}
5167
5168
/**
5169
 * Safe serialize() and unserialize() replacements
5170
 *
5171
 * @license Public Domain
5172
 *
5173
 * @author anthon (dot) pang (at) gmail (dot) com
5174
 */
5175
5176
/**
5177
 * Safe serialize() replacement. Recursive
5178
 * - output a strict subset of PHP's native serialized representation
5179
 * - does not serialize objects
5180
 *
5181
 * @param mixed $value
5182
 * @return string
5183
 */
5184
function _safe_serialize($value)
5185
{
5186
	if(is_null($value))
5187
		return 'N;';
5188
5189
	if(is_bool($value))
5190
		return 'b:'. (int) $value .';';
5191
5192
	if(is_int($value))
5193
		return 'i:'. $value .';';
5194
5195
	if(is_float($value))
5196
		return 'd:'. str_replace(',', '.', $value) .';';
5197
5198
	if(is_string($value))
5199
		return 's:'. strlen($value) .':"'. $value .'";';
5200
5201
	if(is_array($value))
5202
	{
5203
		$out = '';
5204
		foreach($value as $k => $v)
5205
			$out .= _safe_serialize($k) . _safe_serialize($v);
5206
5207
		return 'a:'. count($value) .':{'. $out .'}';
5208
	}
5209
5210
	// safe_serialize cannot serialize resources or objects.
5211
	return false;
5212
}
5213
/**
5214
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
5215
 *
5216
 * @param mixed $value
5217
 * @return string
5218
 */
5219 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...
5220
{
5221
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5222
	if (function_exists('mb_internal_encoding') &&
5223
		(((int) ini_get('mbstring.func_overload')) & 2))
5224
	{
5225
		$mbIntEnc = mb_internal_encoding();
5226
		mb_internal_encoding('ASCII');
5227
	}
5228
5229
	$out = _safe_serialize($value);
5230
5231
	if (isset($mbIntEnc))
5232
		mb_internal_encoding($mbIntEnc);
5233
5234
	return $out;
5235
}
5236
5237
/**
5238
 * Safe unserialize() replacement
5239
 * - accepts a strict subset of PHP's native serialized representation
5240
 * - does not unserialize objects
5241
 *
5242
 * @param string $str
5243
 * @return mixed
5244
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
5245
 */
5246
function _safe_unserialize($str)
5247
{
5248
	// Input  is not a string.
5249
	if(empty($str) || !is_string($str))
5250
		return false;
5251
5252
	$stack = array();
5253
	$expected = array();
5254
5255
	/*
5256
	 * states:
5257
	 *   0 - initial state, expecting a single value or array
5258
	 *   1 - terminal state
5259
	 *   2 - in array, expecting end of array or a key
5260
	 *   3 - in array, expecting value or another array
5261
	 */
5262
	$state = 0;
5263
	while($state != 1)
5264
	{
5265
		$type = isset($str[0]) ? $str[0] : '';
5266
		if($type == '}')
5267
			$str = substr($str, 1);
5268
5269
		else if($type == 'N' && $str[1] == ';')
5270
		{
5271
			$value = null;
5272
			$str = substr($str, 2);
5273
		}
5274
		else if($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
5275
		{
5276
			$value = $matches[1] == '1' ? true : false;
5277
			$str = substr($str, 4);
5278
		}
5279
		else if($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
5280
		{
5281
			$value = (int)$matches[1];
5282
			$str = $matches[2];
5283
		}
5284
		else if($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
5285
		{
5286
			$value = (float)$matches[1];
5287
			$str = $matches[3];
5288
		}
5289
		else if($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int)$matches[1], 2) == '";')
5290
		{
5291
			$value = substr($matches[2], 0, (int)$matches[1]);
5292
			$str = substr($matches[2], (int)$matches[1] + 2);
5293
		}
5294
		else if($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
5295
		{
5296
			$expectedLength = (int)$matches[1];
5297
			$str = $matches[2];
5298
		}
5299
5300
		// Object or unknown/malformed type.
5301
		else
5302
			return false;
5303
5304
		switch($state)
5305
		{
5306
			case 3: // In array, expecting value or another array.
5307
				if($type == 'a')
5308
				{
5309
					$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...
5310
					$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...
5311
					$list = &$list[$key];
5312
					$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...
5313
					$state = 2;
5314
					break;
5315
				}
5316
				if($type != '}')
5317
				{
5318
					$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...
5319
					$state = 2;
5320
					break;
5321
				}
5322
5323
				// Missing array value.
5324
				return false;
5325
5326
			case 2: // in array, expecting end of array or a key
5327
				if($type == '}')
5328
				{
5329
					// Array size is less than expected.
5330
					if(count($list) < end($expected))
5331
						return false;
5332
5333
					unset($list);
5334
					$list = &$stack[count($stack)-1];
5335
					array_pop($stack);
5336
5337
					// Go to terminal state if we're at the end of the root array.
5338
					array_pop($expected);
5339
5340
					if(count($expected) == 0)
5341
						$state = 1;
5342
5343
					break;
5344
				}
5345
5346
				if($type == 'i' || $type == 's')
5347
				{
5348
					// Array size exceeds expected length.
5349
					if(count($list) >= end($expected))
5350
						return false;
5351
5352
					$key = $value;
5353
					$state = 3;
5354
					break;
5355
				}
5356
5357
				// Illegal array index type.
5358
				return false;
5359
5360
			// Expecting array or value.
5361
			case 0:
5362
				if($type == 'a')
5363
				{
5364
					$data = array();
5365
					$list = &$data;
5366
					$expected[] = $expectedLength;
5367
					$state = 2;
5368
					break;
5369
				}
5370
5371
				if($type != '}')
5372
				{
5373
					$data = $value;
5374
					$state = 1;
5375
					break;
5376
				}
5377
5378
				// Not in array.
5379
				return false;
5380
		}
5381
	}
5382
5383
	// Trailing data in input.
5384
	if(!empty($str))
5385
		return false;
5386
5387
	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...
5388
}
5389
5390
/**
5391
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
5392
 *
5393
 * @param string $str
5394
 * @return mixed
5395
 */
5396 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...
5397
{
5398
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5399
	if (function_exists('mb_internal_encoding') &&
5400
		(((int) ini_get('mbstring.func_overload')) & 0x02))
5401
	{
5402
		$mbIntEnc = mb_internal_encoding();
5403
		mb_internal_encoding('ASCII');
5404
	}
5405
5406
	$out = _safe_unserialize($str);
5407
5408
	if (isset($mbIntEnc))
5409
		mb_internal_encoding($mbIntEnc);
5410
5411
	return $out;
5412
}
5413
5414
/**
5415
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
5416
5417
 * @param string $file The file/dir full path.
5418
 * @param int $value Not needed, added for legacy reasons.
5419
 * @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.
5420
 */
5421
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...
5422
{
5423
	// No file? no checks!
5424
	if (empty($file))
5425
		return false;
5426
5427
	// Already writable?
5428
	if (is_writable($file))
5429
		return true;
5430
5431
	// Do we have a file or a dir?
5432
	$isDir = is_dir($file);
5433
	$isWritable = false;
5434
5435
	// Set different modes.
5436
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
5437
5438
	foreach($chmodValues as $val)
5439
	{
5440
		// If it's writable, break out of the loop.
5441
		if (is_writable($file))
5442
		{
5443
			$isWritable = true;
5444
			break;
5445
		}
5446
5447
		else
5448
			@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...
5449
	}
5450
5451
	return $isWritable;
5452
}
5453
5454
/**
5455
 * Wrapper function for json_decode() with error handling.
5456
5457
 * @param string $json The string to decode.
5458
 * @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.
5459
 * @param bool $logIt To specify if the error will be logged if theres any.
5460
 * @return array Either an empty array or the decoded data as an array.
5461
 */
5462
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
5463
{
5464
	global $txt;
5465
5466
	// Come on...
5467
	if (empty($json) || !is_string($json))
5468
		return array();
5469
5470
	$returnArray = @json_decode($json, $returnAsArray);
5471
5472
	// PHP 5.3 so no json_last_error_msg()
5473
	switch(json_last_error())
5474
	{
5475
		case JSON_ERROR_NONE:
5476
			$jsonError = false;
5477
			break;
5478
		case JSON_ERROR_DEPTH:
5479
			$jsonError =  'JSON_ERROR_DEPTH';
5480
			break;
5481
		case JSON_ERROR_STATE_MISMATCH:
5482
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
5483
			break;
5484
		case JSON_ERROR_CTRL_CHAR:
5485
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
5486
			break;
5487
		case JSON_ERROR_SYNTAX:
5488
			$jsonError = 'JSON_ERROR_SYNTAX';
5489
			break;
5490
		case JSON_ERROR_UTF8:
5491
			$jsonError = 'JSON_ERROR_UTF8';
5492
			break;
5493
		default:
5494
			$jsonError = 'unknown';
5495
			break;
5496
	}
5497
5498
	// Something went wrong!
5499
	if (!empty($jsonError) && $logIt)
5500
	{
5501
		// Being a wrapper means we lost our smf_error_handler() privileges :(
5502
		$jsonDebug = debug_backtrace();
5503
		$jsonDebug = $jsonDebug[0];
5504
		loadLanguage('Errors');
5505
5506
		if (!empty($jsonDebug))
5507
			log_error($txt['json_'. $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
5508
5509
		else
5510
			log_error($txt['json_'. $jsonError], 'critical');
5511
5512
		// Everyone expects an array.
5513
		return array();
5514
	}
5515
5516
	return $returnArray;
5517
}
5518
5519
/**
5520
 * Check the given String if he is a valid IPv4 or IPv6
5521
 * return true or false
5522
 */
5523
function isValidIP($IPString)
5524
{
5525
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
5526
}
5527
5528
/**
5529
 * Outputs a response.
5530
 * It assumes the data is already a string.
5531
 * @param string $data The data to print
5532
 * @param string $type The content type. Defaults to Json.
5533
 * @return void
5534
 */
5535
function smf_serverResponse($data = '', $type = 'Content-Type: application/json')
5536
{
5537
	global $db_show_debug, $modSettings;
5538
5539
	// Defensive programming anyone?
5540
	if (empty($data))
5541
		return false;
5542
5543
	// Don't need extra stuff...
5544
	$db_show_debug = false;
5545
5546
	// Kill anything else.
5547
	ob_end_clean();
5548
5549
	if (!empty($modSettings['CompressedOutput']))
5550
		@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...
5551
5552
	else
5553
		ob_start();
5554
5555
	// Set the header.
5556
	header($type);
5557
5558
	// Echo!
5559
	echo $data;
5560
5561
	// Done.
5562
	obExit(false);
5563
}
5564
5565
/**
5566
 * Creates an optimized regex to match all known top level domains.
5567
 *
5568
 * The optimized regex is stored in $modSettings['tld_regex'].
5569
 *
5570
 * To update the stored version of the regex to use the latest list of valid TLDs from iana.org, set
5571
 * the $update parameter to true. Updating can take some time, based on network connectivity, so it
5572
 * should normally only be done by calling this function from a background or scheduled task.
5573
 *
5574
 * If $update is not true, but the regex is missing or invalid, the regex will be regenerated from a
5575
 * hard-coded list of TLDs. This regenerated regex will be overwritten on the next scheduled update.
5576
 *
5577
 * @param bool $update If true, fetch and process the latest offical list of TLDs from iana.org.
5578
 */
5579
function set_tld_regex($update = false)
5580
{
5581
	global $sourcedir, $smcFunc, $modSettings;
5582
	static $done = false;
5583
5584
	// If we don't need to do anything, don't
5585
	if (!$update && $done)
5586
		return;
5587
5588
	// Should we get a new copy of the official list of TLDs?
5589
	if ($update)
5590
	{
5591
		require_once($sourcedir . '/Subs-Package.php');
5592
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
5593
	}
5594
	// If we aren't updating and the regex is valid, we're done
5595
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
5596
	{
5597
		$done = true;
5598
		return;
5599
	}
5600
5601
	// If we successfully got an update, process the list into an array
5602
	if (!empty($tlds))
5603
	{
5604
		// Clean $tlds and convert it to an array
5605
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line) {
5606
			$line = trim($line);
5607
			if (empty($line) || strpos($line, '#') !== false || strpos($line, ' ') !== false)
5608
				return false;
5609
			else
5610
				return true;
5611
		});
5612
5613
		// Convert Punycode to Unicode
5614
		$tlds = array_map(function ($input) {
5615
			$prefix = 'xn--';
5616
			$safe_char = 0xFFFC;
5617
			$base = 36;
5618
			$tmin = 1;
5619
			$tmax = 26;
5620
			$skew = 38;
5621
			$damp = 700;
5622
			$output_parts = array();
5623
5624
			$input = str_replace(strtoupper($prefix), $prefix, $input);
5625
5626
			$enco_parts = (array) explode('.', $input);
5627
5628
			foreach ($enco_parts as $encoded)
5629
			{
5630
				if (strpos($encoded,$prefix) !== 0 || strlen(trim(str_replace($prefix,'',$encoded))) == 0)
5631
				{
5632
					$output_parts[] = $encoded;
5633
					continue;
5634
				}
5635
5636
				$is_first = true;
5637
				$bias = 72;
5638
				$idx = 0;
5639
				$char = 0x80;
5640
				$decoded = array();
5641
				$output='';
5642
				$delim_pos = strrpos($encoded, '-');
5643
5644
				if ($delim_pos > strlen($prefix))
5645
				{
5646
					for ($k = strlen($prefix); $k < $delim_pos; ++$k)
5647
					{
5648
						$decoded[] = ord($encoded{$k});
5649
					}
5650
				}
5651
5652
				$deco_len = count($decoded);
5653
				$enco_len = strlen($encoded);
5654
5655
				for ($enco_idx = $delim_pos ? ($delim_pos + 1) : 0; $enco_idx < $enco_len; ++$deco_len)
5656
				{
5657
					for ($old_idx = $idx, $w = 1, $k = $base; 1 ; $k += $base)
5658
					{
5659
						$cp = ord($encoded{$enco_idx++});
5660
						$digit = ($cp - 48 < 10) ? $cp - 22 : (($cp - 65 < 26) ? $cp - 65 : (($cp - 97 < 26) ? $cp - 97 : $base));
5661
						$idx += $digit * $w;
5662
						$t = ($k <= $bias) ? $tmin : (($k >= $bias + $tmax) ? $tmax : ($k - $bias));
5663
5664
						if ($digit < $t)
5665
							break;
5666
5667
						$w = (int) ($w * ($base - $t));
5668
					}
5669
5670
					$delta = $idx - $old_idx;
5671
					$delta = intval($is_first ? ($delta / $damp) : ($delta / 2));
5672
					$delta += intval($delta / ($deco_len + 1));
5673
5674
					for ($k = 0; $delta > (($base - $tmin) * $tmax) / 2; $k += $base)
5675
						$delta = intval($delta / ($base - $tmin));
5676
5677
					$bias = intval($k + ($base - $tmin + 1) * $delta / ($delta + $skew));
5678
					$is_first = false;
5679
					$char += (int) ($idx / ($deco_len + 1));
5680
					$idx %= ($deco_len + 1);
5681
5682
					if ($deco_len > 0)
5683
					{
5684
						for ($i = $deco_len; $i > $idx; $i--)
5685
							$decoded[$i] = $decoded[($i - 1)];
5686
					}
5687
					$decoded[$idx++] = $char;
5688
				}
5689
5690
				foreach ($decoded as $k => $v)
5691
				{
5692
					// 7bit are transferred literally
5693
					if ($v < 128)
5694
						$output .= chr($v);
5695
5696
					// 2 bytes
5697
					elseif ($v < (1 << 11))
5698
						$output .= chr(192+($v >> 6)) . chr(128+($v & 63));
5699
5700
					// 3 bytes
5701
					elseif ($v < (1 << 16))
5702
						$output .= chr(224+($v >> 12)) . chr(128+(($v >> 6) & 63)) . chr(128+($v & 63));
5703
5704
					// 4 bytes
5705
					elseif ($v < (1 << 21))
5706
						$output .= chr(240+($v >> 18)) . chr(128+(($v >> 12) & 63)) . chr(128+(($v >> 6) & 63)) . chr(128+($v & 63));
5707
5708
					//  'Conversion from UCS-4 to UTF-8 failed: malformed input at byte '.$k
5709
					else
5710
						$output .= $safe_char;
5711
				}
5712
5713
				$output_parts[] = $output;
5714
			}
5715
5716
			return implode('.', $output_parts);
5717
		}, $tlds);
5718
5719
		$schedule_update = false;
5720
	}
5721
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
5722
	else
5723
	{
5724
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz', 'cat',
5725
			'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post', 'pro', 'tel',
5726
			'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq',
5727
			'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh',
5728
			'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc',
5729
			'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv',
5730
			'cx', 'cy', 'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', 'eh',
5731
			'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge',
5732
			'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw',
5733
			'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq',
5734
			'ir', 'is', 'it', 'ja', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn',
5735
			'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu',
5736
			'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp',
5737
			'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf',
5738
			'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph',
5739
			'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru',
5740
			'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
5741
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th',
5742
			'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug',
5743
			'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye',
5744
			'yt', 'yu', 'za', 'zm', 'zw');
5745
5746
		$schedule_update = true;
5747
	}
5748
5749
	// build_regex() returns an array. We only need the first item.
5750
	$tld_regex = build_regex($tlds);
5751
	$tld_regex = array_shift($tld_regex);
5752
5753
	// Remember the new regex in $modSettings
5754
	updateSettings(array('tld_regex' => $tld_regex));
5755
5756
	// Schedule a background update if we need one
5757 View Code Duplication
	if (!empty($schedule_update))
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...
5758
	{
5759
		$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
5760
			array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
5761
			array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
5762
		);
5763
	}
5764
5765
	// Redundant repetition is redundant
5766
	$done = true;
5767
}
5768
5769
/**
5770
 * Creates optimized regular expressions from an array of strings.
5771
 *
5772
 * An optimized regex built using this function will be much faster than a simple regex built using
5773
 * `implode('|', $strings)` --- anywhere from several times to several orders of magnitude faster.
5774
 *
5775
 * However, the time required to build the optimized regex is approximately equal to the time it
5776
 * takes to execute the simple regex. Therefore, it is only worth calling this function if the
5777
 * resulting regex will be used more than once.
5778
 *
5779
 * Because PHP places an upper limit on the allowed length of a regex, very large arrays may be
5780
 * split and returned as multiple regexes. In such cases, you will need to iterate through all
5781
 * elements of the returned array in order to test all possible matches. (Note: if your array of
5782
 * alternative strings is large enough to require multiple regexes to accomodate it all, it is
5783
 * probably time to reconsider your coding choices. There is almost certainly a better way to do
5784
 * whatever you are trying to do with these giant regexes.)
5785
 *
5786
 * @param array $strings An array of strings to make a regex for.
5787
 * @param string $delim An optional delimiter character to pass to preg_quote().
5788
 * @return array An array of one or more regular expressions to match any of the input strings.
5789
 */
5790
function build_regex($strings, $delim = null)
5791
{
5792
	global $smcFunc;
5793
5794
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
5795
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
5796
	{
5797
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
5798
		{
5799
			$current_encoding = mb_internal_encoding();
5800
			mb_internal_encoding($string_encoding);
5801
		}
5802
5803
		$strlen = 'mb_strlen';
5804
		$substr = 'mb_substr';
5805
	}
5806
	else
5807
	{
5808
		$strlen = $smcFunc['strlen'];
5809
		$substr = $smcFunc['substr'];
5810
	}
5811
5812
	// This recursive function creates the index array from the strings
5813
	$add_string_to_index = function ($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
5814
	{
5815
		static $depth = 0;
5816
		$depth++;
5817
5818
		$first = $substr($string, 0, 1);
5819
5820
		if (empty($index[$first]))
5821
			$index[$first] = array();
5822
5823
		if ($strlen($string) > 1)
5824
		{
5825
			// Sanity check on recursion
5826
			if ($depth > 99)
5827
				$index[$first][$substr($string, 1)] = '';
5828
5829
			else
5830
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
5831
		}
5832
		else
5833
			$index[$first][''] = '';
5834
5835
		$depth--;
5836
		return $index;
5837
	};
5838
5839
	// This recursive function turns the index array into a regular expression
5840
	$index_to_regex = function (&$index, $delim) use (&$strlen, &$index_to_regex)
5841
	{
5842
		static $depth = 0;
5843
		$depth++;
5844
5845
		// Absolute max length for a regex is 32768, but we might need wiggle room
5846
		$max_length = 30000;
5847
5848
		$regex = array();
5849
		$length = 0;
5850
5851
		foreach ($index as $key => $value)
5852
		{
5853
			$key_regex = preg_quote($key, $delim);
5854
			$new_key = $key;
5855
5856
			if (empty($value))
5857
				$sub_regex = '';
5858
			else
5859
			{
5860
				$sub_regex = $index_to_regex($value, $delim);
5861
5862
				if (count(array_keys($value)) == 1)
5863
				{
5864
					$new_key_array = explode('(?'.'>', $sub_regex);
5865
					$new_key .= $new_key_array[0];
5866
				}
5867
				else
5868
					$sub_regex = '(?'.'>' . $sub_regex . ')';
5869
			}
5870
5871
			if ($depth > 1)
5872
				$regex[$new_key] = $key_regex . $sub_regex;
5873
			else
5874
			{
5875
				if (($length += strlen($key_regex) + 1) < $max_length || empty($regex))
5876
				{
5877
					$regex[$new_key] = $key_regex . $sub_regex;
5878
					unset($index[$key]);
5879
				}
5880
				else
5881
					break;
5882
			}
5883
		}
5884
5885
		// Sort by key length and then alphabetically
5886
		uksort($regex, function($k1, $k2) use (&$strlen) {
5887
			$l1 = $strlen($k1);
5888
			$l2 = $strlen($k2);
5889
5890
			if ($l1 == $l2)
5891
				return strcmp($k1, $k2) > 0 ? 1 : -1;
5892
			else
5893
				return $l1 > $l2 ? -1 : 1;
5894
		});
5895
5896
		$depth--;
5897
		return implode('|', $regex);
5898
	};
5899
5900
	// Now that the functions are defined, let's do this thing
5901
	$index = array();
5902
	$regexes = array();
5903
5904
	foreach ($strings as $string)
5905
		$index = $add_string_to_index($string, $index);
5906
5907
	while (!empty($index))
5908
		$regexes[] = '(?'.'>' . $index_to_regex($index, $delim) . ')';
5909
5910
	// Restore PHP's internal character encoding to whatever it was originally
5911
	if (!empty($current_encoding))
5912
		mb_internal_encoding($current_encoding);
5913
5914
	return $regexes;
5915
}
5916
5917
?>
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...