Failed Conditions
Branch release-2.1 (4e22cf)
by Rick
06:39
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 4
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)
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))
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)
0 ignored issues
show
Bug introduced by
The expression $postgroups of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
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
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)
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)
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
0 ignored issues
show
Documentation introduced by
Should the return type not be double|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
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
 * @param bool $process_safe Activate setlocale check for changes at runtime. Slower, but safer.
717
 * @return string A formatted timestamp
718
 */
719
function timeformat($log_time, $show_today = true, $offset_type = false, $process_safe = false)
720
{
721
	global $context, $user_info, $txt, $modSettings;
722
	static $non_twelve_hour, $locale_cache;
723
	static $unsupportedFormats, $finalizedFormats;
724
725
	// Offset the time.
726
	if (!$offset_type)
727
		$time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
728
	// Just the forum offset?
729
	elseif ($offset_type == 'forum')
730
		$time = $log_time + $modSettings['time_offset'] * 3600;
731
	else
732
		$time = $log_time;
733
734
	// We can't have a negative date (on Windows, at least.)
735
	if ($log_time < 0)
736
		$log_time = 0;
737
738
	// Today and Yesterday?
739
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
740
	{
741
		// Get the current time.
742
		$nowtime = forum_time();
743
744
		$then = @getdate($time);
745
		$now = @getdate($nowtime);
746
747
		// Try to make something of a time format string...
748
		$s = strpos($user_info['time_format'], '%S') === false ? '' : ':%S';
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $s. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
749
		if (strpos($user_info['time_format'], '%H') === false && strpos($user_info['time_format'], '%T') === false)
750
		{
751
			$h = strpos($user_info['time_format'], '%l') === false ? '%I' : '%l';
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $h. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
752
			$today_fmt = $h . ':%M' . $s . ' %p';
753
		}
754
		else
755
			$today_fmt = '%H:%M' . $s;
756
757
		// Same day of the year, same year.... Today!
758
		if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
759
			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...
760
761
		// 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...
762
		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))
763
			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...
764
	}
765
766
	$str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
767
768
	// Use the cached formats if available
769
	if (is_null($finalizedFormats))
770
		$finalizedFormats = (array) cache_get_data('timeformatstrings', 86400);
771
772
	// Make a supported version for this format if we don't already have one
773
	if (empty($finalizedFormats[$str]))
774
	{
775
		$timeformat = $str;
776
777
		// Not all systems support all formats, and Windows fails altogether if unsupported ones are
778
		// used, so let's prevent that. Some substitutions go to the nearest reasonable fallback, some
779
		// turn into static strings, some (i.e. %a, %A, $b, %B, %p) have special handling below.
780
		$strftimeFormatSubstitutions = array(
781
			// Day
782
			'a' => '%a', 'A' => '%A', 'e' => '%d', 'd' => '&#37;d', 'j' => '&#37;j', 'u' => '%w', 'w' => '&#37;w',
783
			// Week
784
			'U' => '&#37;U', 'V' => '%U', 'W' => '%U',
785
			// Month
786
			'b' => '%b', 'B' => '%B', 'h' => '%b', 'm' => '%b',
787
			// Year
788
			'C' => '&#37;C', 'g' => '%y', 'G' => '%Y', 'y' => '&#37;y', 'Y' => '&#37;Y',
789
			// Time
790
			'H' => '&#37;H', 'k' => '%H', 'I' => '%H', 'l' => '%I', 'M' => '&#37;M', 'p' => '%p', 'P' => '%p',
791
			'r' => '%I:%M:%S %p', 'R' => '%H:%M', 'S' => '&#37;S', 'T' => '%H:%M:%S', 'X' => '%T', 'z' => '&#37;z', 'Z' => '&#37;Z',
792
			// Time and Date Stamps
793
			'c' => '%F %T', 'D' => '%m/%d/%y', 'F' => '%Y-%m-%d', 's' => '&#37;s', 'x' => '%F',
794
			// Miscellaneous
795
			'n' => "\n", 't' => "\t", '%' => '&#37;',
796
		);
797
798
		// No need to do this part again if we already did it once
799
		if (is_null($unsupportedFormats))
800
			$unsupportedFormats = (array) cache_get_data('unsupportedtimeformats', 86400);
801
		if (empty($unsupportedFormats))
802
		{
803
			foreach($strftimeFormatSubstitutions as $format => $substitution)
804
			{
805
				$value = @strftime('%' . $format);
806
807
				// Windows will return false for unsupported formats
808
				// Other operating systems return the format string as a literal
809
				if ($value === false || $value === $format)
810
					$unsupportedFormats[] = $format;
811
			}
812
			cache_put_data('unsupportedtimeformats', $unsupportedFormats, 86400);
813
		}
814
815
		// Windows needs extra help if $timeformat contains something completely invalid, e.g. '%Q'
816
		if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN')
817
			$timeformat = preg_replace('~%(?!' . implode('|', array_keys($strftimeFormatSubstitutions)) . ')~', '&#37;', $timeformat);
818
819
		// Substitute unsupported formats with supported ones
820
		if (!empty($unsupportedFormats))
821
			while (preg_match('~%(' . implode('|', $unsupportedFormats) . ')~', $timeformat, $matches))
822
				$timeformat = str_replace($matches[0], $strftimeFormatSubstitutions[$matches[1]], $timeformat);
823
824
		// Remember this so we don't need to do it again
825
		$finalizedFormats[$str] = $timeformat;
826
		cache_put_data('timeformatstrings', $finalizedFormats, 86400);
827
	}
828
829
	$str = $finalizedFormats[$str];
830
831
	if (!isset($locale_cache))
832
		$locale_cache = setlocale(LC_TIME, $txt['lang_locale']);
833
834
	if ($locale_cache !== false)
835
	{
836
		// Check if another process changed the locale
837
		if ($process_safe === true && setlocale(LC_TIME, '0') != $locale_cache)
838
			setlocale(LC_TIME, $txt['lang_locale']);
839
840
		if (!isset($non_twelve_hour))
841
			$non_twelve_hour = trim(strftime('%p')) === '';
842 View Code Duplication
		if ($non_twelve_hour && strpos($str, '%p') !== false)
843
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
844
845
		foreach (array('%a', '%A', '%b', '%B') as $token)
846
			if (strpos($str, $token) !== false)
847
				$str = str_replace($token, strftime($token, $time), $str);
848
	}
849
	else
850
	{
851
		// Do-it-yourself time localization.  Fun.
852
		foreach (array('%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months') as $token => $text_label)
853
			if (strpos($str, $token) !== false)
854
				$str = str_replace($token, $txt[$text_label][(int) strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
855
856 View Code Duplication
		if (strpos($str, '%p') !== false)
857
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
858
	}
859
860
	// Format the time and then restore any literal percent characters
861
	return str_replace('&#37;', '%', strftime($str, $time));
862
}
863
864
/**
865
 * Removes special entities from strings.  Compatibility...
866
 * Should be used instead of html_entity_decode for PHP version compatibility reasons.
867
 *
868
 * - removes the base entities (&lt;, &quot;, etc.) from text.
869
 * - additionally converts &nbsp; and &#039;.
870
 *
871
 * @param string $string A string
872
 * @return string The string without entities
873
 */
874
function un_htmlspecialchars($string)
875
{
876
	global $context;
877
	static $translation = array();
878
879
	// Determine the character set... Default to UTF-8
880
	if (empty($context['character_set']))
881
		$charset = 'UTF-8';
882
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
883
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
884
		$charset = 'ISO-8859-1';
885
	else
886
		$charset = $context['character_set'];
887
888
	if (empty($translation))
889
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
890
891
	return strtr($string, $translation);
892
}
893
894
/**
895
 * Shorten a subject + internationalization concerns.
896
 *
897
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
898
 * - respects internationalization characters and entities as one character.
899
 * - avoids trailing entities.
900
 * - returns the shortened string.
901
 *
902
 * @param string $subject The subject
903
 * @param int $len How many characters to limit it to
904
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
905
 */
906
function shorten_subject($subject, $len)
907
{
908
	global $smcFunc;
909
910
	// It was already short enough!
911
	if ($smcFunc['strlen']($subject) <= $len)
912
		return $subject;
913
914
	// Shorten it by the length it was too long, and strip off junk from the end.
915
	return $smcFunc['substr']($subject, 0, $len) . '...';
916
}
917
918
/**
919
 * Gets the current time with offset.
920
 *
921
 * - always applies the offset in the time_offset setting.
922
 *
923
 * @param bool $use_user_offset Whether to apply the user's offset as well
924
 * @param int $timestamp A timestamp (null to use current time)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $timestamp not be integer|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
925
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|double?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
926
 */
927
function forum_time($use_user_offset = true, $timestamp = null)
928
{
929
	global $user_info, $modSettings;
930
931
	if ($timestamp === null)
932
		$timestamp = time();
933
	elseif ($timestamp == 0)
934
		return 0;
935
936
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
937
}
938
939
/**
940
 * Calculates all the possible permutations (orders) of array.
941
 * should not be called on huge arrays (bigger than like 10 elements.)
942
 * returns an array containing each permutation.
943
 *
944
 * @deprecated since 2.1
945
 * @param array $array An array
946
 * @return array An array containing each permutation
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array[].

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
947
 */
948
function permute($array)
949
{
950
	$orders = array($array);
951
952
	$n = count($array);
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $n. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
953
	$p = range(0, $n);
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $p. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
954
	for ($i = 1; $i < $n; null)
955
	{
956
		$p[$i]--;
957
		$j = $i % 2 != 0 ? $p[$i] : 0;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $j. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
958
959
		$temp = $array[$i];
960
		$array[$i] = $array[$j];
961
		$array[$j] = $temp;
962
963
		for ($i = 1; $p[$i] == 0; $i++)
964
			$p[$i] = 1;
965
966
		$orders[] = $array;
967
	}
968
969
	return $orders;
970
}
971
972
/**
973
 * Parse bulletin board code in a string, as well as smileys optionally.
974
 *
975
 * - only parses bbc tags which are not disabled in disabledBBC.
976
 * - handles basic HTML, if enablePostHTML is on.
977
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
978
 * - only parses smileys if smileys is true.
979
 * - does nothing if the enableBBC setting is off.
980
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
981
 *  -returns the modified message.
982
 *
983
 * @param string $message The message
984
 * @param bool $smileys Whether to parse smileys as well
985
 * @param string $cache_id The cache ID
986
 * @param array $parse_tags If set, only parses these tags rather than all of them
987
 * @return string The parsed message
988
 */
989
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
990
{
991
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir;
992
	static $bbc_codes = array(), $itemcodes = array(), $no_autolink_tags = array();
993
	static $disabled;
994
995
	// Don't waste cycles
996
	if ($message === '')
997
		return '';
998
999
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1000
	if (!isset($context['utf8']))
1001
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1002
1003
	// Clean up any cut/paste issues we may have
1004
	$message = sanitizeMSCutPaste($message);
1005
1006
	// If the load average is too high, don't parse the BBC.
1007
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1008
	{
1009
		$context['disabled_parse_bbc'] = true;
1010
		return $message;
1011
	}
1012
1013
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1014
		$smileys = (bool) $smileys;
1015
1016
	if (empty($modSettings['enableBBC']) && $message !== false)
1017
	{
1018
		if ($smileys === true)
1019
			parsesmileys($message);
1020
1021
		return $message;
1022
	}
1023
1024
	// If we are not doing every tag then we don't cache this run.
1025
	if (!empty($parse_tags) && !empty($bbc_codes))
1026
	{
1027
		$temp_bbc = $bbc_codes;
1028
		$bbc_codes = array();
1029
	}
1030
1031
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1032
	if (!empty($modSettings['autoLinkUrls']))
1033
		set_tld_regex();
1034
1035
	// Allow mods access before entering the main parse_bbc loop
1036
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1037
1038
	// Sift out the bbc for a performance improvement.
1039
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1040
	{
1041
		if (!empty($modSettings['disabledBBC']))
1042
		{
1043
			$disabled = array();
1044
1045
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1046
1047
			foreach ($temp as $tag)
1048
				$disabled[trim($tag)] = true;
1049
		}
1050
1051
		if (empty($modSettings['enableEmbeddedFlash']))
1052
			$disabled['flash'] = true;
1053
1054
		/* The following bbc are formatted as an array, with keys as follows:
1055
1056
			tag: the tag's name - should be lowercase!
1057
1058
			type: one of...
1059
				- (missing): [tag]parsed content[/tag]
1060
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1061
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1062
				- unparsed_content: [tag]unparsed content[/tag]
1063
				- closed: [tag], [tag/], [tag /]
1064
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1065
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1066
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1067
1068
			parameters: an optional array of parameters, for the form
1069
			  [tag abc=123]content[/tag].  The array is an associative array
1070
			  where the keys are the parameter names, and the values are an
1071
			  array which may contain the following:
1072
				- match: a regular expression to validate and match the value.
1073
				- quoted: true if the value should be quoted.
1074
				- validate: callback to evaluate on the data, which is $data.
1075
				- value: a string in which to replace $1 with the data.
1076
				  either it or validate may be used, not both.
1077
				- optional: true if the parameter is optional.
1078
1079
			test: a regular expression to test immediately after the tag's
1080
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1081
			  Optional.
1082
1083
			content: only available for unparsed_content, closed,
1084
			  unparsed_commas_content, and unparsed_equals_content.
1085
			  $1 is replaced with the content of the tag.  Parameters
1086
			  are replaced in the form {param}.  For unparsed_commas_content,
1087
			  $2, $3, ..., $n are replaced.
1088
1089
			before: only when content is not used, to go before any
1090
			  content.  For unparsed_equals, $1 is replaced with the value.
1091
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1092
1093
			after: similar to before in every way, except that it is used
1094
			  when the tag is closed.
1095
1096
			disabled_content: used in place of content when the tag is
1097
			  disabled.  For closed, default is '', otherwise it is '$1' if
1098
			  block_level is false, '<div>$1</div>' elsewise.
1099
1100
			disabled_before: used in place of before when disabled.  Defaults
1101
			  to '<div>' if block_level, '' if not.
1102
1103
			disabled_after: used in place of after when disabled.  Defaults
1104
			  to '</div>' if block_level, '' if not.
1105
1106
			block_level: set to true the tag is a "block level" tag, similar
1107
			  to HTML.  Block level tags cannot be nested inside tags that are
1108
			  not block level, and will not be implicitly closed as easily.
1109
			  One break following a block level tag may also be removed.
1110
1111
			trim: if set, and 'inside' whitespace after the begin tag will be
1112
			  removed.  If set to 'outside', whitespace after the end tag will
1113
			  meet the same fate.
1114
1115
			validate: except when type is missing or 'closed', a callback to
1116
			  validate the data as $data.  Depending on the tag's type, $data
1117
			  may be a string or an array of strings (corresponding to the
1118
			  replacement.)
1119
1120
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1121
			  may be not set, 'optional', or 'required' corresponding to if
1122
			  the content may be quoted.  This allows the parser to read
1123
			  [tag="abc]def[esdf]"] properly.
1124
1125
			require_parents: an array of tag names, or not set.  If set, the
1126
			  enclosing tag *must* be one of the listed tags, or parsing won't
1127
			  occur.
1128
1129
			require_children: similar to require_parents, if set children
1130
			  won't be parsed if they are not in the list.
1131
1132
			disallow_children: similar to, but very different from,
1133
			  require_children, if it is set the listed tags will not be
1134
			  parsed inside the tag.
1135
1136
			parsed_tags_allowed: an array restricting what BBC can be in the
1137
			  parsed_equals parameter, if desired.
1138
		*/
1139
1140
		$codes = array(
1141
			array(
1142
				'tag' => 'abbr',
1143
				'type' => 'unparsed_equals',
1144
				'before' => '<abbr title="$1">',
1145
				'after' => '</abbr>',
1146
				'quoted' => 'optional',
1147
				'disabled_after' => ' ($1)',
1148
			),
1149
			array(
1150
				'tag' => 'anchor',
1151
				'type' => 'unparsed_equals',
1152
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1153
				'before' => '<span id="post_$1">',
1154
				'after' => '</span>',
1155
			),
1156
			array(
1157
				'tag' => 'attach',
1158
				'type' => 'unparsed_content',
1159
				'parameters' => array(
1160
					'name' => array('optional' => true),
1161
					'type' => array('optional' => true),
1162
					'alt' => array('optional' => true),
1163
					'title' => array('optional' => true),
1164
					'width' => array('optional' => true, 'match' => '(\d+)'),
1165
					'height' => array('optional' => true, 'match' => '(\d+)'),
1166
				),
1167
				'content' => '$1',
1168
				'validate' => function (&$tag, &$data, $disabled, $params) use ($modSettings, $context, $sourcedir, $txt)
1169
				{
1170
					$returnContext = '';
1171
1172
					// BBC or the entire attachments feature is disabled
1173
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1174
						return $data;
1175
1176
					// Save the attach ID.
1177
					$attachID = $data;
1178
1179
					// Kinda need this.
1180
					require_once($sourcedir . '/Subs-Attachments.php');
1181
1182
					$currentAttachment = parseAttachBBC($attachID);
1183
1184
					// parseAttachBBC will return a string ($txt key) rather than diying with a fatal_error. Up to you to decide what to do.
1185
					if (is_string($currentAttachment))
1186
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1187
1188
					if (!empty($currentAttachment['is_image']))
1189
					{
1190
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1191
						$title = !empty($params['{title}']) ? ' title="' . $params['{title}'] . '"' : '';
1192
1193
						$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1194
						$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1195
1196
						if (empty($width) && empty($height))
1197
						{
1198
							$width = ' width="' . $currentAttachment['width'] . '"';
1199
							$height = ' height="' . $currentAttachment['height'] . '"';
1200
						}
1201
1202
						if ($currentAttachment['thumbnail']['has_thumb'] && empty($params['{width}']) && empty($params['{height}']))
1203
							$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>';
1204
						else
1205
							$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img"/>';
1206
					}
1207
1208
					// No image. Show a link.
1209
					else
1210
						$returnContext .= $currentAttachment['link'];
1211
1212
					// Gotta append what we just did.
1213
					$data = $returnContext;
1214
				},
1215
			),
1216
			array(
1217
				'tag' => 'b',
1218
				'before' => '<b>',
1219
				'after' => '</b>',
1220
			),
1221
			array(
1222
				'tag' => 'center',
1223
				'before' => '<div class="centertext">',
1224
				'after' => '</div>',
1225
				'block_level' => true,
1226
			),
1227
			array(
1228
				'tag' => 'code',
1229
				'type' => 'unparsed_content',
1230
				'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>',
1231
				// @todo Maybe this can be simplified?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1232
				'validate' => isset($disabled['code']) ? null : function (&$tag, &$data, $disabled) use ($context)
1233
				{
1234
					if (!isset($disabled['code']))
1235
					{
1236
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1237
1238 View Code Duplication
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1239
						{
1240
							// Do PHP code coloring?
1241
							if ($php_parts[$php_i] != '&lt;?php')
1242
								continue;
1243
1244
							$php_string = '';
1245
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1246
							{
1247
								$php_string .= $php_parts[$php_i];
1248
								$php_parts[$php_i++] = '';
1249
							}
1250
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1251
						}
1252
1253
						// Fix the PHP code stuff...
1254
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1255
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1256
1257
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1258
						if ($context['browser']['is_opera'])
1259
							$data .= '&nbsp;';
1260
					}
1261
				},
1262
				'block_level' => true,
1263
			),
1264
			array(
1265
				'tag' => 'code',
1266
				'type' => 'unparsed_equals_content',
1267
				'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>',
1268
				// @todo Maybe this can be simplified?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1269
				'validate' => isset($disabled['code']) ? null : function (&$tag, &$data, $disabled) use ($context)
1270
				{
1271
					if (!isset($disabled['code']))
1272
					{
1273
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1274
1275 View Code Duplication
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1276
						{
1277
							// Do PHP code coloring?
1278
							if ($php_parts[$php_i] != '&lt;?php')
1279
								continue;
1280
1281
							$php_string = '';
1282
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1283
							{
1284
								$php_string .= $php_parts[$php_i];
1285
								$php_parts[$php_i++] = '';
1286
							}
1287
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1288
						}
1289
1290
						// Fix the PHP code stuff...
1291
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1292
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1293
1294
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1295
						if ($context['browser']['is_opera'])
1296
							$data[0] .= '&nbsp;';
1297
					}
1298
				},
1299
				'block_level' => true,
1300
			),
1301
			array(
1302
				'tag' => 'color',
1303
				'type' => 'unparsed_equals',
1304
				'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]?)\))\]',
1305
				'before' => '<span style="color: $1;" class="bbc_color">',
1306
				'after' => '</span>',
1307
			),
1308
			array(
1309
				'tag' => 'email',
1310
				'type' => 'unparsed_content',
1311
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1312
				// @todo Should this respect guest_hideContacts?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1313
				'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...
1314
				{
1315
					$data = strtr($data, array('<br>' => ''));
1316
				},
1317
			),
1318
			array(
1319
				'tag' => 'email',
1320
				'type' => 'unparsed_equals',
1321
				'before' => '<a href="mailto:$1" class="bbc_email">',
1322
				'after' => '</a>',
1323
				// @todo Should this respect guest_hideContacts?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1324
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1325
				'disabled_after' => ' ($1)',
1326
			),
1327
			array(
1328
				'tag' => 'flash',
1329
				'type' => 'unparsed_commas_content',
1330
				'test' => '\d+,\d+\]',
1331
				'content' => '<embed type="application/x-shockwave-flash" src="$1" width="$2" height="$3" play="true" loop="true" quality="high" AllowScriptAccess="never">',
1332
				'validate' => function (&$tag, &$data, $disabled)
1333
				{
1334
					if (isset($disabled['url']))
1335
						$tag['content'] = '$1';
1336
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1337
					if (empty($scheme))
1338
						$data[0] = '//' . ltrim($data[0], ':/');
1339
				},
1340
				'disabled_content' => '<a href="$1" target="_blank" class="new_win">$1</a>',
1341
			),
1342
			array(
1343
				'tag' => 'float',
1344
				'type' => 'unparsed_equals',
1345
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1346
				'before' => '<div $1>',
1347
				'after' => '</div>',
1348
				'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...
1349
				{
1350
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1351
1352
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1353
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1354
					else
1355
						$css = '';
1356
1357
					$data = $class . $css;
1358
				},
1359
				'trim' => 'outside',
1360
				'block_level' => true,
1361
			),
1362
			array(
1363
				'tag' => 'font',
1364
				'type' => 'unparsed_equals',
1365
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1366
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1367
				'after' => '</span>',
1368
			),
1369
			array(
1370
				'tag' => 'html',
1371
				'type' => 'unparsed_content',
1372
				'content' => '<div>$1</div>',
1373
				'block_level' => true,
1374
				'disabled_content' => '$1',
1375
			),
1376
			array(
1377
				'tag' => 'hr',
1378
				'type' => 'closed',
1379
				'content' => '<hr>',
1380
				'block_level' => true,
1381
			),
1382
			array(
1383
				'tag' => 'i',
1384
				'before' => '<i>',
1385
				'after' => '</i>',
1386
			),
1387
			array(
1388
				'tag' => 'img',
1389
				'type' => 'unparsed_content',
1390
				'parameters' => array(
1391
					'alt' => array('optional' => true),
1392
					'title' => array('optional' => true),
1393
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1394
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1395
				),
1396
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized">',
1397 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...
1398
				{
1399
					global $image_proxy_enabled, $image_proxy_secret, $boardurl;
1400
1401
					$data = strtr($data, array('<br>' => ''));
1402
					$scheme = parse_url($data, PHP_URL_SCHEME);
1403
					if ($image_proxy_enabled)
1404
					{
1405
						if (empty($scheme))
1406
							$data = 'http://' . ltrim($data, ':/');
1407
1408
						if ($scheme != 'https')
1409
							$data = $boardurl . '/proxy.php?request=' . urlencode($data) . '&hash=' . md5($data . $image_proxy_secret);
1410
					}
1411
					elseif (empty($scheme))
1412
						$data = '//' . ltrim($data, ':/');
1413
				},
1414
				'disabled_content' => '($1)',
1415
			),
1416
			array(
1417
				'tag' => 'img',
1418
				'type' => 'unparsed_content',
1419
				'content' => '<img src="$1" alt="" class="bbc_img">',
1420 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...
1421
				{
1422
					global $image_proxy_enabled, $image_proxy_secret, $boardurl;
1423
1424
					$data = strtr($data, array('<br>' => ''));
1425
					$scheme = parse_url($data, PHP_URL_SCHEME);
1426
					if ($image_proxy_enabled)
1427
					{
1428
						if (empty($scheme))
1429
							$data = 'http://' . ltrim($data, ':/');
1430
1431
						if ($scheme != 'https')
1432
							$data = $boardurl . '/proxy.php?request=' . urlencode($data) . '&hash=' . md5($data . $image_proxy_secret);
1433
					}
1434
					elseif (empty($scheme))
1435
						$data = '//' . ltrim($data, ':/');
1436
				},
1437
				'disabled_content' => '($1)',
1438
			),
1439
			array(
1440
				'tag' => 'iurl',
1441
				'type' => 'unparsed_content',
1442
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1443 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...
1444
				{
1445
					$data = strtr($data, array('<br>' => ''));
1446
					$scheme = parse_url($data, PHP_URL_SCHEME);
1447
					if (empty($scheme))
1448
						$data = '//' . ltrim($data, ':/');
1449
				},
1450
			),
1451
			array(
1452
				'tag' => 'iurl',
1453
				'type' => 'unparsed_equals',
1454
				'quoted' => 'optional',
1455
				'before' => '<a href="$1" class="bbc_link">',
1456
				'after' => '</a>',
1457
				'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...
1458
				{
1459
					if (substr($data, 0, 1) == '#')
1460
						$data = '#post_' . substr($data, 1);
1461
					else
1462
					{
1463
						$scheme = parse_url($data, PHP_URL_SCHEME);
1464
						if (empty($scheme))
1465
							$data = '//' . ltrim($data, ':/');
1466
					}
1467
				},
1468
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1469
				'disabled_after' => ' ($1)',
1470
			),
1471
			array(
1472
				'tag' => 'left',
1473
				'before' => '<div style="text-align: left;">',
1474
				'after' => '</div>',
1475
				'block_level' => true,
1476
			),
1477
			array(
1478
				'tag' => 'li',
1479
				'before' => '<li>',
1480
				'after' => '</li>',
1481
				'trim' => 'outside',
1482
				'require_parents' => array('list'),
1483
				'block_level' => true,
1484
				'disabled_before' => '',
1485
				'disabled_after' => '<br>',
1486
			),
1487
			array(
1488
				'tag' => 'list',
1489
				'before' => '<ul class="bbc_list">',
1490
				'after' => '</ul>',
1491
				'trim' => 'inside',
1492
				'require_children' => array('li', 'list'),
1493
				'block_level' => true,
1494
			),
1495
			array(
1496
				'tag' => 'list',
1497
				'parameters' => array(
1498
					'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)'),
1499
				),
1500
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1501
				'after' => '</ul>',
1502
				'trim' => 'inside',
1503
				'require_children' => array('li'),
1504
				'block_level' => true,
1505
			),
1506
			array(
1507
				'tag' => 'ltr',
1508
				'before' => '<bdo dir="ltr">',
1509
				'after' => '</bdo>',
1510
				'block_level' => true,
1511
			),
1512
			array(
1513
				'tag' => 'me',
1514
				'type' => 'unparsed_equals',
1515
				'before' => '<div class="meaction">* $1 ',
1516
				'after' => '</div>',
1517
				'quoted' => 'optional',
1518
				'block_level' => true,
1519
				'disabled_before' => '/me ',
1520
				'disabled_after' => '<br>',
1521
			),
1522
			array(
1523
				'tag' => 'member',
1524
				'type' => 'unparsed_equals',
1525
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1526
				'after' => '</a>',
1527
			),
1528
			array(
1529
				'tag' => 'nobbc',
1530
				'type' => 'unparsed_content',
1531
				'content' => '$1',
1532
			),
1533
			array(
1534
				'tag' => 'php',
1535
				'type' => 'unparsed_content',
1536
				'content' => '<span class="phpcode">$1</span>',
1537
				'validate' => isset($disabled['php']) ? null : function (&$tag, &$data, $disabled)
1538
				{
1539
					if (!isset($disabled['php']))
1540
					{
1541
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1542
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1543
						if ($add_begin)
1544
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1545
					}
1546
				},
1547
				'block_level' => false,
1548
				'disabled_content' => '$1',
1549
			),
1550
			array(
1551
				'tag' => 'pre',
1552
				'before' => '<pre>',
1553
				'after' => '</pre>',
1554
			),
1555
			array(
1556
				'tag' => 'quote',
1557
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1558
				'after' => '</blockquote>',
1559
				'trim' => 'both',
1560
				'block_level' => true,
1561
			),
1562
			array(
1563
				'tag' => 'quote',
1564
				'parameters' => array(
1565
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1566
				),
1567
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1568
				'after' => '</blockquote>',
1569
				'trim' => 'both',
1570
				'block_level' => true,
1571
			),
1572
			array(
1573
				'tag' => 'quote',
1574
				'type' => 'parsed_equals',
1575
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1576
				'after' => '</blockquote>',
1577
				'trim' => 'both',
1578
				'quoted' => 'optional',
1579
				// Don't allow everything to be embedded with the author name.
1580
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1581
				'block_level' => true,
1582
			),
1583
			array(
1584
				'tag' => 'quote',
1585
				'parameters' => array(
1586
					'author' => array('match' => '([^<>]{1,192}?)'),
1587
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1588
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1589
				),
1590
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1591
				'after' => '</blockquote>',
1592
				'trim' => 'both',
1593
				'block_level' => true,
1594
			),
1595
			array(
1596
				'tag' => 'quote',
1597
				'parameters' => array(
1598
					'author' => array('match' => '(.{1,192}?)'),
1599
				),
1600
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1601
				'after' => '</blockquote>',
1602
				'trim' => 'both',
1603
				'block_level' => true,
1604
			),
1605
			array(
1606
				'tag' => 'right',
1607
				'before' => '<div style="text-align: right;">',
1608
				'after' => '</div>',
1609
				'block_level' => true,
1610
			),
1611
			array(
1612
				'tag' => 'rtl',
1613
				'before' => '<bdo dir="rtl">',
1614
				'after' => '</bdo>',
1615
				'block_level' => true,
1616
			),
1617
			array(
1618
				'tag' => 's',
1619
				'before' => '<s>',
1620
				'after' => '</s>',
1621
			),
1622
			array(
1623
				'tag' => 'size',
1624
				'type' => 'unparsed_equals',
1625
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
1626
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1627
				'after' => '</span>',
1628
			),
1629
			array(
1630
				'tag' => 'size',
1631
				'type' => 'unparsed_equals',
1632
				'test' => '[1-7]\]',
1633
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1634
				'after' => '</span>',
1635
				'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...
1636
				{
1637
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
1638
					$data = $sizes[$data] . 'em';
1639
				},
1640
			),
1641
			array(
1642
				'tag' => 'sub',
1643
				'before' => '<sub>',
1644
				'after' => '</sub>',
1645
			),
1646
			array(
1647
				'tag' => 'sup',
1648
				'before' => '<sup>',
1649
				'after' => '</sup>',
1650
			),
1651
			array(
1652
				'tag' => 'table',
1653
				'before' => '<table class="bbc_table">',
1654
				'after' => '</table>',
1655
				'trim' => 'inside',
1656
				'require_children' => array('tr'),
1657
				'block_level' => true,
1658
			),
1659
			array(
1660
				'tag' => 'td',
1661
				'before' => '<td>',
1662
				'after' => '</td>',
1663
				'require_parents' => array('tr'),
1664
				'trim' => 'outside',
1665
				'block_level' => true,
1666
				'disabled_before' => '',
1667
				'disabled_after' => '',
1668
			),
1669
			array(
1670
				'tag' => 'time',
1671
				'type' => 'unparsed_content',
1672
				'content' => '$1',
1673
				'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...
1674
				{
1675
					if (is_numeric($data))
1676
						$data = timeformat($data);
1677
					else
1678
						$tag['content'] = '[time]$1[/time]';
1679
				},
1680
			),
1681
			array(
1682
				'tag' => 'tr',
1683
				'before' => '<tr>',
1684
				'after' => '</tr>',
1685
				'require_parents' => array('table'),
1686
				'require_children' => array('td'),
1687
				'trim' => 'both',
1688
				'block_level' => true,
1689
				'disabled_before' => '',
1690
				'disabled_after' => '',
1691
			),
1692
			array(
1693
				'tag' => 'u',
1694
				'before' => '<u>',
1695
				'after' => '</u>',
1696
			),
1697
			array(
1698
				'tag' => 'url',
1699
				'type' => 'unparsed_content',
1700
				'content' => '<a href="$1" class="bbc_link" target="_blank">$1</a>',
1701 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...
1702
				{
1703
					$data = strtr($data, array('<br>' => ''));
1704
					$scheme = parse_url($data, PHP_URL_SCHEME);
1705
					if (empty($scheme))
1706
						$data = '//' . ltrim($data, ':/');
1707
				},
1708
			),
1709
			array(
1710
				'tag' => 'url',
1711
				'type' => 'unparsed_equals',
1712
				'quoted' => 'optional',
1713
				'before' => '<a href="$1" class="bbc_link" target="_blank">',
1714
				'after' => '</a>',
1715
				'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...
1716
				{
1717
					$scheme = parse_url($data, PHP_URL_SCHEME);
1718
					if (empty($scheme))
1719
						$data = '//' . ltrim($data, ':/');
1720
				},
1721
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1722
				'disabled_after' => ' ($1)',
1723
			),
1724
		);
1725
1726
		// Inside these tags autolink is not recommendable.
1727
		$no_autolink_tags = array(
1728
			'url',
1729
			'iurl',
1730
			'email',
1731
		);
1732
1733
		// Let mods add new BBC without hassle.
1734
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
1735
1736
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
1737
		if ($message === false)
1738
		{
1739
			if (isset($temp_bbc))
1740
				$bbc_codes = $temp_bbc;
1741
			usort($codes, function ($a, $b) {
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $a. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
Comprehensibility introduced by
Avoid variables with short names like $b. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
1742
				return strcmp($a['tag'], $b['tag']);
1743
			});
1744
			return $codes;
1745
		}
1746
1747
		// So the parser won't skip them.
1748
		$itemcodes = array(
1749
			'*' => 'disc',
1750
			'@' => 'disc',
1751
			'+' => 'square',
1752
			'x' => 'square',
1753
			'#' => 'square',
1754
			'o' => 'circle',
1755
			'O' => 'circle',
1756
			'0' => 'circle',
1757
		);
1758
		if (!isset($disabled['li']) && !isset($disabled['list']))
1759
		{
1760
			foreach ($itemcodes as $c => $dummy)
1761
				$bbc_codes[$c] = array();
1762
		}
1763
1764
		// Shhhh!
1765
		if (!isset($disabled['color']))
1766
		{
1767
			$codes[] = array(
1768
				'tag' => 'chrissy',
1769
				'before' => '<span style="color: #cc0099;">',
1770
				'after' => ' :-*</span>',
1771
			);
1772
			$codes[] = array(
1773
				'tag' => 'kissy',
1774
				'before' => '<span style="color: #cc0099;">',
1775
				'after' => ' :-*</span>',
1776
			);
1777
		}
1778
1779
		foreach ($codes as $code)
1780
		{
1781
			// Make it easier to process parameters later
1782
			if (!empty($code['parameters']))
1783
				ksort($code['parameters'], SORT_STRING);
1784
1785
			// If we are not doing every tag only do ones we are interested in.
1786
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
1787
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
1788
		}
1789
		$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...
1790
	}
1791
1792
	// Shall we take the time to cache this?
1793
	if ($cache_id != '' && !empty($modSettings['cache_enable']) && (($modSettings['cache_enable'] >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
1794
	{
1795
		// It's likely this will change if the message is modified.
1796
		$cache_key = 'parse:' . $cache_id . '-' . md5(md5($message) . '-' . $smileys . (empty($disabled) ? '' : implode(',', array_keys($disabled))) . $smcFunc['json_encode']($context['browser']) . $txt['lang_locale'] . $user_info['time_offset'] . $user_info['time_format']);
1797
1798
		if (($temp = cache_get_data($cache_key, 240)) != null)
1799
			return $temp;
1800
1801
		$cache_t = microtime();
1802
	}
1803
1804
	if ($smileys === 'print')
1805
	{
1806
		// [glow], [shadow], and [move] can't really be printed.
1807
		$disabled['glow'] = true;
1808
		$disabled['shadow'] = true;
1809
		$disabled['move'] = true;
1810
1811
		// Colors can't well be displayed... supposed to be black and white.
1812
		$disabled['color'] = true;
1813
		$disabled['black'] = true;
1814
		$disabled['blue'] = true;
1815
		$disabled['white'] = true;
1816
		$disabled['red'] = true;
1817
		$disabled['green'] = true;
1818
		$disabled['me'] = true;
1819
1820
		// Color coding doesn't make sense.
1821
		$disabled['php'] = true;
1822
1823
		// Links are useless on paper... just show the link.
1824
		$disabled['ftp'] = true;
1825
		$disabled['url'] = true;
1826
		$disabled['iurl'] = true;
1827
		$disabled['email'] = true;
1828
		$disabled['flash'] = true;
1829
1830
		// @todo Change maybe?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1831
		if (!isset($_GET['images']))
1832
			$disabled['img'] = true;
1833
1834
		// @todo Interface/setting to add more?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1835
	}
1836
1837
	$open_tags = array();
1838
	$message = strtr($message, array("\n" => '<br>'));
1839
1840
	$alltags = array();
1841
	foreach ($bbc_codes as $section) {
1842
		foreach ($section as $code) {
1843
			$alltags[] = $code['tag'];
1844
		}
1845
	}
1846
	$alltags_regex = '\b' . implode("\b|\b", array_unique($alltags)) . '\b';
1847
1848
	$pos = -1;
1849
	while ($pos !== false)
1850
	{
1851
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
1852
		preg_match('~\[/?(?=' . $alltags_regex . ')~', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
1853
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
1854
1855
		// Failsafe.
1856
		if ($pos === false || $last_pos > $pos)
1857
			$pos = strlen($message) + 1;
1858
1859
		// Can't have a one letter smiley, URL, or email! (sorry.)
1860
		if ($last_pos < $pos - 1)
1861
		{
1862
			// Make sure the $last_pos is not negative.
1863
			$last_pos = max($last_pos, 0);
1864
1865
			// Pick a block of data to do some raw fixing on.
1866
			$data = substr($message, $last_pos, $pos - $last_pos);
1867
1868
			// Take care of some HTML!
1869
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
1870
			{
1871
				$data = preg_replace('~&lt;a\s+href=((?:&quot;)?)((?:https?://|ftps?://|mailto:)\S+?)\\1&gt;~i', '[url=&quot;$2&quot;]', $data);
1872
				$data = preg_replace('~&lt;/a&gt;~i', '[/url]', $data);
1873
1874
				// <br> should be empty.
1875
				$empty_tags = array('br', 'hr');
1876
				foreach ($empty_tags as $tag)
1877
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '[' . $tag . ' /]', $data);
1878
1879
				// b, u, i, s, pre... basic tags.
1880
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote');
1881
				foreach ($closable_tags as $tag)
1882
				{
1883
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
1884
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
1885
1886
					if ($diff > 0)
1887
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
1888
				}
1889
1890
				// Do <img ...> - with security... action= -> action-.
1891
				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);
1892
				if (!empty($matches[0]))
1893
				{
1894
					$replaces = array();
1895
					foreach ($matches[2] as $match => $imgtag)
1896
					{
1897
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
1898
1899
						// Remove action= from the URL - no funny business, now.
1900
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
1901
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
1902
1903
						// Check if the image is larger than allowed.
1904
						if (!empty($modSettings['max_image_width']) && !empty($modSettings['max_image_height']))
1905
						{
1906
							list ($width, $height) = url_image_size($imgtag);
1907
1908 View Code Duplication
							if (!empty($modSettings['max_image_width']) && $width > $modSettings['max_image_width'])
1909
							{
1910
								$height = (int) (($modSettings['max_image_width'] * $height) / $width);
1911
								$width = $modSettings['max_image_width'];
1912
							}
1913
1914 View Code Duplication
							if (!empty($modSettings['max_image_height']) && $height > $modSettings['max_image_height'])
1915
							{
1916
								$width = (int) (($modSettings['max_image_height'] * $width) / $height);
1917
								$height = $modSettings['max_image_height'];
1918
							}
1919
1920
							// Set the new image tag.
1921
							$replaces[$matches[0][$match]] = '[img width=' . $width . ' height=' . $height . $alt . ']' . $imgtag . '[/img]';
1922
						}
1923
						else
1924
							$replaces[$matches[0][$match]] = '[img' . $alt . ']' . $imgtag . '[/img]';
1925
					}
1926
1927
					$data = strtr($data, $replaces);
1928
				}
1929
			}
1930
1931
			if (!empty($modSettings['autoLinkUrls']))
1932
			{
1933
				// Are we inside tags that should be auto linked?
1934
				$no_autolink_area = false;
1935
				if (!empty($open_tags))
1936
				{
1937
					foreach ($open_tags as $open_tag)
1938
						if (in_array($open_tag['tag'], $no_autolink_tags))
1939
							$no_autolink_area = true;
1940
				}
1941
1942
				// Don't go backwards.
1943
				// @todo Don't think is the real solution....
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
1944
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
1945
				if ($pos < $lastAutoPos)
1946
					$no_autolink_area = true;
1947
				$lastAutoPos = $pos;
1948
1949
				if (!$no_autolink_area)
1950
				{
1951
					// Parse any URLs
1952
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
1953
					{
1954
						$url_regex = '
1955
						(?:
1956
							# IRIs with a scheme (or at least an opening "//")
1957
							(?:
1958
								# URI scheme (or lack thereof for schemeless URLs)
1959
								(?:
1960
									# URL scheme and colon
1961
									\b[a-z][\w\-]+:
1962
									| # or
1963
									# A boundary followed by two slashes for schemeless URLs
1964
									(?<=^|\W)(?=//)
1965
								)
1966
1967
								# IRI "authority" chunk
1968
								(?:
1969
									# 2 slashes for IRIs with an "authority"
1970
									//
1971
									# then a domain name
1972
									(?:
1973
										# Either the reserved "localhost" domain name
1974
										localhost
1975
										| # or
1976
										# a run of Unicode domain name characters and a dot
1977
										[\p{L}\p{M}\p{N}\-.:@]+\.
1978
										# and then a TLD valid in the DNS or the reserved "local" TLD
1979
										(?:'. $modSettings['tld_regex'] .'|local)
1980
									)
1981
									# followed by a non-domain character or end of line
1982
									(?=[^\p{L}\p{N}\-.]|$)
1983
1984
									| # Or, if there is no "authority" per se (e.g. mailto: URLs) ...
1985
1986
									# a run of IRI characters
1987
									[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]
1988
									# and then a dot and a closing IRI label
1989
									\.[\p{L}\p{M}\p{N}\-]+
1990
								)
1991
							)
1992
1993
							| # or
1994
1995
							# Naked domains (e.g. "example.com" in "Go to example.com for an example.")
1996
							(?:
1997
								# Preceded by start of line or a non-domain character
1998
								(?<=^|[^\p{L}\p{M}\p{N}\-:@])
1999
2000
								# A run of Unicode domain name characters (excluding [:@])
2001
								[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]
2002
								# and then a dot and a valid TLD
2003
								\.' . $modSettings['tld_regex'] . '
2004
2005
								# Followed by either:
2006
								(?=
2007
									# end of line or a non-domain character (excluding [.:@])
2008
									$|[^\p{L}\p{N}\-]
2009
									| # or
2010
									# a dot followed by end of line or a non-domain character (excluding [.:@])
2011
									\.(?=$|[^\p{L}\p{N}\-])
2012
								)
2013
							)
2014
						)
2015
2016
						# IRI path, query, and fragment (if present)
2017
						(?:
2018
							# If any of these parts exist, must start with a single /
2019
							/
2020
2021
							# And then optionally:
2022
							(?:
2023
								# One or more of:
2024
								(?:
2025
									# a run of non-space, non-()<>
2026
									[^\s()<>]+
2027
									| # or
2028
									# balanced parens, up to 2 levels
2029
									\(([^\s()<>]+|(\([^\s()<>]+\)))*\)
2030
								)+
2031
2032
								# End with:
2033
								(?:
2034
									# balanced parens, up to 2 levels
2035
									\(([^\s()<>]+|(\([^\s()<>]+\)))*\)
2036
									| # or
2037
									# not a space or one of these punct char
2038
									[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]
2039
									| # or
2040
									# a trailing slash (but not two in a row)
2041
									(?<!/)/
2042
								)
2043
							)?
2044
						)?
2045
						';
2046
2047
						$data = preg_replace_callback('~' . $url_regex . '~xi' . ($context['utf8'] ? 'u' : ''), function ($matches) {
2048
							$url = array_shift($matches);
2049
2050
							$scheme = parse_url($url, PHP_URL_SCHEME);
2051
2052
							if ($scheme == 'mailto')
2053
							{
2054
								$email_address = str_replace('mailto:', '', $url);
2055
								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...
2056
									return '[email=' . $email_address . ']' . $url . '[/email]';
2057
								else
2058
									return $url;
2059
							}
2060
2061
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2062
							if (empty($scheme))
2063
								$fullUrl = '//' . ltrim($url, ':/');
2064
							else
2065
								$fullUrl = $url;
2066
2067
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2068
						}, $data);
2069
					}
2070
2071
					// Next, emails...
2072
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false)
2073
					{
2074
						$email_regex = '
2075
						# Preceded by a non-domain character or start of line
2076
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
2077
2078
						# An email address
2079
						[\p{L}\p{M}\p{N}_\-.]{1,80}
2080
						@
2081
						[\p{L}\p{M}\p{N}\-.]+
2082
						\.
2083
						'. $modSettings['tld_regex'] . '
2084
2085
						# Followed by either:
2086
						(?=
2087
							# end of line or a non-domain character (excluding the dot)
2088
							$|[^\p{L}\p{M}\p{N}\-]
2089
							| # or
2090
							# a dot followed by end of line or a non-domain character
2091
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2092
						)';
2093
2094
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2095
					}
2096
				}
2097
			}
2098
2099
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2100
2101
			// If it wasn't changed, no copying or other boring stuff has to happen!
2102
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2103
			{
2104
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2105
2106
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2107
				$old_pos = strlen($data) + $last_pos;
2108
				$pos = strpos($message, '[', $last_pos);
2109
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2110
			}
2111
		}
2112
2113
		// Are we there yet?  Are we there yet?
2114
		if ($pos >= strlen($message) - 1)
2115
			break;
2116
2117
		$tags = strtolower($message[$pos + 1]);
2118
2119
		if ($tags == '/' && !empty($open_tags))
2120
		{
2121
			$pos2 = strpos($message, ']', $pos + 1);
2122
			if ($pos2 == $pos + 2)
2123
				continue;
2124
2125
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2126
2127
			$to_close = array();
2128
			$block_level = null;
2129
2130
			do
2131
			{
2132
				$tag = array_pop($open_tags);
2133
				if (!$tag)
2134
					break;
2135
2136
				if (!empty($tag['block_level']))
2137
				{
2138
					// Only find out if we need to.
2139
					if ($block_level === false)
2140
					{
2141
						array_push($open_tags, $tag);
2142
						break;
2143
					}
2144
2145
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2146 View Code Duplication
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2147
					{
2148
						foreach ($bbc_codes[$look_for[0]] as $temp)
2149
							if ($temp['tag'] == $look_for)
2150
							{
2151
								$block_level = !empty($temp['block_level']);
2152
								break;
2153
							}
2154
					}
2155
2156
					if ($block_level !== true)
2157
					{
2158
						$block_level = false;
2159
						array_push($open_tags, $tag);
2160
						break;
2161
					}
2162
				}
2163
2164
				$to_close[] = $tag;
2165
			}
2166
			while ($tag['tag'] != $look_for);
2167
2168
			// Did we just eat through everything and not find it?
2169
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2170
			{
2171
				$open_tags = $to_close;
2172
				continue;
2173
			}
2174
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2175
			{
2176 View Code Duplication
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2177
				{
2178
					foreach ($bbc_codes[$look_for[0]] as $temp)
2179
						if ($temp['tag'] == $look_for)
2180
						{
2181
							$block_level = !empty($temp['block_level']);
2182
							break;
2183
						}
2184
				}
2185
2186
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2187
				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...
2188
				{
2189
					foreach ($to_close as $tag)
2190
						array_push($open_tags, $tag);
2191
					continue;
2192
				}
2193
			}
2194
2195
			foreach ($to_close as $tag)
2196
			{
2197
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2198
				$pos += strlen($tag['after']) + 2;
2199
				$pos2 = $pos - 1;
2200
2201
				// See the comment at the end of the big loop - just eating whitespace ;).
2202
				$whitespace_regex = '';
2203
				if (!empty($tag['block_level']))
2204
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2205
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2206 View Code Duplication
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2207
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2208
2209 View Code Duplication
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2210
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2211
			}
2212
2213
			if (!empty($to_close))
2214
			{
2215
				$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...
2216
				$pos--;
2217
			}
2218
2219
			continue;
2220
		}
2221
2222
		// No tags for this character, so just keep going (fastest possible course.)
2223
		if (!isset($bbc_codes[$tags]))
2224
			continue;
2225
2226
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2227
		$tag = null;
2228
		foreach ($bbc_codes[$tags] as $possible)
2229
		{
2230
			$pt_strlen = strlen($possible['tag']);
2231
2232
			// Not a match?
2233
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2234
				continue;
2235
2236
			$next_c = $message[$pos + 1 + $pt_strlen];
2237
2238
			// A test validation?
2239
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2240
				continue;
2241
			// Do we want parameters?
2242
			elseif (!empty($possible['parameters']))
2243
			{
2244
				if ($next_c != ' ')
2245
					continue;
2246
			}
2247
			elseif (isset($possible['type']))
2248
			{
2249
				// Do we need an equal sign?
2250
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2251
					continue;
2252
				// Maybe we just want a /...
2253
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2254
					continue;
2255
				// An immediate ]?
2256
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2257
					continue;
2258
			}
2259
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2260
			elseif ($next_c != ']')
2261
				continue;
2262
2263
			// Check allowed tree?
2264
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2265
				continue;
2266
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2267
				continue;
2268
			// If this is in the list of disallowed child tags, don't parse it.
2269
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2270
				continue;
2271
2272
			$pos1 = $pos + 1 + $pt_strlen + 1;
2273
2274
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2275
			if ($possible['tag'] == 'quote')
2276
			{
2277
				// Start with standard
2278
				$quote_alt = false;
2279
				foreach ($open_tags as $open_quote)
2280
				{
2281
					// Every parent quote this quote has flips the styling
2282
					if ($open_quote['tag'] == 'quote')
2283
						$quote_alt = !$quote_alt;
2284
				}
2285
				// Add a class to the quote to style alternating blockquotes
2286
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2287
			}
2288
2289
			// This is long, but it makes things much easier and cleaner.
2290
			if (!empty($possible['parameters']))
2291
			{
2292
				// Build a regular expression for each parameter for the current tag.
2293
				$preg = array();
2294
				foreach ($possible['parameters'] as $p => $info)
2295
					$preg[] = '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
2296
2297
				// Extract the string that potentially holds our parameters.
2298
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
2299
				$blobs = preg_split('~\]~i', $blob[1]);
2300
2301
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
2302
2303
				// Progressively append more blobs until we find our parameters or run out of blobs
2304
				$blob_counter = 1;
2305
				while ($blob_counter <= count($blobs))
2306
				{
2307
2308
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
2309
2310
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2311
					sort($given_params, SORT_STRING);
2312
2313
					$match = preg_match('~^' . implode('', $preg) . '$~i', implode(' ', $given_params), $matches) !== 0;
2314
2315
					if ($match)
2316
						$blob_counter = count($blobs) + 1;
2317
				}
2318
2319
				// Didn't match our parameter list, try the next possible.
2320
				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...
2321
					continue;
2322
2323
				$params = array();
2324
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2325
				{
2326
					$key = strtok(ltrim($matches[$i]), '=');
2327
					if (isset($possible['parameters'][$key]['value']))
2328
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2329
					elseif (isset($possible['parameters'][$key]['validate']))
2330
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2331
					else
2332
						$params['{' . $key . '}'] = $matches[$i + 1];
2333
2334
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2335
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2336
				}
2337
2338
				foreach ($possible['parameters'] as $p => $info)
2339
				{
2340
					if (!isset($params['{' . $p . '}']))
2341
						$params['{' . $p . '}'] = '';
2342
				}
2343
2344
				$tag = $possible;
2345
2346
				// Put the parameters into the string.
2347
				if (isset($tag['before']))
2348
					$tag['before'] = strtr($tag['before'], $params);
2349
				if (isset($tag['after']))
2350
					$tag['after'] = strtr($tag['after'], $params);
2351
				if (isset($tag['content']))
2352
					$tag['content'] = strtr($tag['content'], $params);
2353
2354
				$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...
2355
			}
2356
			else
2357
			{
2358
				$tag = $possible;
2359
				$params = array();
2360
			}
2361
			break;
2362
		}
2363
2364
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2365
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2366
		{
2367
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2368
				continue;
2369
2370
			$tag = $itemcodes[$message[$pos + 1]];
2371
2372
			// First let's set up the tree: it needs to be in a list, or after an li.
2373
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2374
			{
2375
				$open_tags[] = array(
2376
					'tag' => 'list',
2377
					'after' => '</ul>',
2378
					'block_level' => true,
2379
					'require_children' => array('li'),
2380
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2381
				);
2382
				$code = '<ul class="bbc_list">';
2383
			}
2384
			// We're in a list item already: another itemcode?  Close it first.
2385
			elseif ($inside['tag'] == 'li')
2386
			{
2387
				array_pop($open_tags);
2388
				$code = '</li>';
2389
			}
2390
			else
2391
				$code = '';
2392
2393
			// Now we open a new tag.
2394
			$open_tags[] = array(
2395
				'tag' => 'li',
2396
				'after' => '</li>',
2397
				'trim' => 'outside',
2398
				'block_level' => true,
2399
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2400
			);
2401
2402
			// First, open the tag...
2403
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2404
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2405
			$pos += strlen($code) - 1 + 2;
2406
2407
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2408
			$pos2 = strpos($message, '<br>', $pos);
2409
			$pos3 = strpos($message, '[/', $pos);
2410
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2411
			{
2412
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2413
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2414
2415
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2416
			}
2417
			// Tell the [list] that it needs to close specially.
2418
			else
2419
			{
2420
				// Move the li over, because we're not sure what we'll hit.
2421
				$open_tags[count($open_tags) - 1]['after'] = '';
2422
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2423
			}
2424
2425
			continue;
2426
		}
2427
2428
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2429
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2430
		{
2431
			array_pop($open_tags);
2432
2433
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2434
			$pos += strlen($inside['after']) - 1 + 2;
2435
		}
2436
2437
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2438
		if ($tag === null)
2439
			continue;
2440
2441
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2442
		if (isset($inside['disallow_children']))
2443
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2444
2445
		// Is this tag disabled?
2446
		if (isset($disabled[$tag['tag']]))
2447
		{
2448
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2449
			{
2450
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2451
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2452
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2453
			}
2454
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2455
			{
2456
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2457
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2458
			}
2459
			else
2460
				$tag['content'] = $tag['disabled_content'];
2461
		}
2462
2463
		// we use this a lot
2464
		$tag_strlen = strlen($tag['tag']);
2465
2466
		// The only special case is 'html', which doesn't need to close things.
2467
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2468
		{
2469
			$n = count($open_tags) - 1;
2470
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2471
				$n--;
2472
2473
			// Close all the non block level tags so this tag isn't surrounded by them.
2474
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2475
			{
2476
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2477
				$ot_strlen = strlen($open_tags[$i]['after']);
2478
				$pos += $ot_strlen + 2;
2479
				$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...
2480
2481
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2482
				$whitespace_regex = '';
2483
				if (!empty($tag['block_level']))
2484
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2485 View Code Duplication
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2486
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2487 View Code Duplication
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2488
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2489
2490
				array_pop($open_tags);
2491
			}
2492
		}
2493
2494
		// No type means 'parsed_content'.
2495
		if (!isset($tag['type']))
2496
		{
2497
			// @todo Check for end tag first, so people can say "I like that [i] tag"?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
2498
			$open_tags[] = $tag;
2499
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2500
			$pos += strlen($tag['before']) - 1 + 2;
2501
		}
2502
		// Don't parse the content, just skip it.
2503
		elseif ($tag['type'] == 'unparsed_content')
2504
		{
2505
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2506
			if ($pos2 === false)
2507
				continue;
2508
2509
			$data = substr($message, $pos1, $pos2 - $pos1);
2510
2511
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2512
				$data = substr($data, 4);
2513
2514
			if (isset($tag['validate']))
2515
				$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...
2516
2517
			$code = strtr($tag['content'], array('$1' => $data));
2518
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2519
2520
			$pos += strlen($code) - 1 + 2;
2521
			$last_pos = $pos + 1;
2522
2523
		}
2524
		// Don't parse the content, just skip it.
2525
		elseif ($tag['type'] == 'unparsed_equals_content')
2526
		{
2527
			// The value may be quoted for some tags - check.
2528 View Code Duplication
			if (isset($tag['quoted']))
2529
			{
2530
				$quoted = substr($message, $pos1, 6) == '&quot;';
2531
				if ($tag['quoted'] != 'optional' && !$quoted)
2532
					continue;
2533
2534
				if ($quoted)
2535
					$pos1 += 6;
2536
			}
2537
			else
2538
				$quoted = false;
2539
2540
			$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...
2541
			if ($pos2 === false)
2542
				continue;
2543
2544
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2545
			if ($pos3 === false)
2546
				continue;
2547
2548
			$data = array(
2549
				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...
2550
				substr($message, $pos1, $pos2 - $pos1)
2551
			);
2552
2553
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
2554
				$data[0] = substr($data[0], 4);
2555
2556
			// Validation for my parking, please!
2557
			if (isset($tag['validate']))
2558
				$tag['validate']($tag, $data, $disabled, $params);
2559
2560
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
2561
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2562
			$pos += strlen($code) - 1 + 2;
2563
		}
2564
		// A closed tag, with no content or value.
2565
		elseif ($tag['type'] == 'closed')
2566
		{
2567
			$pos2 = strpos($message, ']', $pos);
2568
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
2569
			$pos += strlen($tag['content']) - 1 + 2;
2570
		}
2571
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
2572
		elseif ($tag['type'] == 'unparsed_commas_content')
2573
		{
2574
			$pos2 = strpos($message, ']', $pos1);
2575
			if ($pos2 === false)
2576
				continue;
2577
2578
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2579
			if ($pos3 === false)
2580
				continue;
2581
2582
			// We want $1 to be the content, and the rest to be csv.
2583
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
2584
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
2585
2586
			if (isset($tag['validate']))
2587
				$tag['validate']($tag, $data, $disabled, $params);
2588
2589
			$code = $tag['content'];
2590 View Code Duplication
			foreach ($data as $k => $d)
2591
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2592
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2593
			$pos += strlen($code) - 1 + 2;
2594
		}
2595
		// This has parsed content, and a csv value which is unparsed.
2596
		elseif ($tag['type'] == 'unparsed_commas')
2597
		{
2598
			$pos2 = strpos($message, ']', $pos1);
2599
			if ($pos2 === false)
2600
				continue;
2601
2602
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
2603
2604
			if (isset($tag['validate']))
2605
				$tag['validate']($tag, $data, $disabled, $params);
2606
2607
			// Fix after, for disabled code mainly.
2608 View Code Duplication
			foreach ($data as $k => $d)
2609
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
2610
2611
			$open_tags[] = $tag;
2612
2613
			// 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...
2614
			$code = $tag['before'];
2615 View Code Duplication
			foreach ($data as $k => $d)
2616
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2617
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
2618
			$pos += strlen($code) - 1 + 2;
2619
		}
2620
		// A tag set to a value, parsed or not.
2621
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
2622
		{
2623
			// The value may be quoted for some tags - check.
2624 View Code Duplication
			if (isset($tag['quoted']))
2625
			{
2626
				$quoted = substr($message, $pos1, 6) == '&quot;';
2627
				if ($tag['quoted'] != 'optional' && !$quoted)
2628
					continue;
2629
2630
				if ($quoted)
2631
					$pos1 += 6;
2632
			}
2633
			else
2634
				$quoted = false;
2635
2636
			$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...
2637
			if ($pos2 === false)
2638
				continue;
2639
2640
			$data = substr($message, $pos1, $pos2 - $pos1);
2641
2642
			// Validation for my parking, please!
2643
			if (isset($tag['validate']))
2644
				$tag['validate']($tag, $data, $disabled, $params);
2645
2646
			// For parsed content, we must recurse to avoid security problems.
2647
			if ($tag['type'] != 'unparsed_equals')
2648
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
2649
2650
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
2651
2652
			$open_tags[] = $tag;
2653
2654
			$code = strtr($tag['before'], array('$1' => $data));
2655
			$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...
2656
			$pos += strlen($code) - 1 + 2;
2657
		}
2658
2659
		// If this is block level, eat any breaks after it.
2660
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
2661
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
2662
2663
		// Are we trimming outside this tag?
2664
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
2665
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
2666
	}
2667
2668
	// Close any remaining tags.
2669
	while ($tag = array_pop($open_tags))
2670
		$message .= "\n" . $tag['after'] . "\n";
2671
2672
	// Parse the smileys within the parts where it can be done safely.
2673
	if ($smileys === true)
2674
	{
2675
		$message_parts = explode("\n", $message);
2676
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
2677
			parsesmileys($message_parts[$i]);
2678
2679
		$message = implode('', $message_parts);
2680
	}
2681
2682
	// No smileys, just get rid of the markers.
2683
	else
2684
		$message = strtr($message, array("\n" => ''));
2685
2686
	if ($message !== '' && $message[0] === ' ')
2687
		$message = '&nbsp;' . substr($message, 1);
2688
2689
	// Cleanup whitespace.
2690
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
2691
2692
	// Allow mods access to what parse_bbc created
2693
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
2694
2695
	// Cache the output if it took some time...
2696
	if (isset($cache_key, $cache_t) && array_sum(explode(' ', microtime())) - array_sum(explode(' ', $cache_t)) > 0.05)
2697
		cache_put_data($cache_key, $message, 240);
2698
2699
	// If this was a force parse revert if needed.
2700
	if (!empty($parse_tags))
2701
	{
2702
		if (empty($temp_bbc))
2703
			$bbc_codes = array();
2704
		else
2705
		{
2706
			$bbc_codes = $temp_bbc;
2707
			unset($temp_bbc);
2708
		}
2709
	}
2710
2711
	return $message;
2712
}
2713
2714
/**
2715
 * Parse smileys in the passed message.
2716
 *
2717
 * The smiley parsing function which makes pretty faces appear :).
2718
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
2719
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
2720
 * Caches the smileys from the database or array in memory.
2721
 * Doesn't return anything, but rather modifies message directly.
2722
 *
2723
 * @param string &$message The message to parse smileys in
2724
 */
2725
function parsesmileys(&$message)
2726
{
2727
	global $modSettings, $txt, $user_info, $context, $smcFunc;
2728
	static $smileyPregSearch = null, $smileyPregReplacements = array();
2729
2730
	// No smiley set at all?!
2731
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
2732
		return;
2733
2734
	// If smileyPregSearch hasn't been set, do it now.
2735
	if (empty($smileyPregSearch))
2736
	{
2737
		// Use the default smileys if it is disabled. (better for "portability" of smileys.)
2738
		if (empty($modSettings['smiley_enable']))
2739
		{
2740
			$smileysfrom = array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', '0:)');
2741
			$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');
2742
			$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'], '', '', '', '');
2743
		}
2744
		else
2745
		{
2746
			// Load the smileys in reverse order by length so they don't get parsed wrong.
2747
			if (($temp = cache_get_data('parsing_smileys', 480)) == null)
2748
			{
2749
				$result = $smcFunc['db_query']('', '
2750
					SELECT code, filename, description
2751
					FROM {db_prefix}smileys
2752
					ORDER BY LENGTH(code) DESC',
2753
					array(
2754
					)
2755
				);
2756
				$smileysfrom = array();
2757
				$smileysto = array();
2758
				$smileysdescs = array();
2759
				while ($row = $smcFunc['db_fetch_assoc']($result))
2760
				{
2761
					$smileysfrom[] = $row['code'];
2762
					$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
2763
					$smileysdescs[] = $row['description'];
2764
				}
2765
				$smcFunc['db_free_result']($result);
2766
2767
				cache_put_data('parsing_smileys', array($smileysfrom, $smileysto, $smileysdescs), 480);
2768
			}
2769
			else
2770
				list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
2771
		}
2772
2773
		// The non-breaking-space is a complex thing...
2774
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
2775
2776
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
2777
		$smileyPregReplacements = array();
2778
		$searchParts = array();
2779
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
2780
2781
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
2782
		{
2783
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
2784
			$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">';
2785
2786
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
2787
2788
			$searchParts[] = preg_quote($smileysfrom[$i], '~');
2789
			if ($smileysfrom[$i] != $specialChars)
2790
			{
2791
				$smileyPregReplacements[$specialChars] = $smileyCode;
2792
				$searchParts[] = preg_quote($specialChars, '~');
2793
			}
2794
		}
2795
2796
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . implode('|', $searchParts) . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
2797
	}
2798
2799
	// Replace away!
2800
	$message = preg_replace_callback($smileyPregSearch,
2801
		function ($matches) use ($smileyPregReplacements)
2802
		{
2803
			return $smileyPregReplacements[$matches[1]];
2804
		}, $message);
2805
}
2806
2807
/**
2808
 * Highlight any code.
2809
 *
2810
 * Uses PHP's highlight_string() to highlight PHP syntax
2811
 * does special handling to keep the tabs in the code available.
2812
 * used to parse PHP code from inside [code] and [php] tags.
2813
 *
2814
 * @param string $code The code
2815
 * @return string The code with highlighted HTML.
2816
 */
2817
function highlight_php_code($code)
2818
{
2819
	// Remove special characters.
2820
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
2821
2822
	$oldlevel = error_reporting(0);
2823
2824
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
2825
2826
	error_reporting($oldlevel);
2827
2828
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
2829
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
2830
2831
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
2832
}
2833
2834
/**
2835
 * Make sure the browser doesn't come back and repost the form data.
2836
 * Should be used whenever anything is posted.
2837
 *
2838
 * @param string $setLocation The URL to redirect them to
2839
 * @param bool $refresh Whether to use a meta refresh instead
2840
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
2841
 */
2842
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
2843
{
2844
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
2845
2846
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
2847
	if (!empty($context['flush_mail']))
2848
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
2849
		AddMailQueue(true);
2850
2851
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
2852
2853
	if ($add)
2854
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
2855
2856
	// Put the session ID in.
2857
	if (defined('SID') && SID != '')
2858
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
2859
	// Keep that debug in their for template debugging!
2860 View Code Duplication
	elseif (isset($_GET['debug']))
2861
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
2862
2863
	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'])))
2864
	{
2865
		if (defined('SID') && SID != '')
2866
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '/') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
2867
				function ($m) use ($scripturl)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $m. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
2868
				{
2869
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID. (isset($m[2]) ? "$m[2]" : "");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $m instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Comprehensibility introduced by
The string literal does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
2870
				}, $setLocation);
2871 View Code Duplication
		else
2872
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '/') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
2873
				function ($m) use ($scripturl)
2874
				{
2875
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $m instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Comprehensibility introduced by
The string literal does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
2876
				}, $setLocation);
2877
	}
2878
2879
	// Maybe integrations want to change where we are heading?
2880
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
2881
2882
	// Set the header.
2883
	header('Location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
2884
2885
	// Debugging.
2886
	if (isset($db_show_debug) && $db_show_debug === true)
2887
		$_SESSION['debug_redirect'] = $db_cache;
2888
2889
	obExit(false);
2890
}
2891
2892
/**
2893
 * Ends execution.  Takes care of template loading and remembering the previous URL.
2894
 * @param bool $header Whether to do the header
0 ignored issues
show
Documentation introduced by
Should the type for parameter $header not be boolean|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2895
 * @param bool $do_footer Whether to do the footer
0 ignored issues
show
Documentation introduced by
Should the type for parameter $do_footer not be boolean|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
2896
 * @param bool $from_index Whether we're coming from the board index
2897
 * @param bool $from_fatal_error Whether we're coming from a fatal error
2898
 */
2899
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
2900
{
2901
	global $context, $settings, $modSettings, $txt, $smcFunc;
2902
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
2903
2904
	// Attempt to prevent a recursive loop.
2905
	++$level;
2906
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
2907
		exit;
2908
	if ($from_fatal_error)
2909
		$has_fatal_error = true;
2910
2911
	// Clear out the stat cache.
2912
	trackStats();
2913
2914
	// If we have mail to send, send it.
2915
	if (!empty($context['flush_mail']))
2916
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
2917
		AddMailQueue(true);
2918
2919
	$do_header = $header === null ? !$header_done : $header;
2920
	if ($do_footer === null)
2921
		$do_footer = $do_header;
2922
2923
	// Has the template/header been done yet?
2924
	if ($do_header)
2925
	{
2926
		// Was the page title set last minute? Also update the HTML safe one.
2927
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
2928
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
2929
2930
		// Start up the session URL fixer.
2931
		ob_start('ob_sessrewrite');
2932
2933
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
2934
			$buffers = explode(',', $settings['output_buffers']);
2935
		elseif (!empty($settings['output_buffers']))
2936
			$buffers = $settings['output_buffers'];
2937
		else
2938
			$buffers = array();
2939
2940
		if (isset($modSettings['integrate_buffer']))
2941
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
2942
2943
		if (!empty($buffers))
2944
			foreach ($buffers as $function)
2945
			{
2946
				$call = call_helper($function, true);
2947
2948
				// Is it valid?
2949
				if (!empty($call))
2950
					ob_start($call);
2951
			}
2952
2953
		// Display the screen in the logical order.
2954
		template_header();
2955
		$header_done = true;
2956
	}
2957
	if ($do_footer)
2958
	{
2959
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
2960
2961
		// Anything special to put out?
2962
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
2963
			echo $context['insert_after_template'];
2964
2965
		// Just so we don't get caught in an endless loop of errors from the footer...
2966
		if (!$footer_done)
2967
		{
2968
			$footer_done = true;
2969
			template_footer();
2970
2971
			// (since this is just debugging... it's okay that it's after </html>.)
2972
			if (!isset($_REQUEST['xml']))
2973
				displayDebug();
2974
		}
2975
	}
2976
2977
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
2978
	if (strpos($_SERVER['REQUEST_URL'], 'action=dlattach') === false && strpos($_SERVER['REQUEST_URL'], 'action=viewsmfile') === false)
2979
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
2980
2981
	// For session check verification.... don't switch browsers...
2982
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
2983
2984
	// Hand off the output to the portal, etc. we're integrated with.
2985
	call_integration_hook('integrate_exit', array($do_footer));
2986
2987
	// Don't exit if we're coming from index.php; that will pass through normally.
2988
	if (!$from_index)
2989
		exit;
2990
}
2991
2992
/**
2993
 * Get the size of a specified image with better error handling.
2994
 * @todo see if it's better in Subs-Graphics, but one step at the time.
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
2995
 * Uses getimagesize() to determine the size of a file.
2996
 * Attempts to connect to the server first so it won't time out.
2997
 *
2998
 * @param string $url The URL of the image
2999
 * @return array|false The image size as array (width, height), or false on failure
3000
 */
3001
function url_image_size($url)
3002
{
3003
	global $sourcedir;
3004
3005
	// Make sure it is a proper URL.
3006
	$url = str_replace(' ', '%20', $url);
3007
3008
	// Can we pull this from the cache... please please?
3009
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
3010
		return $temp;
3011
	$t = microtime();
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $t. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
3012
3013
	// Get the host to pester...
3014
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3015
3016
	// Can't figure it out, just try the image size.
3017
	if ($url == '' || $url == 'http://' || $url == 'https://')
3018
	{
3019
		return false;
3020
	}
3021
	elseif (!isset($match[1]))
3022
	{
3023
		$size = @getimagesize($url);
3024
	}
3025
	else
3026
	{
3027
		// Try to connect to the server... give it half a second.
3028
		$temp = 0;
3029
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fp. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
3030
3031
		// 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...
3032
		if ($fp != false)
3033
		{
3034
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3035
			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");
3036
3037
			// Read in the HTTP/1.1 or whatever.
3038
			$test = substr(fgets($fp, 11), -1);
3039
			fclose($fp);
3040
3041
			// See if it returned a 404/403 or something.
3042
			if ($test < 4)
3043
			{
3044
				$size = @getimagesize($url);
3045
3046
				// This probably means allow_url_fopen is off, let's try GD.
3047
				if ($size === false && function_exists('imagecreatefromstring'))
3048
				{
3049
					include_once($sourcedir . '/Subs-Package.php');
3050
3051
					// It's going to hate us for doing this, but another request...
3052
					$image = @imagecreatefromstring(fetch_web_data($url));
3053
					if ($image !== false)
3054
					{
3055
						$size = array(imagesx($image), imagesy($image));
3056
						imagedestroy($image);
3057
					}
3058
				}
3059
			}
3060
		}
3061
	}
3062
3063
	// If we didn't get it, we failed.
3064
	if (!isset($size))
3065
		$size = false;
3066
3067
	// If this took a long time, we may never have to do it again, but then again we might...
3068 View Code Duplication
	if (array_sum(explode(' ', microtime())) - array_sum(explode(' ', $t)) > 0.8)
3069
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3070
3071
	// Didn't work.
3072
	return $size;
3073
}
3074
3075
/**
3076
 * Sets up the basic theme context stuff.
3077
 * @param bool $forceload Whether to load the theme even if it's already loaded
3078
 */
3079
function setupThemeContext($forceload = false)
3080
{
3081
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3082
	global $smcFunc;
3083
	static $loaded = false;
3084
3085
	// Under SSI this function can be called more then once.  That can cause some problems.
3086
	//   So only run the function once unless we are forced to run it again.
3087
	if ($loaded && !$forceload)
3088
		return;
3089
3090
	$loaded = true;
3091
3092
	$context['in_maintenance'] = !empty($maintenance);
3093
	$context['current_time'] = timeformat(time(), false);
3094
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3095
3096
	// Get some news...
3097
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3098
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3099
	{
3100
		if (trim($context['news_lines'][$i]) == '')
3101
			continue;
3102
3103
		// Clean it up for presentation ;).
3104
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3105
	}
3106
	if (!empty($context['news_lines']))
3107
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3108
3109
	if (!$user_info['is_guest'])
3110
	{
3111
		$context['user']['messages'] = &$user_info['messages'];
3112
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3113
		$context['user']['alerts'] = &$user_info['alerts'];
3114
3115
		// Personal message popup...
3116
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3117
			$context['user']['popup_messages'] = true;
3118
		else
3119
			$context['user']['popup_messages'] = false;
3120
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3121
3122
		if (allowedTo('moderate_forum'))
3123
			$context['unapproved_members'] = (!empty($modSettings['registration_method']) && ($modSettings['registration_method'] == 2 || (!empty($modSettings['coppaType']) && $modSettings['coppaType'] == 2))) || !empty($modSettings['approveAccountDeletion']) ? $modSettings['unapprovedMembers'] : 0;
3124
3125
		$context['user']['avatar'] = array();
3126
3127
		// Check for gravatar first since we might be forcing them...
3128
		if (($modSettings['gravatarEnabled'] && substr($user_info['avatar']['url'], 0, 11) == 'gravatar://') || !empty($modSettings['gravatarOverride']))
3129
		{
3130
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3131
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3132
			else
3133
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3134
		}
3135
		// Uploaded?
3136
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3137
			$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';
3138
		// Full URL?
3139
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3140
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3141
		// Otherwise we assume it's server stored.
3142
		elseif ($user_info['avatar']['url'] != '')
3143
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3144
		// No avatar at all? Fine, we have a big fat default avatar ;)
3145
		else
3146
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3147
3148
		if (!empty($context['user']['avatar']))
3149
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3150
3151
		// Figure out how long they've been logged in.
3152
		$context['user']['total_time_logged_in'] = array(
3153
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3154
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3155
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3156
		);
3157
	}
3158
	else
3159
	{
3160
		$context['user']['messages'] = 0;
3161
		$context['user']['unread_messages'] = 0;
3162
		$context['user']['avatar'] = array();
3163
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3164
		$context['user']['popup_messages'] = false;
3165
3166
		if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
3167
			$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
3168
3169
		// If we've upgraded recently, go easy on the passwords.
3170
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3171
			$context['disable_login_hashing'] = true;
3172
	}
3173
3174
	// Setup the main menu items.
3175
	setupMenuContext();
3176
3177
	// This is here because old index templates might still use it.
3178
	$context['show_news'] = !empty($settings['enable_news']);
3179
3180
	// This is done to allow theme authors to customize it as they want.
3181
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3182
3183
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3184
	if ($context['show_pm_popup'])
3185
		addInlineJavaScript('
3186
		jQuery(document).ready(function($) {
3187
			new smc_Popup({
3188
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3189
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3190
				icon_class: \'generic_icons mail_new\'
3191
			});
3192
		});');
3193
3194
	// Add a generic "Are you sure?" confirmation message.
3195
	addInlineJavaScript('
3196
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) .';');
3197
3198
	// Now add the capping code for avatars.
3199
	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')
3200
		addInlineCss('
3201
img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3202
3203
	// This looks weird, but it's because BoardIndex.php references the variable.
3204
	$context['common_stats']['latest_member'] = array(
3205
		'id' => $modSettings['latestMember'],
3206
		'name' => $modSettings['latestRealName'],
3207
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3208
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3209
	);
3210
	$context['common_stats'] = array(
3211
		'total_posts' => comma_format($modSettings['totalMessages']),
3212
		'total_topics' => comma_format($modSettings['totalTopics']),
3213
		'total_members' => comma_format($modSettings['totalMembers']),
3214
		'latest_member' => $context['common_stats']['latest_member'],
3215
	);
3216
	$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']);
3217
3218
	if (empty($settings['theme_version']))
3219
		addJavaScriptVar('smf_scripturl', $scripturl);
3220
3221
	if (!isset($context['page_title']))
3222
		$context['page_title'] = '';
3223
3224
	// Set some specific vars.
3225
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3226
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3227
3228
	// Content related meta tags, including Open Graph
3229
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3230
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3231
3232
	if (!empty($context['meta_keywords']))
3233
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3234
3235
	if (!empty($context['canonical_url']))
3236
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3237
3238
	if (!empty($settings['og_image']))
3239
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3240
3241
	if (!empty($context['meta_description']))
3242
	{
3243
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3244
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3245
	}
3246
	else
3247
	{
3248
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3249
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3250
	}
3251
3252
	call_integration_hook('integrate_theme_context');
3253
}
3254
3255
/**
3256
 * Helper function to set the system memory to a needed value
3257
 * - If the needed memory is greater than current, will attempt to get more
3258
 * - if in_use is set to true, will also try to take the current memory usage in to account
3259
 *
3260
 * @param string $needed The amount of memory to request, if needed, like 256M
3261
 * @param bool $in_use Set to true to account for current memory usage of the script
3262
 * @return boolean True if we have at least the needed memory
3263
 */
3264
function setMemoryLimit($needed, $in_use = false)
3265
{
3266
	// everything in bytes
3267
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3268
	$memory_needed = memoryReturnBytes($needed);
3269
3270
	// should we account for how much is currently being used?
3271
	if ($in_use)
3272
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3273
3274
	// if more is needed, request it
3275
	if ($memory_current < $memory_needed)
3276
	{
3277
		@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...
3278
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3279
	}
3280
3281
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
3282
3283
	// return success or not
3284
	return (bool) ($memory_current >= $memory_needed);
3285
}
3286
3287
/**
3288
 * Helper function to convert memory string settings to bytes
3289
 *
3290
 * @param string $val The byte string, like 256M or 1G
3291
 * @return integer The string converted to a proper integer in bytes
3292
 */
3293
function memoryReturnBytes($val)
3294
{
3295
	if (is_integer($val))
3296
		return $val;
3297
3298
	// Separate the number from the designator
3299
	$val = trim($val);
3300
	$num = intval(substr($val, 0, strlen($val) - 1));
3301
	$last = strtolower(substr($val, -1));
3302
3303
	// convert to bytes
3304
	switch ($last)
3305
	{
3306
		case 'g':
3307
			$num *= 1024;
3308
		case 'm':
3309
			$num *= 1024;
3310
		case 'k':
3311
			$num *= 1024;
3312
	}
3313
	return $num;
3314
}
3315
3316
/**
3317
 * The header template
3318
 */
3319
function template_header()
3320
{
3321
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir;
3322
3323
	setupThemeContext();
3324
3325
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
3326
	if (empty($context['no_last_modified']))
3327
	{
3328
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
3329
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
3330
3331
		// Are we debugging the template/html content?
3332
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
3333
			header('Content-Type: application/xhtml+xml');
3334 View Code Duplication
		elseif (!isset($_REQUEST['xml']))
3335
			header('Content-Type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3336
	}
3337
3338
	header('Content-Type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3339
3340
	// We need to splice this in after the body layer, or after the main layer for older stuff.
3341
	if ($context['in_maintenance'] && $context['user']['is_admin'])
3342
	{
3343
		$position = array_search('body', $context['template_layers']);
3344
		if ($position === false)
3345
			$position = array_search('main', $context['template_layers']);
3346
3347
		if ($position !== false)
3348
		{
3349
			$before = array_slice($context['template_layers'], 0, $position + 1);
3350
			$after = array_slice($context['template_layers'], $position + 1);
3351
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
3352
		}
3353
	}
3354
3355
	$checked_securityFiles = false;
3356
	$showed_banned = false;
3357
	foreach ($context['template_layers'] as $layer)
3358
	{
3359
		loadSubTemplate($layer . '_above', true);
3360
3361
		// May seem contrived, but this is done in case the body and main layer aren't there...
3362
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
3363
		{
3364
			$checked_securityFiles = true;
3365
3366
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
3367
3368
			// Add your own files.
3369
			call_integration_hook('integrate_security_files', array(&$securityFiles));
3370
3371
			foreach ($securityFiles as $i => $securityFile)
3372
			{
3373
				if (!file_exists($boarddir . '/' . $securityFile))
3374
					unset($securityFiles[$i]);
3375
			}
3376
3377
			// We are already checking so many files...just few more doesn't make any difference! :P
3378
			if (!empty($modSettings['currentAttachmentUploadDir']))
3379
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
3380
3381
			else
3382
				$path = $modSettings['attachmentUploadDir'];
3383
3384
			secureDirectory($path, true);
3385
			secureDirectory($cachedir);
3386
3387
			// If agreement is enabled, at least the english version shall exists
3388
			if ($modSettings['requireAgreement'])
3389
				$agreement = !file_exists($boarddir . '/agreement.txt');
3390
3391
			if (!empty($securityFiles) || (!empty($modSettings['cache_enable']) && !is_writable($cachedir)) || !empty($agreement))
3392
			{
3393
				echo '
3394
		<div class="errorbox">
3395
			<p class="alert">!!</p>
3396
			<h3>', empty($securityFiles) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
3397
			<p>';
3398
3399
				foreach ($securityFiles as $securityFile)
3400
				{
3401
					echo '
3402
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
3403
3404
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
3405
						echo '
3406
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
3407
				}
3408
3409
				if (!empty($modSettings['cache_enable']) && !is_writable($cachedir))
3410
					echo '
3411
				<strong>', $txt['cache_writable'], '</strong><br>';
3412
3413
				if (!empty($agreement))
3414
					echo '
3415
				<strong>', $txt['agreement_missing'], '</strong><br>';
3416
3417
				echo '
3418
			</p>
3419
		</div>';
3420
			}
3421
		}
3422
		// If the user is banned from posting inform them of it.
3423
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
3424
		{
3425
			$showed_banned = true;
3426
			echo '
3427
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
3428
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
3429
3430
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
3431
				echo '
3432
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
3433
3434
			if (!empty($_SESSION['ban']['expire_time']))
3435
				echo '
3436
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
3437
			else
3438
				echo '
3439
					<div>', $txt['your_ban_expires_never'], '</div>';
3440
3441
			echo '
3442
				</div>';
3443
		}
3444
	}
3445
}
3446
3447
/**
3448
 * Show the copyright.
3449
 */
3450
function theme_copyright()
3451
{
3452
	global $forum_copyright, $software_year, $forum_version;
3453
3454
	// Don't display copyright for things like SSI.
3455
	if (!isset($forum_version) || !isset($software_year))
3456
		return;
3457
3458
	// Put in the version...
3459
	printf($forum_copyright, $forum_version, $software_year);
3460
}
3461
3462
/**
3463
 * The template footer
3464
 */
3465
function template_footer()
3466
{
3467
	global $context, $modSettings, $time_start, $db_count;
3468
3469
	// Show the load time?  (only makes sense for the footer.)
3470
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
3471
	$context['load_time'] = comma_format(round(array_sum(explode(' ', microtime())) - array_sum(explode(' ', $time_start)), 3));
3472
	$context['load_queries'] = $db_count;
3473
3474
	foreach (array_reverse($context['template_layers']) as $layer)
3475
		loadSubTemplate($layer . '_below', true);
3476
}
3477
3478
/**
3479
 * Output the Javascript files
3480
 * 	- tabbing in this function is to make the HTML source look good proper
3481
 *  - if defered is set function will output all JS (source & inline) set to load at page end
3482
 *
3483
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
3484
 */
3485
function template_javascript($do_deferred = false)
3486
{
3487
	global $context, $modSettings, $settings;
3488
3489
	// Use this hook to minify/optimize Javascript files and vars
3490
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
3491
3492
	$toMinify = array();
3493
	$toMinifyDefer = array();
3494
3495
	// Ouput the declared Javascript variables.
3496
	if (!empty($context['javascript_vars']) && !$do_deferred)
3497
	{
3498
		echo '
3499
	<script>';
3500
3501
		foreach ($context['javascript_vars'] as $key => $value)
3502
		{
3503
			if (empty($value))
3504
			{
3505
				echo '
3506
		var ', $key, ';';
3507
			}
3508
			else
3509
			{
3510
				echo '
3511
		var ', $key, ' = ', $value, ';';
3512
			}
3513
		}
3514
3515
		echo '
3516
	</script>';
3517
	}
3518
3519
	// While we have JavaScript files to place in the template.
3520
	foreach ($context['javascript_files'] as $id => $js_file)
3521
	{
3522
		// Last minute call! allow theme authors to disable single files.
3523
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
3524
			continue;
3525
3526
		// By default all files don't get minimized unless the file explicitly says so!
3527
		if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
3528
		{
3529
			if ($do_deferred && !empty($js_file['options']['defer']))
3530
				$toMinifyDefer[] = $js_file;
3531
3532
			elseif (!$do_deferred && empty($js_file['options']['defer']))
3533
				$toMinify[] = $js_file;
3534
3535
			// Grab a random seed.
3536
			if (!isset($minSeed))
3537
				$minSeed = $js_file['options']['seed'];
3538
		}
3539
3540
		elseif ((!$do_deferred && empty($js_file['options']['defer'])) || ($do_deferred && !empty($js_file['options']['defer'])))
3541
			echo '
3542
	<script src="', $js_file['fileUrl'], '"', !empty($js_file['options']['async']) ? ' async="async"' : '', '></script>';
3543
	}
3544
3545
	if ((!$do_deferred && !empty($toMinify)) || ($do_deferred && !empty($toMinifyDefer)))
3546
	{
3547
		$result = custMinify(($do_deferred ? $toMinifyDefer : $toMinify), 'js', $do_deferred);
3548
3549
		// Minify process couldn't work, print each individual files.
3550
		if (!empty($result) && is_array($result))
3551
			foreach ($result as $minFailedFile)
3552
				echo '
3553
	<script src="', $minFailedFile['fileUrl'], '"', !empty($minFailedFile['options']['async']) ? ' async="async"' : '', '></script>';
3554
3555
		else
3556
			echo '
3557
	<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...
3558
	}
3559
3560
	// Inline JavaScript - Actually useful some times!
3561
	if (!empty($context['javascript_inline']))
3562
	{
3563 View Code Duplication
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
3564
		{
3565
			echo '
3566
<script>';
3567
3568
			foreach ($context['javascript_inline']['defer'] as $js_code)
3569
				echo $js_code;
3570
3571
			echo '
3572
</script>';
3573
		}
3574
3575 View Code Duplication
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
3576
		{
3577
			echo '
3578
	<script>';
3579
3580
			foreach ($context['javascript_inline']['standard'] as $js_code)
3581
				echo $js_code;
3582
3583
			echo '
3584
	</script>';
3585
		}
3586
	}
3587
}
3588
3589
/**
3590
 * Output the CSS files
3591
 *
3592
 */
3593
function template_css()
3594
{
3595
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
3596
3597
	// Use this hook to minify/optimize CSS files
3598
	call_integration_hook('integrate_pre_css_output');
3599
3600
	$toMinify = array();
3601
	$normal = array();
3602
3603
	foreach ($context['css_files'] as $id => $file)
3604
	{
3605
		// Last minute call! allow theme authors to disable single files.
3606
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
3607
			continue;
3608
3609
		// By default all files don't get minimized unless the file explicitly says so!
3610
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']))
3611
		{
3612
			$toMinify[] = $file;
3613
3614
			// Grab a random seed.
3615
			if (!isset($minSeed))
3616
				$minSeed = $file['options']['seed'];
3617
		}
3618
3619
		else
3620
			$normal[] = $file['fileUrl'];
3621
	}
3622
3623
	if (!empty($toMinify))
3624
	{
3625
		$result = custMinify($toMinify, 'css');
3626
3627
		// Minify process couldn't work, print each individual files.
3628
		if (!empty($result) && is_array($result))
3629
			foreach ($result as $minFailedFile)
3630
				echo '
3631
	<link rel="stylesheet" href="', $minFailedFile['fileUrl'], '">';
3632
3633
		else
3634
			echo '
3635
	<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...
3636
	}
3637
3638
	// Print the rest after the minified files.
3639
	if (!empty($normal))
3640
		foreach ($normal as $nf)
3641
			echo '
3642
	<link rel="stylesheet" href="', $nf ,'">';
3643
3644
	if ($db_show_debug === true)
3645
	{
3646
		// Try to keep only what's useful.
3647
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
3648
		foreach ($context['css_files'] as $file)
3649
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
3650
	}
3651
3652
	if (!empty($context['css_header']))
3653
	{
3654
		echo '
3655
	<style>';
3656
3657
		foreach ($context['css_header'] as $css)
3658
			echo $css .'
3659
	';
3660
3661
		echo'
3662
	</style>';
3663
	}
3664
}
3665
3666
/**
3667
 * Get an array of previously defined files and adds them to our main minified file.
3668
 * Sets a one day cache to avoid re-creating a file on every request.
3669
 *
3670
 * @param array $data The files to minify.
3671
 * @param string $type either css or js.
3672
 * @param bool $do_deferred use for type js to indicate if the minified file will be deferred, IE, put at the closing </body> tag.
3673
 * @return bool|array If an array the minify process failed and the data is returned intact.
3674
 */
3675
function custMinify($data, $type, $do_deferred = false)
3676
{
3677
	global $settings, $txt;
3678
3679
	$types = array('css', 'js');
3680
	$type = !empty($type) && in_array($type, $types) ? $type : false;
3681
	$data = !empty($data) ? $data : false;
3682
3683
	if (empty($type) || empty($data))
3684
		return false;
3685
3686
	// Did we already did this?
3687
	$toCache = cache_get_data('minimized_'. $settings['theme_id'] .'_'. $type, 86400);
3688
3689
	// Already done?
3690
	if (!empty($toCache))
3691
		return true;
3692
3693
	// No namespaces, sorry!
3694
	$classType = 'MatthiasMullie\\Minify\\'. strtoupper($type);
3695
3696
	// Temp path.
3697
	$cTempPath = $settings['theme_dir'] .'/'. ($type == 'css' ? 'css' : 'scripts') .'/';
3698
3699
	// What kind of file are we going to create?
3700
	$toCreate = $cTempPath .'minified'. ($do_deferred ? '_deferred' : '') .'.'. $type;
3701
3702
	// File has to exists, if it isn't try to create it.
3703
	if ((!file_exists($toCreate) && @fopen($toCreate, 'w') === false) || !smf_chmod($toCreate))
3704
	{
3705
		loadLanguage('Errors');
3706
		log_error(sprintf($txt['file_not_created'], $toCreate), 'general');
3707
		cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, null);
3708
3709
		// The process failed so roll back to print each individual file.
3710
		return $data;
3711
	}
3712
3713
	$minifier = new $classType();
3714
3715
	foreach ($data as $file)
3716
	{
3717
		$tempFile = str_replace($file['options']['seed'], '', $file['filePath']);
3718
		$toAdd = file_exists($tempFile) ? $tempFile : false;
3719
3720
		// The file couldn't be located so it won't be added, log this error.
3721
		if (empty($toAdd))
3722
		{
3723
			loadLanguage('Errors');
3724
			log_error(sprintf($txt['file_minimize_fail'], $file['fileName']), 'general');
3725
			continue;
3726
		}
3727
3728
		// Add this file to the list.
3729
		$minifier->add($toAdd);
3730
	}
3731
3732
	// Create the file.
3733
	$minifier->minify($toCreate);
3734
	unset($minifier);
3735
	clearstatcache();
3736
3737
	// Minify process failed.
3738
	if (!filesize($toCreate))
3739
	{
3740
		loadLanguage('Errors');
3741
		log_error(sprintf($txt['file_not_created'], $toCreate), 'general');
3742
		cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, null);
3743
3744
		// The process failed so roll back to print each individual file.
3745
		return $data;
3746
	}
3747
3748
	// And create a long lived cache entry.
3749
	cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, $toCreate, 86400);
3750
3751
	return true;
3752
}
3753
3754
/**
3755
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
3756
 * @todo this currently returns the hash if new, and the full filename otherwise.
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
3757
 * Something messy like that.
3758
 * @todo and of course everything relies on this behavior and work around it. :P.
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
3759
 * Converters included.
3760
 *
3761
 * @param string $filename The name of the file
3762
 * @param int $attachment_id The ID of the attachment
3763
 * @param string $dir Which directory it should be in (null to use current one)
0 ignored issues
show
Documentation introduced by
Should the type for parameter $dir not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
3764
 * @param bool $new Whether this is a new attachment
3765
 * @param string $file_hash The file hash
3766
 * @return string The path to the file
0 ignored issues
show
Documentation introduced by
Should the return type not be string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
3767
 */
3768
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
3769
{
3770
	global $modSettings, $smcFunc;
3771
3772
	// Just make up a nice hash...
3773
	if ($new)
3774
		return sha1(md5($filename . time()) . mt_rand());
3775
3776
	// Just make sure that attachment id is only a int
3777
	$attachment_id = (int) $attachment_id;
3778
3779
	// Grab the file hash if it wasn't added.
3780
	// Left this for legacy.
3781
	if ($file_hash === '')
3782
	{
3783
		$request = $smcFunc['db_query']('', '
3784
			SELECT file_hash
3785
			FROM {db_prefix}attachments
3786
			WHERE id_attach = {int:id_attach}',
3787
			array(
3788
				'id_attach' => $attachment_id,
3789
			));
3790
3791
		if ($smcFunc['db_num_rows']($request) === 0)
3792
			return false;
3793
3794
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
3795
		$smcFunc['db_free_result']($request);
3796
	}
3797
3798
	// Still no hash? mmm...
3799
	if (empty($file_hash))
3800
		$file_hash = sha1(md5($filename . time()) . mt_rand());
3801
3802
	// Are we using multiple directories?
3803
	if (is_array($modSettings['attachmentUploadDir']))
3804
		$path = $modSettings['attachmentUploadDir'][$dir];
3805
3806
	else
3807
		$path = $modSettings['attachmentUploadDir'];
3808
3809
	return $path . '/' . $attachment_id . '_' . $file_hash .'.dat';
3810
}
3811
3812
/**
3813
 * Convert a single IP to a ranged IP.
3814
 * internal function used to convert a user-readable format to a format suitable for the database.
3815
 *
3816
 * @param string $fullip The full IP
3817
 * @return array An array of IP parts
3818
 */
3819
function ip2range($fullip)
3820
{
3821
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
3822
	if ($fullip == 'unknown')
3823
		$fullip = '255.255.255.255';
3824
3825
	$ip_parts = explode('-', $fullip);
3826
	$ip_array = array();
3827
3828
	// if ip 22.12.31.21
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...
3829
	if (count($ip_parts) == 1 && isValidIP($fullip))
3830
	{
3831
		$ip_array['low'] = $fullip;
3832
		$ip_array['high'] = $fullip;
3833
		return $ip_array;
3834
	} // if ip 22.12.* -> 22.12.* - 22.12.*
3835
	elseif (count($ip_parts) == 1)
3836
	{
3837
		$ip_parts[0] = $fullip;
3838
		$ip_parts[1] = $fullip;
3839
	}
3840
3841
	// if ip 22.12.31.21-12.21.31.21
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% 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...
3842
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
3843
	{
3844
		$ip_array['low'] = $ip_parts[0];
3845
		$ip_array['high'] = $ip_parts[1];
3846
		return $ip_array;
3847
	}
3848
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
3849
	{
3850
		$valid_low = isValidIP($ip_parts[0]);
3851
		$valid_high = isValidIP($ip_parts[1]);
3852
		$count = 0;
3853
		$mode = (preg_match('/:/',$ip_parts[0]) > 0 ? ':' : '.');
3854
		$max = ($mode == ':' ? 'ffff' : '255');
3855
		$min = 0;
3856 View Code Duplication
		if(!$valid_low)
3857
		{
3858
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
3859
			$valid_low = isValidIP($ip_parts[0]);
3860
			while (!$valid_low)
3861
			{
3862
				$ip_parts[0] .= $mode . $min;
3863
				$valid_low = isValidIP($ip_parts[0]);
3864
				$count++;
3865
				if ($count > 9) break;
3866
			}
3867
		}
3868
3869
		$count = 0;
3870 View Code Duplication
		if(!$valid_high)
3871
		{
3872
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
3873
			$valid_high = isValidIP($ip_parts[1]);
3874
			while (!$valid_high)
3875
			{
3876
				$ip_parts[1] .= $mode . $max;
3877
				$valid_high = isValidIP($ip_parts[1]);
3878
				$count++;
3879
				if ($count > 9) break;
3880
			}
3881
		}
3882
3883
		if($valid_high && $valid_low)
3884
		{
3885
			$ip_array['low'] = $ip_parts[0];
3886
			$ip_array['high'] = $ip_parts[1];
3887
		}
3888
3889
	}
3890
3891
	return $ip_array;
3892
}
3893
3894
/**
3895
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
3896
 *
3897
 * @param string $ip The IP to get the hostname from
3898
 * @return string The hostname
3899
 */
3900
function host_from_ip($ip)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $ip. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
3901
{
3902
	global $modSettings;
3903
3904
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
3905
		return $host;
3906
	$t = microtime();
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $t. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
3907
3908
	// Try the Linux host command, perhaps?
3909
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
3910
	{
3911
		if (!isset($modSettings['host_to_dis']))
3912
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
3913
		else
3914
			$test = @shell_exec('host ' . @escapeshellarg($ip));
3915
3916
		// Did host say it didn't find anything?
3917
		if (strpos($test, 'not found') !== false)
3918
			$host = '';
3919
		// Invalid server option?
3920
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
3921
			updateSettings(array('host_to_dis' => 1));
3922
		// Maybe it found something, after all?
3923
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
3924
			$host = $match[1];
3925
	}
3926
3927
	// This is nslookup; usually only Windows, but possibly some Unix?
3928
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
3929
	{
3930
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
3931
		if (strpos($test, 'Non-existent domain') !== false)
3932
			$host = '';
3933
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
3934
			$host = $match[1];
3935
	}
3936
3937
	// This is the last try :/.
3938
	if (!isset($host) || $host === false)
3939
		$host = @gethostbyaddr($ip);
3940
3941
	// It took a long time, so let's cache it!
3942 View Code Duplication
	if (array_sum(explode(' ', microtime())) - array_sum(explode(' ', $t)) > 0.5)
3943
		cache_put_data('hostlookup-' . $ip, $host, 600);
3944
3945
	return $host;
3946
}
3947
3948
/**
3949
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
3950
 *
3951
 * @param string $text The text to split into words
3952
 * @param int $max_chars The maximum number of characters per word
3953
 * @param bool $encrypt Whether to encrypt the results
3954
 * @return array An array of ints or words depending on $encrypt
3955
 */
3956
function text2words($text, $max_chars = 20, $encrypt = false)
3957
{
3958
	global $smcFunc, $context;
3959
3960
	// Step 1: Remove entities/things we don't consider words:
3961
	$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>' => ' ')));
3962
3963
	// Step 2: Entities we left to letters, where applicable, lowercase.
3964
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
3965
3966
	// Step 3: Ready to split apart and index!
3967
	$words = explode(' ', $words);
3968
3969
	if ($encrypt)
3970
	{
3971
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
3972
		$returned_ints = array();
3973
		foreach ($words as $word)
3974
		{
3975
			if (($word = trim($word, '-_\'')) !== '')
3976
			{
3977
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
3978
				$total = 0;
3979
				for ($i = 0; $i < $max_chars; $i++)
3980
					$total += $possible_chars[ord($encrypted{$i})] * pow(63, $i);
3981
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
3982
			}
3983
		}
3984
		return array_unique($returned_ints);
3985
	}
3986
	else
3987
	{
3988
		// Trim characters before and after and add slashes for database insertion.
3989
		$returned_words = array();
3990
		foreach ($words as $word)
3991
			if (($word = trim($word, '-_\'')) !== '')
3992
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
3993
3994
		// Filter out all words that occur more than once.
3995
		return array_unique($returned_words);
3996
	}
3997
}
3998
3999
/**
4000
 * Creates an image/text button
4001
 *
4002
 * @param string $name The name of the button (should be a generic_icons class or the name of an image)
4003
 * @param string $alt The alt text
4004
 * @param string $label The $txt string to use as the label
4005
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
4006
 * @param boolean $force_use Whether to force use of this when template_create_button is available
4007
 * @return string The HTML to display the button
4008
 */
4009
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
4010
{
4011
	global $settings, $txt;
4012
4013
	// Does the current loaded theme have this and we are not forcing the usage of this function?
4014
	if (function_exists('template_create_button') && !$force_use)
4015
		return template_create_button($name, $alt, $label = '', $custom = '');
4016
4017
	if (!$settings['use_image_buttons'])
4018
		return $txt[$alt];
4019
	elseif (!empty($settings['use_buttons']))
4020
		return '<span class="generic_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
4021
	else
4022
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
4023
}
4024
4025
/**
4026
 * Sets up all of the top menu buttons
4027
 * Saves them in the cache if it is available and on
4028
 * Places the results in $context
4029
 *
4030
 */
4031
function setupMenuContext()
4032
{
4033
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings;
4034
4035
	// Set up the menu privileges.
4036
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
4037
	$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'));
4038
4039
	$context['allow_memberlist'] = allowedTo('view_mlist');
4040
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
4041
	$context['allow_moderation_center'] = $context['user']['can_mod'];
4042
	$context['allow_pm'] = allowedTo('pm_read');
4043
4044
	$cacheTime = $modSettings['lastActive'] * 60;
4045
4046
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
4047
	if (!isset($context['allow_calendar_event']))
4048
	{
4049
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
4050
4051
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
4052 View Code Duplication
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
4053
		{
4054
			$boards_can_post = boardsAllowedTo('post_new');
4055
			$context['allow_calendar_event'] &= !empty($boards_can_post);
4056
		}
4057
	}
4058
4059
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
4060
	if (!$context['user']['is_guest'])
4061
	{
4062
		addInlineJavaScript('
4063
	var user_menus = new smc_PopupMenu();
4064
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
4065
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u='. $context['user']['id'] .'");', true);
4066
		if ($context['allow_pm'])
4067
			addInlineJavaScript('
4068
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
4069
4070
		if (!empty($modSettings['enable_ajax_alerts']))
4071
		{
4072
			require_once($sourcedir . '/Subs-Notify.php');
4073
4074
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
4075
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
4076
4077
			addInlineJavaScript('
4078
	var new_alert_title = "' . $context['forum_name'] . '";
4079
	var alert_timeout = ' . $timeout . ';');
4080
			loadJavaScriptFile('alerts.js', array(), 'smf_alerts');
4081
		}
4082
	}
4083
4084
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
4085
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
4086
	{
4087
		$buttons = array(
4088
			'home' => array(
4089
				'title' => $txt['home'],
4090
				'href' => $scripturl,
4091
				'show' => true,
4092
				'sub_buttons' => array(
4093
				),
4094
				'is_last' => $context['right_to_left'],
4095
			),
4096
			'search' => array(
4097
				'title' => $txt['search'],
4098
				'href' => $scripturl . '?action=search',
4099
				'show' => $context['allow_search'],
4100
				'sub_buttons' => array(
4101
				),
4102
			),
4103
			'admin' => array(
4104
				'title' => $txt['admin'],
4105
				'href' => $scripturl . '?action=admin',
4106
				'show' => $context['allow_admin'],
4107
				'sub_buttons' => array(
4108
					'featuresettings' => array(
4109
						'title' => $txt['modSettings_title'],
4110
						'href' => $scripturl . '?action=admin;area=featuresettings',
4111
						'show' => allowedTo('admin_forum'),
4112
					),
4113
					'packages' => array(
4114
						'title' => $txt['package'],
4115
						'href' => $scripturl . '?action=admin;area=packages',
4116
						'show' => allowedTo('admin_forum'),
4117
					),
4118
					'errorlog' => array(
4119
						'title' => $txt['errlog'],
4120
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
4121
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
4122
					),
4123
					'permissions' => array(
4124
						'title' => $txt['edit_permissions'],
4125
						'href' => $scripturl . '?action=admin;area=permissions',
4126
						'show' => allowedTo('manage_permissions'),
4127
					),
4128
					'memberapprove' => array(
4129
						'title' => $txt['approve_members_waiting'],
4130
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
4131
						'show' => !empty($context['unapproved_members']),
4132
						'is_last' => true,
4133
					),
4134
				),
4135
			),
4136
			'moderate' => array(
4137
				'title' => $txt['moderate'],
4138
				'href' => $scripturl . '?action=moderate',
4139
				'show' => $context['allow_moderation_center'],
4140
				'sub_buttons' => array(
4141
					'modlog' => array(
4142
						'title' => $txt['modlog_view'],
4143
						'href' => $scripturl . '?action=moderate;area=modlog',
4144
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4145
					),
4146
					'poststopics' => array(
4147
						'title' => $txt['mc_unapproved_poststopics'],
4148
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
4149
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4150
					),
4151
					'attachments' => array(
4152
						'title' => $txt['mc_unapproved_attachments'],
4153
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
4154
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4155
					),
4156
					'reports' => array(
4157
						'title' => $txt['mc_reported_posts'],
4158
						'href' => $scripturl . '?action=moderate;area=reportedposts',
4159
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4160
					),
4161
					'reported_members' => array(
4162
						'title' => $txt['mc_reported_members'],
4163
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
4164
						'show' => allowedTo('moderate_forum'),
4165
						'is_last' => true,
4166
					)
4167
				),
4168
			),
4169
			'calendar' => array(
4170
				'title' => $txt['calendar'],
4171
				'href' => $scripturl . '?action=calendar',
4172
				'show' => $context['allow_calendar'],
4173
				'sub_buttons' => array(
4174
					'view' => array(
4175
						'title' => $txt['calendar_menu'],
4176
						'href' => $scripturl . '?action=calendar',
4177
						'show' => $context['allow_calendar_event'],
4178
					),
4179
					'post' => array(
4180
						'title' => $txt['calendar_post_event'],
4181
						'href' => $scripturl . '?action=calendar;sa=post',
4182
						'show' => $context['allow_calendar_event'],
4183
						'is_last' => true,
4184
					),
4185
				),
4186
			),
4187
			'mlist' => array(
4188
				'title' => $txt['members_title'],
4189
				'href' => $scripturl . '?action=mlist',
4190
				'show' => $context['allow_memberlist'],
4191
				'sub_buttons' => array(
4192
					'mlist_view' => array(
4193
						'title' => $txt['mlist_menu_view'],
4194
						'href' => $scripturl . '?action=mlist',
4195
						'show' => true,
4196
					),
4197
					'mlist_search' => array(
4198
						'title' => $txt['mlist_search'],
4199
						'href' => $scripturl . '?action=mlist;sa=search',
4200
						'show' => true,
4201
						'is_last' => true,
4202
					),
4203
				),
4204
			),
4205
			'signup' => array(
4206
				'title' => $txt['register'],
4207
				'href' => $scripturl . '?action=signup',
4208
				'show' => $user_info['is_guest'] && $context['can_register'],
4209
				'sub_buttons' => array(
4210
				),
4211
				'is_last' => !$context['right_to_left'],
4212
			),
4213
			'logout' => array(
4214
				'title' => $txt['logout'],
4215
				'href' => $scripturl . '?action=logout;%1$s=%2$s',
4216
				'show' => !$user_info['is_guest'],
4217
				'sub_buttons' => array(
4218
				),
4219
				'is_last' => !$context['right_to_left'],
4220
			),
4221
		);
4222
4223
		// Allow editing menu buttons easily.
4224
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
4225
4226
		// Now we put the buttons in the context so the theme can use them.
4227
		$menu_buttons = array();
4228
		foreach ($buttons as $act => $button)
4229
			if (!empty($button['show']))
4230
			{
4231
				$button['active_button'] = false;
4232
4233
				// This button needs some action.
4234
				if (isset($button['action_hook']))
4235
					$needs_action_hook = true;
4236
4237
				// Make sure the last button truly is the last button.
4238
				if (!empty($button['is_last']))
4239
				{
4240
					if (isset($last_button))
4241
						unset($menu_buttons[$last_button]['is_last']);
4242
					$last_button = $act;
4243
				}
4244
4245
				// Go through the sub buttons if there are any.
4246
				if (!empty($button['sub_buttons']))
4247
					foreach ($button['sub_buttons'] as $key => $subbutton)
4248
					{
4249
						if (empty($subbutton['show']))
4250
							unset($button['sub_buttons'][$key]);
4251
4252
						// 2nd level sub buttons next...
4253
						if (!empty($subbutton['sub_buttons']))
4254
						{
4255
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
4256
							{
4257
								if (empty($sub_button2['show']))
4258
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
4259
							}
4260
						}
4261
					}
4262
4263
				// Does this button have its own icon?
4264
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
4265
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
4266
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
4267
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
4268
				elseif (isset($button['icon']))
4269
					$button['icon'] = '<span class="generic_icons ' . $button['icon'] . '"></span>';
4270
				else
4271
					$button['icon'] = '<span class="generic_icons ' . $act . '"></span>';
4272
4273
				$menu_buttons[$act] = $button;
4274
			}
4275
4276
		if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2)
4277
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
4278
	}
4279
4280
	$context['menu_buttons'] = $menu_buttons;
4281
4282
	// Logging out requires the session id in the url.
4283
	if (isset($context['menu_buttons']['logout']))
4284
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
4285
4286
	// Figure out which action we are doing so we can set the active tab.
4287
	// Default to home.
4288
	$current_action = 'home';
4289
4290
	if (isset($context['menu_buttons'][$context['current_action']]))
4291
		$current_action = $context['current_action'];
4292
	elseif ($context['current_action'] == 'search2')
4293
		$current_action = 'search';
4294
	elseif ($context['current_action'] == 'theme')
4295
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
4296
	elseif ($context['current_action'] == 'register2')
4297
		$current_action = 'register';
4298
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
4299
		$current_action = 'login';
4300
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
4301
		$current_action = 'moderate';
4302
4303
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
4304
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
4305
	{
4306
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
4307
		$context[$current_action] = true;
4308
	}
4309
	elseif ($context['current_action'] == 'pm')
4310
	{
4311
		$current_action = 'self_pm';
4312
		$context['self_pm'] = true;
4313
	}
4314
4315
	$total_mod_reports = 0;
4316
4317
	if (!empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1' && !empty($context['open_mod_reports']))
4318
	{
4319
		$total_mod_reports = $context['open_mod_reports'];
4320
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['title'] .= ' <span class="amt">' . $context['open_mod_reports'] . '</span>';
4321
	}
4322
4323
	// Show how many errors there are
4324 View Code Duplication
	if (!empty($context['num_errors']) && allowedTo('admin_forum'))
4325
	{
4326
		$context['menu_buttons']['admin']['title'] .= ' <span class="amt">' . $context['num_errors'] . '</span>';
4327
		$context['menu_buttons']['admin']['sub_buttons']['errorlog']['title'] .= ' <span class="amt">' . $context['num_errors'] . '</span>';
4328
	}
4329
4330
	// Show number of reported members
4331
	if (!empty($context['open_member_reports']) && allowedTo('moderate_forum'))
4332
	{
4333
		$total_mod_reports += $context['open_member_reports'];
4334
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['title'] .= ' <span class="amt">' . $context['open_member_reports'] . '</span>';
4335
	}
4336
4337 View Code Duplication
	if (!empty($context['unapproved_members']))
4338
	{
4339
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['title'] .= ' <span class="amt">' . $context['unapproved_members'] . '</span>';
4340
		$context['menu_buttons']['admin']['title'] .= ' <span class="amt">' . $context['unapproved_members'] . '</span>';
4341
	}
4342
4343
	// Do we have any open reports?
4344
	if ($total_mod_reports > 0)
4345
	{
4346
		$context['menu_buttons']['moderate']['title'] .= ' <span class="amt">' . $total_mod_reports . '</span>';
4347
	}
4348
4349
	// Not all actions are simple.
4350
	if (!empty($needs_action_hook))
4351
		call_integration_hook('integrate_current_action', array(&$current_action));
4352
4353
	if (isset($context['menu_buttons'][$current_action]))
4354
		$context['menu_buttons'][$current_action]['active_button'] = true;
4355
}
4356
4357
/**
4358
 * Generate a random seed and ensure it's stored in settings.
4359
 */
4360
function smf_seed_generator()
4361
{
4362
	updateSettings(array('rand_seed' => microtime() * 1000000));
4363
}
4364
4365
/**
4366
 * Process functions of an integration hook.
4367
 * calls all functions of the given hook.
4368
 * supports static class method calls.
4369
 *
4370
 * @param string $hook The hook name
4371
 * @param array $parameters An array of parameters this hook implements
4372
 * @return array The results of the functions
4373
 */
4374
function call_integration_hook($hook, $parameters = array())
4375
{
4376
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
4377
	global $context, $txt;
4378
4379
	if ($db_show_debug === true)
4380
		$context['debug']['hooks'][] = $hook;
4381
4382
	// Need to have some control.
4383
	if (!isset($context['instances']))
4384
		$context['instances'] = array();
4385
4386
	$results = array();
4387
	if (empty($modSettings[$hook]))
4388
		return $results;
4389
4390
	$functions = explode(',', $modSettings[$hook]);
4391
	// Loop through each function.
4392
	foreach ($functions as $function)
4393
	{
4394
		// Hook has been marked as "disabled". Skip it!
4395
		if (strpos($function, '!') !== false)
4396
			continue;
4397
4398
		$call = call_helper($function, true);
4399
4400
		// Is it valid?
4401
		if (!empty($call))
4402
			$results[$function] = call_user_func_array($call, $parameters);
4403
4404
		// Whatever it was suppose to call, it failed :(
4405
		elseif (!empty($function))
4406
		{
4407
			loadLanguage('Errors');
4408
4409
			// Get a full path to show on error.
4410
			if (strpos($function, '|') !== false)
4411
			{
4412
				list ($file, $string) = explode('|', $function);
4413
				$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'])));
4414
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
4415
			}
4416
4417
			// "Assume" the file resides on $boarddir somewhere...
4418
			else
4419
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
4420
		}
4421
	}
4422
4423
	return $results;
4424
}
4425
4426
/**
4427
 * Add a function for integration hook.
4428
 * does nothing if the function is already added.
4429
 *
4430
 * @param string $hook The complete hook name.
4431
 * @param string $function The function name. Can be a call to a method via Class::method.
4432
 * @param bool $permanent If true, updates the value in settings table.
4433
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4434
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4435
 */
4436
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
4437
{
4438
	global $smcFunc, $modSettings;
4439
4440
	// Any objects?
4441
	if ($object)
4442
		$function = $function . '#';
4443
4444
	// Any files  to load?
4445
	if (!empty($file) && is_string($file))
4446
		$function = $file . (!empty($function) ? '|' . $function : '');
4447
4448
	// Get the correct string.
4449
	$integration_call = $function;
4450
4451
	// Is it going to be permanent?
4452
	if ($permanent)
4453
	{
4454
		$request = $smcFunc['db_query']('', '
4455
			SELECT value
4456
			FROM {db_prefix}settings
4457
			WHERE variable = {string:variable}',
4458
			array(
4459
				'variable' => $hook,
4460
			)
4461
		);
4462
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
4463
		$smcFunc['db_free_result']($request);
4464
4465
		if (!empty($current_functions))
4466
		{
4467
			$current_functions = explode(',', $current_functions);
4468
			if (in_array($integration_call, $current_functions))
4469
				return;
4470
4471
			$permanent_functions = array_merge($current_functions, array($integration_call));
4472
		}
4473
		else
4474
			$permanent_functions = array($integration_call);
4475
4476
		updateSettings(array($hook => implode(',', $permanent_functions)));
4477
	}
4478
4479
	// Make current function list usable.
4480
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4481
4482
	// Do nothing, if it's already there.
4483
	if (in_array($integration_call, $functions))
4484
		return;
4485
4486
	$functions[] = $integration_call;
4487
	$modSettings[$hook] = implode(',', $functions);
4488
}
4489
4490
/**
4491
 * Remove an integration hook function.
4492
 * Removes the given function from the given hook.
4493
 * Does nothing if the function is not available.
4494
 *
4495
 * @param string $hook The complete hook name.
4496
 * @param string $function The function name. Can be a call to a method via Class::method.
4497
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
4498
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4499
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4500
 * @see add_integration_function
4501
 */
4502
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...
4503
{
4504
	global $smcFunc, $modSettings;
4505
4506
	// Any objects?
4507
	if ($object)
4508
		$function = $function . '#';
4509
4510
	// Any files  to load?
4511
	if (!empty($file) && is_string($file))
4512
		$function = $file . '|' . $function;
4513
4514
	// Get the correct string.
4515
	$integration_call = $function;
4516
4517
	// Get the permanent functions.
4518
	$request = $smcFunc['db_query']('', '
4519
		SELECT value
4520
		FROM {db_prefix}settings
4521
		WHERE variable = {string:variable}',
4522
		array(
4523
			'variable' => $hook,
4524
		)
4525
	);
4526
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
4527
	$smcFunc['db_free_result']($request);
4528
4529
	if (!empty($current_functions))
4530
	{
4531
		$current_functions = explode(',', $current_functions);
4532
4533
		if (in_array($integration_call, $current_functions))
4534
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
4535
	}
4536
4537
	// Turn the function list into something usable.
4538
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4539
4540
	// You can only remove it if it's available.
4541
	if (!in_array($integration_call, $functions))
4542
		return;
4543
4544
	$functions = array_diff($functions, array($integration_call));
4545
	$modSettings[$hook] = implode(',', $functions);
4546
}
4547
4548
/**
4549
 * Receives a string and tries to figure it out if its a method or a function.
4550
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
4551
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
4552
 * Prepare and returns a callable depending on the type of method/function found.
4553
 *
4554
 * @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)
4555
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
4556
 * @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.
4557
 */
4558
function call_helper($string, $return = false)
4559
{
4560
	global $context, $smcFunc, $txt, $db_show_debug;
4561
4562
	// Really?
4563
	if (empty($string))
4564
		return false;
4565
4566
	// An array? should be a "callable" array IE array(object/class, valid_callable).
4567
	// A closure? should be a callable one.
4568
	if (is_array($string) || $string instanceof Closure)
4569
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
4570
4571
	// No full objects, sorry! pass a method or a property instead!
4572
	if (is_object($string))
4573
		return false;
4574
4575
	// Stay vitaminized my friends...
4576
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
4577
4578
	// Is there a file to load?
4579
	$string = load_file($string);
4580
4581
	// Loaded file failed
4582
	if (empty($string))
4583
		return false;
4584
4585
	// Found a method.
4586
	if (strpos($string, '::') !== false)
4587
	{
4588
		list ($class, $method) = explode('::', $string);
4589
4590
		// Check if a new object will be created.
4591
		if (strpos($method, '#') !== false)
4592
		{
4593
			// Need to remove the # thing.
4594
			$method = str_replace('#', '', $method);
4595
4596
			// Don't need to create a new instance for every method.
4597
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
4598
			{
4599
				$context['instances'][$class] = new $class;
4600
4601
				// Add another one to the list.
4602
				if ($db_show_debug === true)
4603
				{
4604
					if (!isset($context['debug']['instances']))
4605
						$context['debug']['instances'] = array();
4606
4607
					$context['debug']['instances'][$class] = $class;
4608
				}
4609
			}
4610
4611
			$func = array($context['instances'][$class], $method);
4612
		}
4613
4614
		// Right then. This is a call to a static method.
4615
		else
4616
			$func = array($class, $method);
4617
	}
4618
4619
	// Nope! just a plain regular function.
4620
	else
4621
		$func = $string;
4622
4623
	// Right, we got what we need, time to do some checks.
4624
	if (!is_callable($func, false, $callable_name))
4625
	{
4626
		loadLanguage('Errors');
4627
		log_error(sprintf($txt['subAction_fail'], $callable_name), 'general');
4628
4629
		// Gotta tell everybody.
4630
		return false;
4631
	}
4632
4633
	// Everything went better than expected.
4634
	else
4635
	{
4636
		// What are we gonna do about it?
4637
		if ($return)
4638
			return $func;
4639
4640
		// If this is a plain function, avoid the heat of calling call_user_func().
4641
		else
4642
		{
4643
			if (is_array($func))
4644
				call_user_func($func);
4645
4646
			else
4647
				$func();
4648
		}
4649
	}
4650
}
4651
4652
/**
4653
 * Receives a string and tries to figure it out if it contains info to load a file.
4654
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
4655
 * 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.
4656
 *
4657
 * @param string $string The string containing a valid format.
4658
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
4659
 */
4660
function load_file($string)
4661
{
4662
	global $sourcedir, $txt, $boarddir, $settings;
4663
4664
	if (empty($string))
4665
		return false;
4666
4667
	if (strpos($string, '|') !== false)
4668
	{
4669
		list ($file, $string) = explode('|', $string);
4670
4671
		// Match the wildcards to their regular vars.
4672
		if (empty($settings['theme_dir']))
4673
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
4674
4675
		else
4676
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
4677
4678
		// Load the file if it can be loaded.
4679
		if (file_exists($absPath))
4680
			require_once($absPath);
4681
4682
		// No? try a fallback to $sourcedir
4683
		else
4684
		{
4685
			$absPath = $sourcedir .'/'. $file;
4686
4687
			if (file_exists($absPath))
4688
				require_once($absPath);
4689
4690
			// Sorry, can't do much for you at this point.
4691
			else
4692
			{
4693
				loadLanguage('Errors');
4694
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
4695
4696
				// File couldn't be loaded.
4697
				return false;
4698
			}
4699
		}
4700
	}
4701
4702
	return $string;
4703
}
4704
4705
/**
4706
 * Prepares an array of "likes" info for the topic specified by $topic
4707
 * @param integer $topic The topic ID to fetch the info from.
4708
 * @return array An array of IDs of messages in the specified topic that the current user likes
0 ignored issues
show
Documentation introduced by
Should the return type not be array|string? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
4709
 */
4710
function prepareLikesContext($topic)
4711
{
4712
	global $user_info, $smcFunc;
4713
4714
	// Make sure we have something to work with.
4715
	if (empty($topic))
4716
		return array();
4717
4718
4719
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
4720
	$user = $user_info['id'];
4721
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
4722
	$ttl = 180;
4723
4724
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
4725
	{
4726
		$temp = array();
4727
		$request = $smcFunc['db_query']('', '
4728
			SELECT content_id
4729
			FROM {db_prefix}user_likes AS l
4730
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
4731
			WHERE l.id_member = {int:current_user}
4732
				AND l.content_type = {literal:msg}
4733
				AND m.id_topic = {int:topic}',
4734
			array(
4735
				'current_user' => $user,
4736
				'topic' => $topic,
4737
			)
4738
		);
4739
		while ($row = $smcFunc['db_fetch_assoc']($request))
4740
			$temp[] = (int) $row['content_id'];
4741
4742
		cache_put_data($cache_key, $temp, $ttl);
4743
	}
4744
4745
	return $temp;
4746
}
4747
4748
/**
4749
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
4750
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
4751
 * that are not normally displayable.  This converts the popular ones that
4752
 * appear from a cut and paste from windows.
4753
 *
4754
 * @param string $string The string
4755
 * @return string The sanitized string
4756
 */
4757
function sanitizeMSCutPaste($string)
4758
{
4759
	global $context;
4760
4761
	if (empty($string))
4762
		return $string;
4763
4764
	// UTF-8 occurences of MS special characters
4765
	$findchars_utf8 = array(
4766
		"\xe2\x80\x9a",	// single low-9 quotation mark
4767
		"\xe2\x80\x9e",	// double low-9 quotation mark
4768
		"\xe2\x80\xa6",	// horizontal ellipsis
4769
		"\xe2\x80\x98",	// left single curly quote
4770
		"\xe2\x80\x99",	// right single curly quote
4771
		"\xe2\x80\x9c",	// left double curly quote
4772
		"\xe2\x80\x9d",	// right double curly quote
4773
		"\xe2\x80\x93",	// en dash
4774
		"\xe2\x80\x94",	// em dash
4775
	);
4776
4777
	// windows 1252 / iso equivalents
4778
	$findchars_iso = array(
4779
		chr(130),
4780
		chr(132),
4781
		chr(133),
4782
		chr(145),
4783
		chr(146),
4784
		chr(147),
4785
		chr(148),
4786
		chr(150),
4787
		chr(151),
4788
	);
4789
4790
	// safe replacements
4791
	$replacechars = array(
4792
		',',	// &sbquo;
4793
		',,',	// &bdquo;
4794
		'...',	// &hellip;
4795
		"'",	// &lsquo;
4796
		"'",	// &rsquo;
4797
		'"',	// &ldquo;
4798
		'"',	// &rdquo;
4799
		'-',	// &ndash;
4800
		'--',	// &mdash;
4801
	);
4802
4803
	if ($context['utf8'])
4804
		$string = str_replace($findchars_utf8, $replacechars, $string);
4805
	else
4806
		$string = str_replace($findchars_iso, $replacechars, $string);
4807
4808
	return $string;
4809
}
4810
4811
/**
4812
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
4813
 *
4814
 * Callback function for preg_replace_callback in subs-members
4815
 * Uses capture group 2 in the supplied array
4816
 * Does basic scan to ensure characters are inside a valid range
4817
 *
4818
 * @param array $matches An array of matches (relevant info should be the 3rd item)
4819
 * @return string A fixed string
4820
 */
4821
function replaceEntities__callback($matches)
4822
{
4823
	global $context;
4824
4825
	if (!isset($matches[2]))
4826
		return '';
4827
4828
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
4829
4830
	// remove left to right / right to left overrides
4831
	if ($num === 0x202D || $num === 0x202E)
4832
		return '';
4833
4834
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
4835
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
4836
		return '&#' . $num . ';';
4837
4838
	if (empty($context['utf8']))
4839
	{
4840
		// no control characters
4841
		if ($num < 0x20)
4842
			return '';
4843
		// text is text
4844
		elseif ($num < 0x80)
4845
			return chr($num);
4846
		// all others get html-ised
4847
		else
4848
			return '&#' . $matches[2] . ';';
4849
	}
4850
	else
4851
	{
4852
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
4853
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
4854
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
4855
			return '';
4856
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
4857
		elseif ($num < 0x80)
4858
			return chr($num);
4859
		// <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...
4860 View Code Duplication
		elseif ($num < 0x800)
4861
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
4862
		// < 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...
4863 View Code Duplication
		elseif ($num < 0x10000)
4864
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4865
		// <= 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...
4866 View Code Duplication
		else
4867
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4868
	}
4869
}
4870
4871
/**
4872
 * Converts html entities to utf8 equivalents
4873
 *
4874
 * Callback function for preg_replace_callback
4875
 * Uses capture group 1 in the supplied array
4876
 * Does basic checks to keep characters inside a viewable range.
4877
 *
4878
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
4879
 * @return string The fixed string
4880
 */
4881
function fixchar__callback($matches)
4882
{
4883
	if (!isset($matches[1]))
4884
		return '';
4885
4886
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
4887
4888
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
4889
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
4890
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
4891
		return '';
4892
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
4893
	elseif ($num < 0x80)
4894
		return chr($num);
4895
	// <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...
4896 View Code Duplication
	elseif ($num < 0x800)
4897
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
4898
	// < 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...
4899 View Code Duplication
	elseif ($num < 0x10000)
4900
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4901
	// <= 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...
4902 View Code Duplication
	else
4903
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4904
}
4905
4906
/**
4907
 * Strips out invalid html entities, replaces others with html style &#123; codes
4908
 *
4909
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
4910
 * strpos, strlen, substr etc
4911
 *
4912
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
4913
 * @return string The fixed string
4914
 */
4915
function entity_fix__callback($matches)
4916
{
4917
	if (!isset($matches[2]))
4918
		return '';
4919
4920
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
4921
4922
	// we don't allow control characters, characters out of range, byte markers, etc
4923
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
4924
		return '';
4925
	else
4926
		return '&#' . $num . ';';
4927
}
4928
4929
/**
4930
 * Return a Gravatar URL based on
4931
 * - the supplied email address,
4932
 * - the global maximum rating,
4933
 * - the global default fallback,
4934
 * - maximum sizes as set in the admin panel.
4935
 *
4936
 * It is SSL aware, and caches most of the parameters.
4937
 *
4938
 * @param string $email_address The user's email address
4939
 * @return string The gravatar URL
4940
 */
4941
function get_gravatar_url($email_address)
4942
{
4943
	global $modSettings, $smcFunc;
4944
	static $url_params = null;
4945
4946
	if ($url_params === null)
4947
	{
4948
		$ratings = array('G', 'PG', 'R', 'X');
4949
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
4950
		$url_params = array();
4951 View Code Duplication
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
4952
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
4953 View Code Duplication
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
4954
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
4955
		if (!empty($modSettings['avatar_max_width_external']))
4956
			$size_string = (int) $modSettings['avatar_max_width_external'];
4957
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
4958
			if ((int) $modSettings['avatar_max_height_external'] < $size_string)
4959
				$size_string = $modSettings['avatar_max_height_external'];
4960
4961
		if (!empty($size_string))
4962
			$url_params[] = 's=' . $size_string;
4963
	}
4964
	$http_method = !empty($modSettings['force_ssl']) && $modSettings['force_ssl'] == 2 ? 'https://secure' : 'http://www';
4965
4966
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
4967
}
4968
4969
/**
4970
 * Get a list of timezones.
4971
 *
4972
 * @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'.
4973
 * @return array An array of timezone info.
4974
 */
4975
function smf_list_timezones($when = 'now')
4976
{
4977
	global $smcFunc, $modSettings;
4978
	static $timezones = null, $lastwhen = null;
4979
4980
	// No point doing this over if we already did it once
4981
	if (!empty($timezones) && $when == $lastwhen)
4982
		return $timezones;
4983
	else
4984
		$lastwhen = $when;
4985
4986
	// Parseable datetime string?
4987
	if (is_int($timestamp = strtotime($when)))
4988
		$when = $timestamp;
4989
4990
	// A Unix timestamp?
4991
	elseif (is_numeric($when))
4992
		$when = intval($when);
4993
4994
	// Invalid value? Just get current Unix timestamp.
4995
	else
4996
		$when = time();
4997
4998
	// We'll need these too
4999
	$date_when = date_create('@' . $when);
5000
	$later = (int) date_format(date_add($date_when, date_interval_create_from_date_string('1 year')), 'U');
5001
5002
	// Prefer and give custom descriptions for these time zones
5003
	// If the description is left empty, it will be filled in with the names of matching cities
5004
	$timezone_descriptions = array(
5005
		'America/Adak' => 'Aleutian Islands',
5006
		'Pacific/Marquesas' => 'Marquesas Islands',
5007
		'Pacific/Gambier' => 'Gambier Islands',
5008
		'America/Anchorage' => 'Alaska',
5009
		'Pacific/Pitcairn' => 'Pitcairn Islands',
5010
		'America/Los_Angeles' => 'Pacific Time (USA, Canada)',
5011
		'America/Denver' => 'Mountain Time (USA, Canada)',
5012
		'America/Phoenix' => 'Mountain Time (no DST)',
5013
		'America/Chicago' => 'Central Time (USA, Canada)',
5014
		'America/Belize' => 'Central Time (no DST)',
5015
		'America/New_York' => 'Eastern Time (USA, Canada)',
5016
		'America/Atikokan' => 'Eastern Time (no DST)',
5017
		'America/Halifax' => 'Atlantic Time (Canada)',
5018
		'America/Anguilla' => 'Atlantic Time (no DST)',
5019
		'America/St_Johns' => 'Newfoundland',
5020
		'America/Chihuahua' => 'Chihuahua, Mazatlan',
5021
		'Pacific/Easter' => 'Easter Island',
5022
		'Atlantic/Stanley' => 'Falkland Islands',
5023
		'America/Miquelon' => 'Saint Pierre and Miquelon',
5024
		'America/Argentina/Buenos_Aires' => 'Buenos Aires',
5025
		'America/Sao_Paulo' => 'Brasilia Time',
5026
		'America/Araguaina' => 'Brasilia Time (no DST)',
5027
		'America/Godthab' => 'Greenland',
5028
		'America/Noronha' => 'Fernando de Noronha',
5029
		'Atlantic/Reykjavik' => 'Greenwich Mean Time (no DST)',
5030
		'Europe/London' => '',
5031
		'Europe/Berlin' => 'Central European Time',
5032
		'Europe/Helsinki' => 'Eastern European Time',
5033
		'Africa/Brazzaville' => 'Brazzaville, Lagos, Porto-Novo',
5034
		'Asia/Jerusalem' => 'Jerusalem',
5035
		'Europe/Moscow' => '',
5036
		'Africa/Khartoum' => 'Eastern Africa Time',
5037
		'Asia/Riyadh' => 'Arabia Time',
5038
		'Asia/Kolkata' => 'India, Sri Lanka',
5039
		'Asia/Yekaterinburg' => 'Yekaterinburg, Tyumen',
5040
		'Asia/Dhaka' => 'Astana, Dhaka',
5041
		'Asia/Rangoon' => 'Yangon/Rangoon',
5042
		'Indian/Christmas' => 'Christmas Island',
5043
		'Antarctica/DumontDUrville' => 'Dumont D\'Urville Station',
5044
		'Antarctica/Vostok' => 'Vostok Station',
5045
		'Australia/Lord_Howe' => 'Lord Howe Island',
5046
		'Pacific/Guadalcanal' => 'Solomon Islands',
5047
		'Pacific/Norfolk' => 'Norfolk Island',
5048
		'Pacific/Noumea' => 'New Caledonia',
5049
		'Pacific/Auckland' => 'Auckland, McMurdo Station',
5050
		'Pacific/Kwajalein' => 'Marshall Islands',
5051
		'Pacific/Chatham' => 'Chatham Islands',
5052
	);
5053
5054
	// Should we put time zones from certain countries at the top of the list?
5055
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5056
	$priority_tzids = array();
5057
	foreach ($priority_countries as $country)
5058
	{
5059
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
5060
		if (!empty($country_tzids))
5061
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5062
	}
5063
5064
	// Process the preferred timezones first, then the rest.
5065
	$tzids = array_keys($timezone_descriptions) + array_diff(timezone_identifiers_list(), array_keys($timezone_descriptions));
5066
5067
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5068
	foreach ($tzids as $tzid)
5069
	{
5070
		// We don't want UTC right now
5071
		if ($tzid == 'UTC')
5072
			continue;
5073
5074
		$tz = timezone_open($tzid);
5075
5076
		// First, get the set of transition rules for this tzid
5077
		$tzinfo = timezone_transitions_get($tz, $when, $later);
5078
5079
		$tzinfo[0]['abbr'] = fix_tz_abbrev($tzid, $tzinfo[0]['abbr']);
5080
5081
		$tzkey = $smcFunc['json_encode']($tzinfo);
5082
5083
		// Next, get the geographic info for this tzid
5084
		$tzgeo = timezone_location_get($tz);
5085
5086
		// Don't overwrite our preferred tzids
5087
		if (empty($zones[$tzkey]['tzid']))
5088
			$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...
5089
5090
		// A time zone from a prioritized country?
5091
		if (in_array($tzid, $priority_tzids))
5092
			$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...
5093
5094
		// Keep track of the location and offset for this tzid
5095
		$tzid_parts = explode('/', $tzid);
5096
		$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...
5097
		$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...
5098
		$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...
5099
	}
5100
5101
	// Sort by offset then longitude
5102
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $longitudes, SORT_ASC, SORT_NUMERIC, $zones);
5103
5104
	// Build the final array of formatted values
5105
	$priority_timezones = array();
5106
	$timezones = array();
5107
	foreach ($zones as $tzkey => $tzvalue)
5108
	{
5109
		// !!! TODO: Why encode this and then decode it here?
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
5110
		$tzinfo = $smcFunc['json_decode']($tzkey, true);
5111
5112
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
5113
5114
		if (!empty($timezone_descriptions[$tzvalue['tzid']]))
5115
			$desc = $timezone_descriptions[$tzvalue['tzid']];
5116
		else
5117
			$desc = implode(', ', array_unique($tzvalue['locations']));
5118
5119
		if (isset($priority_zones[$tzkey]))
5120
			$priority_timezones[$tzvalue['tzid']] = $tzinfo[0]['abbr'] . ' - ' . $desc . ' [UTC' . date_format($date_when, 'P') . ']';
5121
		else
5122
			$timezones[$tzvalue['tzid']] = $tzinfo[0]['abbr'] . ' - ' . $desc . ' [UTC' . date_format($date_when, 'P') . ']';
5123
	}
5124
5125
	$timezones = array_merge(
5126
		$priority_timezones,
5127
		array('' => '(Forum Default)', 'UTC' => 'UTC - Coordinated Universal Time'),
5128
		$timezones
5129
	);
5130
5131
	return $timezones;
5132
}
5133
5134
/**
5135
 * Reformats certain time zone abbreviations to look better.
5136
 *
5137
 * Some of PHP's time zone abbreviations are just numerical offsets from UTC, e.g. '+04'
5138
 * These look weird and are kind of useless, so we make them look better.
5139
 *
5140
 * @param string $tzid The Olsen time zome identifier for a time zone.
5141
 * @param string $tz_abbrev The abbreviation PHP provided for this time zone.
5142
 * @return string The fixed version of $tz_abbrev.
5143
 */
5144
function fix_tz_abbrev($tzid, $tz_abbrev)
5145
{
5146
	// Is this abbreviation just a numerical offset?
5147
	if (strspn($tz_abbrev, '+-') > 0)
5148
	{
5149
		// To get on this list, a time zone must be historically stable and must not observe daylight saving time
5150
		$missing_tz_abbrs = array(
5151
			'Antarctica/Casey' => 'CAST',
5152
			'Antarctica/Davis' => 'DAVT',
5153
			'Antarctica/DumontDUrville' => 'DDUT',
5154
			'Antarctica/Mawson' => 'MAWT',
5155
			'Antarctica/Rothera' => 'ART',
5156
			'Antarctica/Syowa' => 'SYOT',
5157
			'Antarctica/Vostok' => 'VOST',
5158
			'Asia/Almaty' => 'ALMT',
5159
			'Asia/Aqtau' => 'ORAT',
5160
			'Asia/Aqtobe' => 'AQTT',
5161
			'Asia/Ashgabat' => 'TMT',
5162
			'Asia/Bishkek' => 'KGT',
5163
			'Asia/Colombo' => 'IST',
5164
			'Asia/Dushanbe' => 'TJT',
5165
			'Asia/Oral' => 'ORAT',
5166
			'Asia/Qyzylorda' => 'QYZT',
5167
			'Asia/Samarkand' => 'UZT',
5168
			'Asia/Tashkent' => 'UZT',
5169
			'Asia/Tbilisi' => 'GET',
5170
			'Asia/Yerevan' => 'AMT',
5171
			'Europe/Istanbul' => 'TRT',
5172
			'Europe/Minsk' => 'MSK',
5173
			'Indian/Kerguelen' => 'TFT',
5174
		);
5175
5176
		if (!empty($missing_tz_abbrs[$tzid]))
5177
			$tz_abbrev = $missing_tz_abbrs[$tzid];
5178
		else
5179
		{
5180
			// Russia likes to experiment with time zones often, and names them as offsets from Moscow
5181
			$tz_location = timezone_location_get(timezone_open($tzid));
5182
			if ($tz_location['country_code'] == 'RU')
5183
			{
5184
				$msk_offset = intval($tz_abbrev) - 3;
5185
				$tz_abbrev = 'MSK' . (!empty($msk_offset) ? sprintf('%+0d', $msk_offset) : '');
5186
			}
5187
		}
5188
5189
		// Still no good? We'll just mark it as a UTC offset
5190
		if (strspn($tz_abbrev, '+-') > 0)
5191
		{
5192
			$tz_abbrev = intval($tz_abbrev);
5193
			$tz_abbrev = 'UTC' . (!empty($tz_abbrev) ? sprintf('%+0d', $tz_abbrev) : '');
5194
		}
5195
	}
5196
5197
	return $tz_abbrev;
5198
}
5199
5200
/**
5201
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
5202
 * @return string|false The IP address in binary or false
5203
 */
5204
function inet_ptod($ip_address)
5205
{
5206
	if (!isValidIP($ip_address))
5207
		return $ip_address;
5208
5209
	$bin = inet_pton($ip_address);
5210
	return $bin;
5211
}
5212
5213
/**
5214
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
5215
 * @return string|false The IP address in presentation format or false on error
5216
 */
5217
function inet_dtop($bin)
5218
{
5219
	if(empty($bin))
5220
		return '';
5221
5222
	global $db_type;
5223
5224
	if ($db_type == 'postgresql')
5225
		return $bin;
5226
5227
	$ip_address = inet_ntop($bin);
5228
5229
	return $ip_address;
5230
}
5231
5232
/**
5233
 * Safe serialize() and unserialize() replacements
5234
 *
5235
 * @license Public Domain
5236
 *
5237
 * @author anthon (dot) pang (at) gmail (dot) com
5238
 */
5239
5240
/**
5241
 * Safe serialize() replacement. Recursive
5242
 * - output a strict subset of PHP's native serialized representation
5243
 * - does not serialize objects
5244
 *
5245
 * @param mixed $value
5246
 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
5247
 */
5248
function _safe_serialize($value)
5249
{
5250
	if(is_null($value))
5251
		return 'N;';
5252
5253
	if(is_bool($value))
5254
		return 'b:'. (int) $value .';';
5255
5256
	if(is_int($value))
5257
		return 'i:'. $value .';';
5258
5259
	if(is_float($value))
5260
		return 'd:'. str_replace(',', '.', $value) .';';
5261
5262
	if(is_string($value))
5263
		return 's:'. strlen($value) .':"'. $value .'";';
5264
5265
	if(is_array($value))
5266
	{
5267
		$out = '';
5268
		foreach($value as $k => $v)
5269
			$out .= _safe_serialize($k) . _safe_serialize($v);
5270
5271
		return 'a:'. count($value) .':{'. $out .'}';
5272
	}
5273
5274
	// safe_serialize cannot serialize resources or objects.
5275
	return false;
5276
}
5277
/**
5278
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
5279
 *
5280
 * @param mixed $value
5281
 * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|false?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
5282
 */
5283 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...
5284
{
5285
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5286
	if (function_exists('mb_internal_encoding') &&
5287
		(((int) ini_get('mbstring.func_overload')) & 2))
5288
	{
5289
		$mbIntEnc = mb_internal_encoding();
5290
		mb_internal_encoding('ASCII');
5291
	}
5292
5293
	$out = _safe_serialize($value);
5294
5295
	if (isset($mbIntEnc))
5296
		mb_internal_encoding($mbIntEnc);
5297
5298
	return $out;
5299
}
5300
5301
/**
5302
 * Safe unserialize() replacement
5303
 * - accepts a strict subset of PHP's native serialized representation
5304
 * - does not unserialize objects
5305
 *
5306
 * @param string $str
5307
 * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use boolean|integer|double|string|null|array.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
5308
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
5309
 */
5310
function _safe_unserialize($str)
5311
{
5312
	// Input  is not a string.
5313
	if(empty($str) || !is_string($str))
5314
		return false;
5315
5316
	$stack = array();
5317
	$expected = array();
5318
5319
	/*
5320
	 * states:
5321
	 *   0 - initial state, expecting a single value or array
5322
	 *   1 - terminal state
5323
	 *   2 - in array, expecting end of array or a key
5324
	 *   3 - in array, expecting value or another array
5325
	 */
5326
	$state = 0;
5327
	while($state != 1)
5328
	{
5329
		$type = isset($str[0]) ? $str[0] : '';
5330
		if($type == '}')
5331
			$str = substr($str, 1);
5332
5333
		else if($type == 'N' && $str[1] == ';')
5334
		{
5335
			$value = null;
5336
			$str = substr($str, 2);
5337
		}
5338
		else if($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
5339
		{
5340
			$value = $matches[1] == '1' ? true : false;
5341
			$str = substr($str, 4);
5342
		}
5343
		else if($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
5344
		{
5345
			$value = (int)$matches[1];
5346
			$str = $matches[2];
5347
		}
5348
		else if($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
5349
		{
5350
			$value = (float)$matches[1];
5351
			$str = $matches[3];
5352
		}
5353
		else if($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int)$matches[1], 2) == '";')
5354
		{
5355
			$value = substr($matches[2], 0, (int)$matches[1]);
5356
			$str = substr($matches[2], (int)$matches[1] + 2);
5357
		}
5358
		else if($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
5359
		{
5360
			$expectedLength = (int)$matches[1];
5361
			$str = $matches[2];
5362
		}
5363
5364
		// Object or unknown/malformed type.
5365
		else
5366
			return false;
5367
5368
		switch($state)
5369
		{
5370
			case 3: // In array, expecting value or another array.
5371
				if($type == 'a')
5372
				{
5373
					$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...
5374
					$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...
5375
					$list = &$list[$key];
5376
					$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...
5377
					$state = 2;
5378
					break;
5379
				}
5380
				if($type != '}')
5381
				{
5382
					$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...
5383
					$state = 2;
5384
					break;
5385
				}
5386
5387
				// Missing array value.
5388
				return false;
5389
5390
			case 2: // in array, expecting end of array or a key
5391
				if($type == '}')
5392
				{
5393
					// Array size is less than expected.
5394
					if(count($list) < end($expected))
5395
						return false;
5396
5397
					unset($list);
5398
					$list = &$stack[count($stack)-1];
5399
					array_pop($stack);
5400
5401
					// Go to terminal state if we're at the end of the root array.
5402
					array_pop($expected);
5403
5404
					if(count($expected) == 0)
5405
						$state = 1;
5406
5407
					break;
5408
				}
5409
5410
				if($type == 'i' || $type == 's')
5411
				{
5412
					// Array size exceeds expected length.
5413
					if(count($list) >= end($expected))
5414
						return false;
5415
5416
					$key = $value;
5417
					$state = 3;
5418
					break;
5419
				}
5420
5421
				// Illegal array index type.
5422
				return false;
5423
5424
			// Expecting array or value.
5425
			case 0:
5426
				if($type == 'a')
5427
				{
5428
					$data = array();
5429
					$list = &$data;
5430
					$expected[] = $expectedLength;
5431
					$state = 2;
5432
					break;
5433
				}
5434
5435
				if($type != '}')
5436
				{
5437
					$data = $value;
5438
					$state = 1;
5439
					break;
5440
				}
5441
5442
				// Not in array.
5443
				return false;
5444
		}
5445
	}
5446
5447
	// Trailing data in input.
5448
	if(!empty($str))
5449
		return false;
5450
5451
	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...
5452
}
5453
5454
/**
5455
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
5456
 *
5457
 * @param string $str
5458
 * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use boolean|integer|double|string|null|array.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
5459
 */
5460 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...
5461
{
5462
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5463
	if (function_exists('mb_internal_encoding') &&
5464
		(((int) ini_get('mbstring.func_overload')) & 0x02))
5465
	{
5466
		$mbIntEnc = mb_internal_encoding();
5467
		mb_internal_encoding('ASCII');
5468
	}
5469
5470
	$out = _safe_unserialize($str);
5471
5472
	if (isset($mbIntEnc))
5473
		mb_internal_encoding($mbIntEnc);
5474
5475
	return $out;
5476
}
5477
5478
/**
5479
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
5480
5481
 * @param string $file The file/dir full path.
5482
 * @param int $value Not needed, added for legacy reasons.
5483
 * @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.
5484
 */
5485
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...
5486
{
5487
	// No file? no checks!
5488
	if (empty($file))
5489
		return false;
5490
5491
	// Already writable?
5492
	if (is_writable($file))
5493
		return true;
5494
5495
	// Do we have a file or a dir?
5496
	$isDir = is_dir($file);
5497
	$isWritable = false;
5498
5499
	// Set different modes.
5500
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
5501
5502
	foreach($chmodValues as $val)
5503
	{
5504
		// If it's writable, break out of the loop.
5505
		if (is_writable($file))
5506
		{
5507
			$isWritable = true;
5508
			break;
5509
		}
5510
5511
		else
5512
			@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...
5513
	}
5514
5515
	return $isWritable;
5516
}
5517
5518
/**
5519
 * Wrapper function for json_decode() with error handling.
5520
5521
 * @param string $json The string to decode.
5522
 * @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.
5523
 * @param bool $logIt To specify if the error will be logged if theres any.
5524
 * @return array Either an empty array or the decoded data as an array.
5525
 */
5526
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
5527
{
5528
	global $txt;
5529
5530
	// Come on...
5531
	if (empty($json) || !is_string($json))
5532
		return array();
5533
5534
	$returnArray = @json_decode($json, $returnAsArray);
5535
5536
	// PHP 5.3 so no json_last_error_msg()
5537
	switch(json_last_error())
5538
	{
5539
		case JSON_ERROR_NONE:
5540
			$jsonError = false;
5541
			break;
5542
		case JSON_ERROR_DEPTH:
5543
			$jsonError =  'JSON_ERROR_DEPTH';
5544
			break;
5545
		case JSON_ERROR_STATE_MISMATCH:
5546
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
5547
			break;
5548
		case JSON_ERROR_CTRL_CHAR:
5549
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
5550
			break;
5551
		case JSON_ERROR_SYNTAX:
5552
			$jsonError = 'JSON_ERROR_SYNTAX';
5553
			break;
5554
		case JSON_ERROR_UTF8:
5555
			$jsonError = 'JSON_ERROR_UTF8';
5556
			break;
5557
		default:
5558
			$jsonError = 'unknown';
5559
			break;
5560
	}
5561
5562
	// Something went wrong!
5563
	if (!empty($jsonError) && $logIt)
5564
	{
5565
		// Being a wrapper means we lost our smf_error_handler() privileges :(
5566
		$jsonDebug = debug_backtrace();
5567
		$jsonDebug = $jsonDebug[0];
5568
		loadLanguage('Errors');
5569
5570
		if (!empty($jsonDebug))
5571
			log_error($txt['json_'. $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
5572
5573
		else
5574
			log_error($txt['json_'. $jsonError], 'critical');
5575
5576
		// Everyone expects an array.
5577
		return array();
5578
	}
5579
5580
	return $returnArray;
5581
}
5582
5583
/**
5584
 * Check the given String if he is a valid IPv4 or IPv6
5585
 * return true or false
5586
 */
5587
function isValidIP($IPString)
5588
{
5589
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
5590
}
5591
5592
/**
5593
 * Outputs a response.
5594
 * It assumes the data is already a string.
5595
 * @param string $data The data to print
5596
 * @param string $type The content type. Defaults to Json.
5597
 * @return void
0 ignored issues
show
Documentation introduced by
Should the return type not be false|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
5598
 */
5599
function smf_serverResponse($data = '', $type = 'Content-Type: application/json')
5600
{
5601
	global $db_show_debug, $modSettings;
5602
5603
	// Defensive programming anyone?
5604
	if (empty($data))
5605
		return false;
5606
5607
	// Don't need extra stuff...
5608
	$db_show_debug = false;
5609
5610
	// Kill anything else.
5611
	ob_end_clean();
5612
5613
	if (!empty($modSettings['CompressedOutput']))
5614
		@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...
5615
5616
	else
5617
		ob_start();
5618
5619
	// Set the header.
5620
	header($type);
5621
5622
	// Echo!
5623
	echo $data;
5624
5625
	// Done.
5626
	obExit(false);
5627
}
5628
5629
/**
5630
 * Creates an optimized regex to match all known top level domains.
5631
 *
5632
 * The optimized regex is stored in $modSettings['tld_regex'].
5633
 *
5634
 * To update the stored version of the regex to use the latest list of valid TLDs from iana.org, set
5635
 * the $update parameter to true. Updating can take some time, based on network connectivity, so it
5636
 * should normally only be done by calling this function from a background or scheduled task.
5637
 *
5638
 * If $update is not true, but the regex is missing or invalid, the regex will be regenerated from a
5639
 * hard-coded list of TLDs. This regenerated regex will be overwritten on the next scheduled update.
5640
 *
5641
 * @param bool $update If true, fetch and process the latest offical list of TLDs from iana.org.
5642
 */
5643
function set_tld_regex($update = false)
5644
{
5645
	global $sourcedir, $smcFunc, $modSettings;
5646
	static $done = false;
5647
5648
	// If we don't need to do anything, don't
5649
	if (!$update && $done)
5650
		return;
5651
5652
	// Should we get a new copy of the official list of TLDs?
5653
	if ($update)
5654
	{
5655
		require_once($sourcedir . '/Subs-Package.php');
5656
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
5657
5658
		// If the Internet Assigned Numbers Authority can't be reached, the Internet is gone. We're probably running on a server hidden in a bunker deep underground to protect it from marauding bandits roaming on the surface. We don't want to waste precious electricity on pointlessly repeating background tasks, so we'll wait until the next regularly scheduled update to see if civilization has been restored.
5659
		if ($tlds === false)
5660
			$postapocalypticNightmare = true;
5661
	}
5662
	// If we aren't updating and the regex is valid, we're done
5663
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
5664
	{
5665
		$done = true;
5666
		return;
5667
	}
5668
5669
	// If we successfully got an update, process the list into an array
5670
	if (!empty($tlds))
5671
	{
5672
		// Clean $tlds and convert it to an array
5673
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line) {
5674
			$line = trim($line);
5675
			if (empty($line) || strpos($line, '#') !== false || strpos($line, ' ') !== false)
0 ignored issues
show
Coding Style introduced by
The if-else statement can be simplified to return !(empty($line) ||...$line, ' ') !== false);.
Loading history...
5676
				return false;
5677
			else
5678
				return true;
5679
		});
5680
5681
		// Convert Punycode to Unicode
5682
		$tlds = array_map(function ($input) {
5683
			$prefix = 'xn--';
5684
			$safe_char = 0xFFFC;
5685
			$base = 36;
5686
			$tmin = 1;
5687
			$tmax = 26;
5688
			$skew = 38;
5689
			$damp = 700;
5690
			$output_parts = array();
5691
5692
			$input = str_replace(strtoupper($prefix), $prefix, $input);
5693
5694
			$enco_parts = (array) explode('.', $input);
5695
5696
			foreach ($enco_parts as $encoded)
5697
			{
5698
				if (strpos($encoded,$prefix) !== 0 || strlen(trim(str_replace($prefix,'',$encoded))) == 0)
5699
				{
5700
					$output_parts[] = $encoded;
5701
					continue;
5702
				}
5703
5704
				$is_first = true;
5705
				$bias = 72;
5706
				$idx = 0;
5707
				$char = 0x80;
5708
				$decoded = array();
5709
				$output='';
5710
				$delim_pos = strrpos($encoded, '-');
5711
5712
				if ($delim_pos > strlen($prefix))
5713
				{
5714
					for ($k = strlen($prefix); $k < $delim_pos; ++$k)
5715
					{
5716
						$decoded[] = ord($encoded{$k});
5717
					}
5718
				}
5719
5720
				$deco_len = count($decoded);
5721
				$enco_len = strlen($encoded);
5722
5723
				for ($enco_idx = $delim_pos ? ($delim_pos + 1) : 0; $enco_idx < $enco_len; ++$deco_len)
5724
				{
5725
					for ($old_idx = $idx, $w = 1, $k = $base; 1 ; $k += $base)
5726
					{
5727
						$cp = ord($encoded{$enco_idx++});
5728
						$digit = ($cp - 48 < 10) ? $cp - 22 : (($cp - 65 < 26) ? $cp - 65 : (($cp - 97 < 26) ? $cp - 97 : $base));
5729
						$idx += $digit * $w;
5730
						$t = ($k <= $bias) ? $tmin : (($k >= $bias + $tmax) ? $tmax : ($k - $bias));
5731
5732
						if ($digit < $t)
5733
							break;
5734
5735
						$w = (int) ($w * ($base - $t));
5736
					}
5737
5738
					$delta = $idx - $old_idx;
5739
					$delta = intval($is_first ? ($delta / $damp) : ($delta / 2));
5740
					$delta += intval($delta / ($deco_len + 1));
5741
5742
					for ($k = 0; $delta > (($base - $tmin) * $tmax) / 2; $k += $base)
5743
						$delta = intval($delta / ($base - $tmin));
5744
5745
					$bias = intval($k + ($base - $tmin + 1) * $delta / ($delta + $skew));
5746
					$is_first = false;
5747
					$char += (int) ($idx / ($deco_len + 1));
5748
					$idx %= ($deco_len + 1);
5749
5750
					if ($deco_len > 0)
5751
					{
5752
						for ($i = $deco_len; $i > $idx; $i--)
5753
							$decoded[$i] = $decoded[($i - 1)];
5754
					}
5755
					$decoded[$idx++] = $char;
5756
				}
5757
5758
				foreach ($decoded as $k => $v)
5759
				{
5760
					// 7bit are transferred literally
5761
					if ($v < 128)
5762
						$output .= chr($v);
5763
5764
					// 2 bytes
5765
					elseif ($v < (1 << 11))
5766
						$output .= chr(192+($v >> 6)) . chr(128+($v & 63));
5767
5768
					// 3 bytes
5769
					elseif ($v < (1 << 16))
5770
						$output .= chr(224+($v >> 12)) . chr(128+(($v >> 6) & 63)) . chr(128+($v & 63));
5771
5772
					// 4 bytes
5773
					elseif ($v < (1 << 21))
5774
						$output .= chr(240+($v >> 18)) . chr(128+(($v >> 12) & 63)) . chr(128+(($v >> 6) & 63)) . chr(128+($v & 63));
5775
5776
					//  'Conversion from UCS-4 to UTF-8 failed: malformed input at byte '.$k
5777
					else
5778
						$output .= $safe_char;
5779
				}
5780
5781
				$output_parts[] = $output;
5782
			}
5783
5784
			return implode('.', $output_parts);
5785
		}, $tlds);
5786
	}
5787
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
5788
	else
5789
	{
5790
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz', 'cat',
5791
			'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post', 'pro', 'tel',
5792
			'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq',
5793
			'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh',
5794
			'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc',
5795
			'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv',
5796
			'cx', 'cy', 'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', 'eh',
5797
			'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge',
5798
			'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw',
5799
			'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq',
5800
			'ir', 'is', 'it', 'ja', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn',
5801
			'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu',
5802
			'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp',
5803
			'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf',
5804
			'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph',
5805
			'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru',
5806
			'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
5807
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th',
5808
			'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug',
5809
			'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye',
5810
			'yt', 'yu', 'za', 'zm', 'zw');
5811
5812
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
5813
		$schedule_update = empty($postapocalypticNightmare);
5814
	}
5815
5816
	// Get an optimized regex to match all the TLDs
5817
	$tld_regex = build_regex($tlds);
5818
5819
	// Remember the new regex in $modSettings
5820
	updateSettings(array('tld_regex' => $tld_regex));
5821
5822
	// Schedule a background update if we need one
5823
	if (!empty($schedule_update))
5824
	{
5825
		$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
5826
			array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
5827
			array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
5828
		);
5829
	}
5830
5831
	// Redundant repetition is redundant
5832
	$done = true;
5833
}
5834
5835
/**
5836
 * Creates optimized regular expressions from an array of strings.
5837
 *
5838
 * An optimized regex built using this function will be much faster than a simple regex built using
5839
 * `implode('|', $strings)` --- anywhere from several times to several orders of magnitude faster.
5840
 *
5841
 * However, the time required to build the optimized regex is approximately equal to the time it
5842
 * takes to execute the simple regex. Therefore, it is only worth calling this function if the
5843
 * resulting regex will be used more than once.
5844
 *
5845
 * Because PHP places an upper limit on the allowed length of a regex, very large arrays of $strings
5846
 * may not fit in a single regex. Normally, the excess strings will simply be dropped. However, if
5847
 * the $returnArray parameter is set to true, this function will build as many regexes as necessary
5848
 * to accomodate everything in $strings and return them in an array. You will need to iterate
5849
 * through all elements of the returned array in order to test all possible matches.
5850
 *
5851
 * @param array $strings An array of strings to make a regex for.
5852
 * @param string $delim An optional delimiter character to pass to preg_quote().
0 ignored issues
show
Documentation introduced by
Should the type for parameter $delim not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
5853
 * @param string $returnArray If true, returns an array of regexes.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $returnArray not be false|string?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
5854
 * @return string|array One or more regular expressions to match any of the input strings.
5855
 */
5856
function build_regex($strings, $delim = null, $returnArray = false)
5857
{
5858
	global $smcFunc;
5859
5860
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
5861
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
5862
	{
5863
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
5864
		{
5865
			$current_encoding = mb_internal_encoding();
5866
			mb_internal_encoding($string_encoding);
5867
		}
5868
5869
		$strlen = 'mb_strlen';
5870
		$substr = 'mb_substr';
5871
	}
5872
	else
5873
	{
5874
		$strlen = $smcFunc['strlen'];
5875
		$substr = $smcFunc['substr'];
5876
	}
5877
5878
	// This recursive function creates the index array from the strings
5879
	$add_string_to_index = function ($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
5880
	{
5881
		static $depth = 0;
5882
		$depth++;
5883
5884
		$first = $substr($string, 0, 1);
5885
5886
		if (empty($index[$first]))
5887
			$index[$first] = array();
5888
5889
		if ($strlen($string) > 1)
5890
		{
5891
			// Sanity check on recursion
5892
			if ($depth > 99)
5893
				$index[$first][$substr($string, 1)] = '';
5894
5895
			else
5896
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
5897
		}
5898
		else
5899
			$index[$first][''] = '';
5900
5901
		$depth--;
5902
		return $index;
5903
	};
5904
5905
	// This recursive function turns the index array into a regular expression
5906
	$index_to_regex = function (&$index, $delim) use (&$strlen, &$index_to_regex)
5907
	{
5908
		static $depth = 0;
5909
		$depth++;
5910
5911
		// Absolute max length for a regex is 32768, but we might need wiggle room
5912
		$max_length = 30000;
5913
5914
		$regex = array();
5915
		$length = 0;
5916
5917
		foreach ($index as $key => $value)
5918
		{
5919
			$key_regex = preg_quote($key, $delim);
5920
			$new_key = $key;
5921
5922
			if (empty($value))
5923
				$sub_regex = '';
5924
			else
5925
			{
5926
				$sub_regex = $index_to_regex($value, $delim);
5927
5928
				if (count(array_keys($value)) == 1)
5929
				{
5930
					$new_key_array = explode('(?'.'>', $sub_regex);
5931
					$new_key .= $new_key_array[0];
5932
				}
5933
				else
5934
					$sub_regex = '(?'.'>' . $sub_regex . ')';
5935
			}
5936
5937
			if ($depth > 1)
5938
				$regex[$new_key] = $key_regex . $sub_regex;
5939
			else
5940
			{
5941
				if (($length += strlen($key_regex) + 1) < $max_length || empty($regex))
5942
				{
5943
					$regex[$new_key] = $key_regex . $sub_regex;
5944
					unset($index[$key]);
5945
				}
5946
				else
5947
					break;
5948
			}
5949
		}
5950
5951
		// Sort by key length and then alphabetically
5952
		uksort($regex, function($k1, $k2) use (&$strlen) {
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $k1. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
Comprehensibility introduced by
Avoid variables with short names like $k2. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
5953
			$l1 = $strlen($k1);
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $l1. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
5954
			$l2 = $strlen($k2);
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $l2. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
5955
5956
			if ($l1 == $l2)
5957
				return strcmp($k1, $k2) > 0 ? 1 : -1;
5958
			else
5959
				return $l1 > $l2 ? -1 : 1;
5960
		});
5961
5962
		$depth--;
5963
		return implode('|', $regex);
5964
	};
5965
5966
	// Now that the functions are defined, let's do this thing
5967
	$index = array();
5968
	$regex = '';
0 ignored issues
show
Unused Code introduced by
$regex 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...
5969
5970
	foreach ($strings as $string)
5971
		$index = $add_string_to_index($string, $index);
5972
5973
	if ($returnArray === true)
5974
	{
5975
		$regex = array();
5976
		while (!empty($index))
5977
			$regex[] = '(?'.'>' . $index_to_regex($index, $delim) . ')';
5978
	}
5979
	else
5980
		$regex = '(?'.'>' . $index_to_regex($index, $delim) . ')';
5981
5982
	// Restore PHP's internal character encoding to whatever it was originally
5983
	if (!empty($current_encoding))
5984
		mb_internal_encoding($current_encoding);
5985
5986
	return $regex;
5987
}
5988
5989
?>