Completed
Push — release-2.1 ( c5fac3...040693 )
by Mert
39:33
created

Subs.php ➔ text2words()   C

Complexity

Conditions 11
Paths 10

Size

Total Lines 42
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 11
eloc 22
nc 10
nop 3
dl 0
loc 42
rs 5.2653
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
758
759
		// Day-of-year is one less and same year, or it's the first of the year and that's the last of the year...
760
		if ($modSettings['todayMod'] == '2' && (($then['yday'] == $now['yday'] - 1 && $then['year'] == $now['year']) || ($now['yday'] == 0 && $then['year'] == $now['year'] - 1) && $then['mon'] == 12 && $then['mday'] == 31))
761
			return $txt['yesterday'] . timeformat($log_time, $today_fmt, $offset_type);
0 ignored issues
show
Documentation introduced by
$today_fmt is of type string, but the function expects a boolean.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

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

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

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

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

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

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

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

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

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

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

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

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

Loading history...
1208
						{
1209
							// Do PHP code coloring?
1210
							if ($php_parts[$php_i] != '&lt;?php')
1211
								continue;
1212
1213
							$php_string = '';
1214
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1215
							{
1216
								$php_string .= $php_parts[$php_i];
1217
								$php_parts[$php_i++] = '';
1218
							}
1219
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1220
						}
1221
1222
						// Fix the PHP code stuff...
1223
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1224
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1225
1226
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1227
						if ($context['browser']['is_opera'])
1228
							$data[0] .= '&nbsp;';
1229
					}
1230
				},
1231
				'block_level' => true,
1232
			),
1233
			array(
1234
				'tag' => 'color',
1235
				'type' => 'unparsed_equals',
1236
				'test' => '(#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,20}|rgb\((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s?,\s?){2}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\))\]',
1237
				'before' => '<span style="color: $1;" class="bbc_color">',
1238
				'after' => '</span>',
1239
			),
1240
			array(
1241
				'tag' => 'email',
1242
				'type' => 'unparsed_content',
1243
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1244
				// @todo Should this respect guest_hideContacts?
1245
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1246
				{
1247
					$data = strtr($data, array('<br>' => ''));
1248
				},
1249
			),
1250
			array(
1251
				'tag' => 'email',
1252
				'type' => 'unparsed_equals',
1253
				'before' => '<a href="mailto:$1" class="bbc_email">',
1254
				'after' => '</a>',
1255
				// @todo Should this respect guest_hideContacts?
1256
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1257
				'disabled_after' => ' ($1)',
1258
			),
1259
			array(
1260
				'tag' => 'flash',
1261
				'type' => 'unparsed_commas_content',
1262
				'test' => '\d+,\d+\]',
1263
				'content' => '<embed type="application/x-shockwave-flash" src="$1" width="$2" height="$3" play="true" loop="true" quality="high" AllowScriptAccess="never">',
1264
				'validate' => function (&$tag, &$data, $disabled)
1265
				{
1266
					if (isset($disabled['url']))
1267
						$tag['content'] = '$1';
1268
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1269
					if (empty($scheme))
1270
						$data[0] = '//' . ltrim($data[0], ':/');
1271
				},
1272
				'disabled_content' => '<a href="$1" target="_blank" class="new_win">$1</a>',
1273
			),
1274
			array(
1275
				'tag' => 'font',
1276
				'type' => 'unparsed_equals',
1277
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1278
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1279
				'after' => '</span>',
1280
			),
1281
			array(
1282
				'tag' => 'html',
1283
				'type' => 'unparsed_content',
1284
				'content' => '<div>$1</div>',
1285
				'block_level' => true,
1286
				'disabled_content' => '$1',
1287
			),
1288
			array(
1289
				'tag' => 'hr',
1290
				'type' => 'closed',
1291
				'content' => '<hr>',
1292
				'block_level' => true,
1293
			),
1294
			array(
1295
				'tag' => 'i',
1296
				'before' => '<i>',
1297
				'after' => '</i>',
1298
			),
1299
			array(
1300
				'tag' => 'img',
1301
				'type' => 'unparsed_content',
1302
				'parameters' => array(
1303
					'alt' => array('optional' => true),
1304
					'title' => array('optional' => true),
1305
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1306
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1307
				),
1308
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized">',
1309 View Code Duplication
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1310
				{
1311
					global $image_proxy_enabled, $image_proxy_secret, $boardurl;
1312
1313
					$data = strtr($data, array('<br>' => ''));
1314
					$scheme = parse_url($data, PHP_URL_SCHEME);
1315
					if ($image_proxy_enabled)
1316
					{
1317
						if (empty($scheme))
1318
							$data = 'http://' . ltrim($data, ':/');
1319
1320
						if ($scheme != 'https')
1321
							$data = $boardurl . '/proxy.php?request=' . urlencode($data) . '&hash=' . md5($data . $image_proxy_secret);
1322
					}
1323
					elseif (empty($scheme))
1324
						$data = '//' . ltrim($data, ':/');
1325
				},
1326
				'disabled_content' => '($1)',
1327
			),
1328
			array(
1329
				'tag' => 'img',
1330
				'type' => 'unparsed_content',
1331
				'content' => '<img src="$1" alt="" class="bbc_img">',
1332 View Code Duplication
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1333
				{
1334
					global $image_proxy_enabled, $image_proxy_secret, $boardurl;
1335
1336
					$data = strtr($data, array('<br>' => ''));
1337
					$scheme = parse_url($data, PHP_URL_SCHEME);
1338
					if ($image_proxy_enabled)
1339
					{
1340
						if (empty($scheme))
1341
							$data = 'http://' . ltrim($data, ':/');
1342
1343
						if ($scheme != 'https')
1344
							$data = $boardurl . '/proxy.php?request=' . urlencode($data) . '&hash=' . md5($data . $image_proxy_secret);
1345
					}
1346
					elseif (empty($scheme))
1347
						$data = '//' . ltrim($data, ':/');
1348
				},
1349
				'disabled_content' => '($1)',
1350
			),
1351
			array(
1352
				'tag' => 'iurl',
1353
				'type' => 'unparsed_content',
1354
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1355 View Code Duplication
				'validate' => function (&$tag, &$data, $disabled)
0 ignored issues
show
Unused Code introduced by
The parameter $disabled is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1614
				{
1615
					$data = strtr($data, array('<br>' => ''));
1616
					$scheme = parse_url($data, PHP_URL_SCHEME);
1617
					if (empty($scheme))
1618
						$data = '//' . ltrim($data, ':/');
1619
				},
1620
			),
1621
			array(
1622
				'tag' => 'url',
1623
				'type' => 'unparsed_equals',
1624
				'quoted' => 'optional',
1625
				'before' => '<a href="$1" class="bbc_link" target="_blank">',
1626
				'after' => '</a>',
1627
				'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...
1628
				{
1629
					$scheme = parse_url($data, PHP_URL_SCHEME);
1630
					if (empty($scheme))
1631
						$data = '//' . ltrim($data, ':/');
1632
				},
1633
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1634
				'disabled_after' => ' ($1)',
1635
			),
1636
		);
1637
1638
		// Inside these tags autolink is not recommendable.
1639
		$no_autolink_tags = array(
1640
			'url',
1641
			'iurl',
1642
			'email',
1643
		);
1644
1645
		// Let mods add new BBC without hassle.
1646
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
1647
1648
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
1649
		if ($message === false)
1650
		{
1651
			if (isset($temp_bbc))
1652
				$bbc_codes = $temp_bbc;
1653
			usort($codes, function ($a, $b) {
1654
				return strcmp($a['tag'], $b['tag']);
1655
			});
1656
			return $codes;
1657
		}
1658
1659
		// So the parser won't skip them.
1660
		$itemcodes = array(
1661
			'*' => 'disc',
1662
			'@' => 'disc',
1663
			'+' => 'square',
1664
			'x' => 'square',
1665
			'#' => 'square',
1666
			'o' => 'circle',
1667
			'O' => 'circle',
1668
			'0' => 'circle',
1669
		);
1670
		if (!isset($disabled['li']) && !isset($disabled['list']))
1671
		{
1672
			foreach ($itemcodes as $c => $dummy)
1673
				$bbc_codes[$c] = array();
1674
		}
1675
1676
		// Shhhh!
1677
		if (!isset($disabled['color']))
1678
		{
1679
			$codes[] = array(
1680
				'tag' => 'chrissy',
1681
				'before' => '<span style="color: #cc0099;">',
1682
				'after' => ' :-*</span>',
1683
			);
1684
			$codes[] = array(
1685
				'tag' => 'kissy',
1686
				'before' => '<span style="color: #cc0099;">',
1687
				'after' => ' :-*</span>',
1688
			);
1689
		}
1690
1691
		foreach ($codes as $code)
1692
		{
1693
			// Make it easier to process parameters later
1694
			if (!empty($code['parameters']))
1695
				ksort($code['parameters'], SORT_STRING);
1696
1697
			// If we are not doing every tag only do ones we are interested in.
1698
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
1699
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
1700
		}
1701
		$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...
1702
	}
1703
1704
	// Shall we take the time to cache this?
1705
	if ($cache_id != '' && !empty($modSettings['cache_enable']) && (($modSettings['cache_enable'] >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
1706
	{
1707
		// It's likely this will change if the message is modified.
1708
		$cache_key = 'parse:' . $cache_id . '-' . md5(md5($message) . '-' . $smileys . (empty($disabled) ? '' : implode(',', array_keys($disabled))) . json_encode($context['browser']) . $txt['lang_locale'] . $user_info['time_offset'] . $user_info['time_format']);
1709
1710
		if (($temp = cache_get_data($cache_key, 240)) != null)
1711
			return $temp;
1712
1713
		$cache_t = microtime();
1714
	}
1715
1716
	if ($smileys === 'print')
1717
	{
1718
		// [glow], [shadow], and [move] can't really be printed.
1719
		$disabled['glow'] = true;
1720
		$disabled['shadow'] = true;
1721
		$disabled['move'] = true;
1722
1723
		// Colors can't well be displayed... supposed to be black and white.
1724
		$disabled['color'] = true;
1725
		$disabled['black'] = true;
1726
		$disabled['blue'] = true;
1727
		$disabled['white'] = true;
1728
		$disabled['red'] = true;
1729
		$disabled['green'] = true;
1730
		$disabled['me'] = true;
1731
1732
		// Color coding doesn't make sense.
1733
		$disabled['php'] = true;
1734
1735
		// Links are useless on paper... just show the link.
1736
		$disabled['ftp'] = true;
1737
		$disabled['url'] = true;
1738
		$disabled['iurl'] = true;
1739
		$disabled['email'] = true;
1740
		$disabled['flash'] = true;
1741
1742
		// @todo Change maybe?
1743
		if (!isset($_GET['images']))
1744
			$disabled['img'] = true;
1745
1746
		// @todo Interface/setting to add more?
1747
	}
1748
1749
	$open_tags = array();
1750
	$message = strtr($message, array("\n" => '<br>'));
1751
1752
	$alltags = array();
1753
	foreach ($bbc_codes as $section) {
1754
		foreach ($section as $code) {
1755
			$alltags[] = $code['tag'];
1756
		}
1757
	}
1758
	$alltags_regex = '\b' . implode("\b|\b", array_unique($alltags)) . '\b';
1759
1760
	$pos = -1;
1761
	while ($pos !== false)
1762
	{
1763
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
1764
		preg_match('~\[/?(?=' . $alltags_regex . ')~', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
1765
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
1766
1767
		// Failsafe.
1768
		if ($pos === false || $last_pos > $pos)
1769
			$pos = strlen($message) + 1;
1770
1771
		// Can't have a one letter smiley, URL, or email! (sorry.)
1772
		if ($last_pos < $pos - 1)
1773
		{
1774
			// Make sure the $last_pos is not negative.
1775
			$last_pos = max($last_pos, 0);
1776
1777
			// Pick a block of data to do some raw fixing on.
1778
			$data = substr($message, $last_pos, $pos - $last_pos);
1779
1780
			// Take care of some HTML!
1781
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
1782
			{
1783
				$data = preg_replace('~&lt;a\s+href=((?:&quot;)?)((?:https?://|ftps?://|mailto:)\S+?)\\1&gt;~i', '[url=&quot;$2&quot;]', $data);
1784
				$data = preg_replace('~&lt;/a&gt;~i', '[/url]', $data);
1785
1786
				// <br> should be empty.
1787
				$empty_tags = array('br', 'hr');
1788
				foreach ($empty_tags as $tag)
1789
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '[' . $tag . ' /]', $data);
1790
1791
				// b, u, i, s, pre... basic tags.
1792
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote');
1793
				foreach ($closable_tags as $tag)
1794
				{
1795
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
1796
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
1797
1798
					if ($diff > 0)
1799
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
1800
				}
1801
1802
				// Do <img ...> - with security... action= -> action-.
1803
				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);
1804
				if (!empty($matches[0]))
1805
				{
1806
					$replaces = array();
1807
					foreach ($matches[2] as $match => $imgtag)
1808
					{
1809
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
1810
1811
						// Remove action= from the URL - no funny business, now.
1812
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
1813
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
1814
1815
						// Check if the image is larger than allowed.
1816
						if (!empty($modSettings['max_image_width']) && !empty($modSettings['max_image_height']))
1817
						{
1818
							list ($width, $height) = url_image_size($imgtag);
1819
1820 View Code Duplication
							if (!empty($modSettings['max_image_width']) && $width > $modSettings['max_image_width'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1821
							{
1822
								$height = (int) (($modSettings['max_image_width'] * $height) / $width);
1823
								$width = $modSettings['max_image_width'];
1824
							}
1825
1826 View Code Duplication
							if (!empty($modSettings['max_image_height']) && $height > $modSettings['max_image_height'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
1827
							{
1828
								$width = (int) (($modSettings['max_image_height'] * $width) / $height);
1829
								$height = $modSettings['max_image_height'];
1830
							}
1831
1832
							// Set the new image tag.
1833
							$replaces[$matches[0][$match]] = '[img width=' . $width . ' height=' . $height . $alt . ']' . $imgtag . '[/img]';
1834
						}
1835
						else
1836
							$replaces[$matches[0][$match]] = '[img' . $alt . ']' . $imgtag . '[/img]';
1837
					}
1838
1839
					$data = strtr($data, $replaces);
1840
				}
1841
			}
1842
1843
			if (!empty($modSettings['autoLinkUrls']))
1844
			{
1845
				// Are we inside tags that should be auto linked?
1846
				$no_autolink_area = false;
1847
				if (!empty($open_tags))
1848
				{
1849
					foreach ($open_tags as $open_tag)
1850
						if (in_array($open_tag['tag'], $no_autolink_tags))
1851
							$no_autolink_area = true;
1852
				}
1853
1854
				// Don't go backwards.
1855
				// @todo Don't think is the real solution....
1856
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
1857
				if ($pos < $lastAutoPos)
1858
					$no_autolink_area = true;
1859
				$lastAutoPos = $pos;
1860
1861
				if (!$no_autolink_area)
1862
				{
1863
					// Parse any URLs
1864
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
1865
					{
1866
						$url_regex = '
1867
						(?:
1868
							# IRIs with a scheme (or at least an opening "//")
1869
							(?:
1870
								# URI scheme (or lack thereof for schemeless URLs)
1871
								(?:
1872
									# URL scheme and colon
1873
									\b[a-z][\w\-]+:
1874
									| # or
1875
									# A boundary followed by two slashes for schemeless URLs
1876
									(?<=^|\W)(?=//)
1877
								)
1878
1879
								# IRI "authority" chunk
1880
								(?:
1881
									# 2 slashes for IRIs with an "authority"
1882
									//
1883
									# then a domain name
1884
									(?:
1885
										# Either the reserved "localhost" domain name
1886
										localhost
1887
										| # or
1888
										# a run of Unicode domain name characters and a dot
1889
										[\p{L}\p{M}\p{N}\-.:@]+\.
1890
										# and then a TLD valid in the DNS or the reserved "local" TLD
1891
										(?:'. $modSettings['tld_regex'] .'|local)
1892
									)
1893
									# followed by a non-domain character or end of line
1894
									(?=[^\p{L}\p{N}\-.]|$)
1895
1896
									| # Or, if there is no "authority" per se (e.g. mailto: URLs) ...
1897
1898
									# a run of IRI characters
1899
									[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]
1900
									# and then a dot and a closing IRI label
1901
									\.[\p{L}\p{M}\p{N}\-]+
1902
								)
1903
							)
1904
1905
							| # or
1906
1907
							# Naked domains (e.g. "example.com" in "Go to example.com for an example.")
1908
							(?:
1909
								# Preceded by start of line or a non-domain character
1910
								(?<=^|[^\p{L}\p{M}\p{N}\-:@])
1911
1912
								# A run of Unicode domain name characters (excluding [:@])
1913
								[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]
1914
								# and then a dot and a valid TLD
1915
								\.' . $modSettings['tld_regex'] . '
1916
1917
								# Followed by either:
1918
								(?=
1919
									# end of line or a non-domain character (excluding [.:@])
1920
									$|[^\p{L}\p{N}\-]
1921
									| # or
1922
									# a dot followed by end of line or a non-domain character (excluding [.:@])
1923
									\.(?=$|[^\p{L}\p{N}\-])
1924
								)
1925
							)
1926
						)
1927
1928
						# IRI path, query, and fragment (if present)
1929
						(?:
1930
							# If any of these parts exist, must start with a single /
1931
							/
1932
1933
							# And then optionally:
1934
							(?:
1935
								# One or more of:
1936
								(?:
1937
									# a run of non-space, non-()<>
1938
									[^\s()<>]+
1939
									| # or
1940
									# balanced parens, up to 2 levels
1941
									\(([^\s()<>]+|(\([^\s()<>]+\)))*\)
1942
								)+
1943
1944
								# End with:
1945
								(?:
1946
									# balanced parens, up to 2 levels
1947
									\(([^\s()<>]+|(\([^\s()<>]+\)))*\)
1948
									| # or
1949
									# not a space or one of these punct char
1950
									[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]
1951
									| # or
1952
									# a trailing slash (but not two in a row)
1953
									(?<!/)/
1954
								)
1955
							)?
1956
						)?
1957
						';
1958
1959
						$data = preg_replace_callback('~' . $url_regex . '~xi' . ($context['utf8'] ? 'u' : ''), function ($matches) {
1960
							$url = array_shift($matches);
1961
1962
							$scheme = parse_url($url, PHP_URL_SCHEME);
1963
1964
							if ($scheme == 'mailto')
1965
							{
1966
								$email_address = str_replace('mailto:', '', $url);
1967
								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...
1968
									return '[email=' . $email_address . ']' . $url . '[/email]';
1969
								else
1970
									return $url;
1971
							}
1972
1973
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
1974
							if (empty($scheme))
1975
								$fullUrl = '//' . ltrim($url, ':/');
1976
							else
1977
								$fullUrl = $url;
1978
1979
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
1980
						}, $data);
1981
					}
1982
1983
					// Next, emails...
1984
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false)
1985
					{
1986
						$email_regex = '
1987
						# Preceded by a non-domain character or start of line
1988
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
1989
1990
						# An email address
1991
						[\p{L}\p{M}\p{N}_\-.]{1,80}
1992
						@
1993
						[\p{L}\p{M}\p{N}\-.]+
1994
						\.
1995
						'. $modSettings['tld_regex'] . '
1996
1997
						# Followed by either:
1998
						(?=
1999
							# end of line or a non-domain character (excluding the dot)
2000
							$|[^\p{L}\p{M}\p{N}\-]
2001
							| # or
2002
							# a dot followed by end of line or a non-domain character
2003
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2004
						)';
2005
2006
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2007
					}
2008
				}
2009
			}
2010
2011
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2012
2013
			// If it wasn't changed, no copying or other boring stuff has to happen!
2014
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2015
			{
2016
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2017
2018
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2019
				$old_pos = strlen($data) + $last_pos;
2020
				$pos = strpos($message, '[', $last_pos);
2021
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2022
			}
2023
		}
2024
2025
		// Are we there yet?  Are we there yet?
2026
		if ($pos >= strlen($message) - 1)
2027
			break;
2028
2029
		$tags = strtolower($message[$pos + 1]);
2030
2031
		if ($tags == '/' && !empty($open_tags))
2032
		{
2033
			$pos2 = strpos($message, ']', $pos + 1);
2034
			if ($pos2 == $pos + 2)
2035
				continue;
2036
2037
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2038
2039
			$to_close = array();
2040
			$block_level = null;
2041
2042
			do
2043
			{
2044
				$tag = array_pop($open_tags);
2045
				if (!$tag)
2046
					break;
2047
2048
				if (!empty($tag['block_level']))
2049
				{
2050
					// Only find out if we need to.
2051
					if ($block_level === false)
2052
					{
2053
						array_push($open_tags, $tag);
2054
						break;
2055
					}
2056
2057
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2058 View Code Duplication
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2059
					{
2060
						foreach ($bbc_codes[$look_for[0]] as $temp)
2061
							if ($temp['tag'] == $look_for)
2062
							{
2063
								$block_level = !empty($temp['block_level']);
2064
								break;
2065
							}
2066
					}
2067
2068
					if ($block_level !== true)
2069
					{
2070
						$block_level = false;
2071
						array_push($open_tags, $tag);
2072
						break;
2073
					}
2074
				}
2075
2076
				$to_close[] = $tag;
2077
			}
2078
			while ($tag['tag'] != $look_for);
2079
2080
			// Did we just eat through everything and not find it?
2081
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2082
			{
2083
				$open_tags = $to_close;
2084
				continue;
2085
			}
2086
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2087
			{
2088 View Code Duplication
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2089
				{
2090
					foreach ($bbc_codes[$look_for[0]] as $temp)
2091
						if ($temp['tag'] == $look_for)
2092
						{
2093
							$block_level = !empty($temp['block_level']);
2094
							break;
2095
						}
2096
				}
2097
2098
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2099
				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...
2100
				{
2101
					foreach ($to_close as $tag)
2102
						array_push($open_tags, $tag);
2103
					continue;
2104
				}
2105
			}
2106
2107
			foreach ($to_close as $tag)
2108
			{
2109
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2110
				$pos += strlen($tag['after']) + 2;
2111
				$pos2 = $pos - 1;
2112
2113
				// See the comment at the end of the big loop - just eating whitespace ;).
2114 View Code Duplication
				if (!empty($tag['block_level']) && substr($message, $pos, 4) == '<br>')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

Loading history...
2117
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2118
			}
2119
2120
			if (!empty($to_close))
2121
			{
2122
				$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...
2123
				$pos--;
2124
			}
2125
2126
			continue;
2127
		}
2128
2129
		// No tags for this character, so just keep going (fastest possible course.)
2130
		if (!isset($bbc_codes[$tags]))
2131
			continue;
2132
2133
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2134
		$tag = null;
2135
		foreach ($bbc_codes[$tags] as $possible)
2136
		{
2137
			$pt_strlen = strlen($possible['tag']);
2138
2139
			// Not a match?
2140
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2141
				continue;
2142
2143
			$next_c = $message[$pos + 1 + $pt_strlen];
2144
2145
			// A test validation?
2146
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2147
				continue;
2148
			// Do we want parameters?
2149
			elseif (!empty($possible['parameters']))
2150
			{
2151
				if ($next_c != ' ')
2152
					continue;
2153
			}
2154
			elseif (isset($possible['type']))
2155
			{
2156
				// Do we need an equal sign?
2157
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2158
					continue;
2159
				// Maybe we just want a /...
2160
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2161
					continue;
2162
				// An immediate ]?
2163
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2164
					continue;
2165
			}
2166
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2167
			elseif ($next_c != ']')
2168
				continue;
2169
2170
			// Check allowed tree?
2171
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2172
				continue;
2173
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2174
				continue;
2175
			// If this is in the list of disallowed child tags, don't parse it.
2176
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2177
				continue;
2178
2179
			$pos1 = $pos + 1 + $pt_strlen + 1;
2180
2181
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2182
			if ($possible['tag'] == 'quote')
2183
			{
2184
				// Start with standard
2185
				$quote_alt = false;
2186
				foreach ($open_tags as $open_quote)
2187
				{
2188
					// Every parent quote this quote has flips the styling
2189
					if ($open_quote['tag'] == 'quote')
2190
						$quote_alt = !$quote_alt;
2191
				}
2192
				// Add a class to the quote to style alternating blockquotes
2193
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2194
			}
2195
2196
			// This is long, but it makes things much easier and cleaner.
2197
			if (!empty($possible['parameters']))
2198
			{
2199
				// Build a regular expression for each parameter for the current tag.
2200
				$preg = array();
2201
				foreach ($possible['parameters'] as $p => $info)
2202
					$preg[] = '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
2203
2204
				// Extract the string that potentially holds our parameters.
2205
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
2206
				$blobs = preg_split('~\]~i', $blob[1]);
2207
2208
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
2209
2210
				// Progressively append more blobs until we find our parameters or run out of blobs
2211
				$blob_counter = 1;
2212
				while ($blob_counter <= count($blobs))
2213
				{
2214
2215
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
2216
2217
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2218
					sort($given_params, SORT_STRING);
2219
2220
					$match = preg_match('~^' . implode('', $preg) . '$~i', implode(' ', $given_params), $matches) !== 0;
2221
2222
					if ($match)
2223
						$blob_counter = count($blobs) + 1;
2224
				}
2225
2226
				// Didn't match our parameter list, try the next possible.
2227
				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...
2228
					continue;
2229
2230
				$params = array();
2231
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2232
				{
2233
					$key = strtok(ltrim($matches[$i]), '=');
2234
					if (isset($possible['parameters'][$key]['value']))
2235
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2236
					elseif (isset($possible['parameters'][$key]['validate']))
2237
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2238
					else
2239
						$params['{' . $key . '}'] = $matches[$i + 1];
2240
2241
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2242
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2243
				}
2244
2245
				foreach ($possible['parameters'] as $p => $info)
2246
				{
2247
					if (!isset($params['{' . $p . '}']))
2248
						$params['{' . $p . '}'] = '';
2249
				}
2250
2251
				$tag = $possible;
2252
2253
				// Put the parameters into the string.
2254
				if (isset($tag['before']))
2255
					$tag['before'] = strtr($tag['before'], $params);
2256
				if (isset($tag['after']))
2257
					$tag['after'] = strtr($tag['after'], $params);
2258
				if (isset($tag['content']))
2259
					$tag['content'] = strtr($tag['content'], $params);
2260
2261
				$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...
2262
			}
2263
			else
2264
			{
2265
				$tag = $possible;
2266
				$params = array();
2267
			}
2268
			break;
2269
		}
2270
2271
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2272
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2273
		{
2274
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2275
				continue;
2276
2277
			$tag = $itemcodes[$message[$pos + 1]];
2278
2279
			// First let's set up the tree: it needs to be in a list, or after an li.
2280
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2281
			{
2282
				$open_tags[] = array(
2283
					'tag' => 'list',
2284
					'after' => '</ul>',
2285
					'block_level' => true,
2286
					'require_children' => array('li'),
2287
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2288
				);
2289
				$code = '<ul class="bbc_list">';
2290
			}
2291
			// We're in a list item already: another itemcode?  Close it first.
2292
			elseif ($inside['tag'] == 'li')
2293
			{
2294
				array_pop($open_tags);
2295
				$code = '</li>';
2296
			}
2297
			else
2298
				$code = '';
2299
2300
			// Now we open a new tag.
2301
			$open_tags[] = array(
2302
				'tag' => 'li',
2303
				'after' => '</li>',
2304
				'trim' => 'outside',
2305
				'block_level' => true,
2306
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2307
			);
2308
2309
			// First, open the tag...
2310
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2311
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2312
			$pos += strlen($code) - 1 + 2;
2313
2314
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2315
			$pos2 = strpos($message, '<br>', $pos);
2316
			$pos3 = strpos($message, '[/', $pos);
2317
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2318
			{
2319
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2320
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2321
2322
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2323
			}
2324
			// Tell the [list] that it needs to close specially.
2325
			else
2326
			{
2327
				// Move the li over, because we're not sure what we'll hit.
2328
				$open_tags[count($open_tags) - 1]['after'] = '';
2329
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2330
			}
2331
2332
			continue;
2333
		}
2334
2335
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2336
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2337
		{
2338
			array_pop($open_tags);
2339
2340
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2341
			$pos += strlen($inside['after']) - 1 + 2;
2342
		}
2343
2344
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2345
		if ($tag === null)
2346
			continue;
2347
2348
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2349
		if (isset($inside['disallow_children']))
2350
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2351
2352
		// Is this tag disabled?
2353
		if (isset($disabled[$tag['tag']]))
2354
		{
2355
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2356
			{
2357
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2358
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2359
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2360
			}
2361
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2362
			{
2363
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2364
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2365
			}
2366
			else
2367
				$tag['content'] = $tag['disabled_content'];
2368
		}
2369
2370
		// we use this a lot
2371
		$tag_strlen = strlen($tag['tag']);
2372
2373
		// The only special case is 'html', which doesn't need to close things.
2374
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2375
		{
2376
			$n = count($open_tags) - 1;
2377
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2378
				$n--;
2379
2380
			// Close all the non block level tags so this tag isn't surrounded by them.
2381
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2382
			{
2383
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2384
				$ot_strlen = strlen($open_tags[$i]['after']);
2385
				$pos += $ot_strlen + 2;
2386
				$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...
2387
2388
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2389 View Code Duplication
				if (!empty($open_tags[$i]['block_level']) && substr($message, $pos, 4) == '<br>')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

Loading history...
2392
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2393
2394
				array_pop($open_tags);
2395
			}
2396
		}
2397
2398
		// No type means 'parsed_content'.
2399
		if (!isset($tag['type']))
2400
		{
2401
			// @todo Check for end tag first, so people can say "I like that [i] tag"?
2402
			$open_tags[] = $tag;
2403
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2404
			$pos += strlen($tag['before']) - 1 + 2;
2405
		}
2406
		// Don't parse the content, just skip it.
2407
		elseif ($tag['type'] == 'unparsed_content')
2408
		{
2409
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2410
			if ($pos2 === false)
2411
				continue;
2412
2413
			$data = substr($message, $pos1, $pos2 - $pos1);
2414
2415
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2416
				$data = substr($data, 4);
2417
2418
			if (isset($tag['validate']))
2419
				$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...
2420
2421
			$code = strtr($tag['content'], array('$1' => $data));
2422
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2423
2424
			$pos += strlen($code) - 1 + 2;
2425
			$last_pos = $pos + 1;
2426
2427
		}
2428
		// Don't parse the content, just skip it.
2429
		elseif ($tag['type'] == 'unparsed_equals_content')
2430
		{
2431
			// The value may be quoted for some tags - check.
2432 View Code Duplication
			if (isset($tag['quoted']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2433
			{
2434
				$quoted = substr($message, $pos1, 6) == '&quot;';
2435
				if ($tag['quoted'] != 'optional' && !$quoted)
2436
					continue;
2437
2438
				if ($quoted)
2439
					$pos1 += 6;
2440
			}
2441
			else
2442
				$quoted = false;
2443
2444
			$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...
2445
			if ($pos2 === false)
2446
				continue;
2447
2448
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2449
			if ($pos3 === false)
2450
				continue;
2451
2452
			$data = array(
2453
				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...
2454
				substr($message, $pos1, $pos2 - $pos1)
2455
			);
2456
2457
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
2458
				$data[0] = substr($data[0], 4);
2459
2460
			// Validation for my parking, please!
2461
			if (isset($tag['validate']))
2462
				$tag['validate']($tag, $data, $disabled, $params);
2463
2464
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
2465
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2466
			$pos += strlen($code) - 1 + 2;
2467
		}
2468
		// A closed tag, with no content or value.
2469
		elseif ($tag['type'] == 'closed')
2470
		{
2471
			$pos2 = strpos($message, ']', $pos);
2472
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
2473
			$pos += strlen($tag['content']) - 1 + 2;
2474
		}
2475
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
2476
		elseif ($tag['type'] == 'unparsed_commas_content')
2477
		{
2478
			$pos2 = strpos($message, ']', $pos1);
2479
			if ($pos2 === false)
2480
				continue;
2481
2482
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2483
			if ($pos3 === false)
2484
				continue;
2485
2486
			// We want $1 to be the content, and the rest to be csv.
2487
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
2488
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
2489
2490
			if (isset($tag['validate']))
2491
				$tag['validate']($tag, $data, $disabled, $params);
2492
2493
			$code = $tag['content'];
2494 View Code Duplication
			foreach ($data as $k => $d)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2495
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2496
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2497
			$pos += strlen($code) - 1 + 2;
2498
		}
2499
		// This has parsed content, and a csv value which is unparsed.
2500
		elseif ($tag['type'] == 'unparsed_commas')
2501
		{
2502
			$pos2 = strpos($message, ']', $pos1);
2503
			if ($pos2 === false)
2504
				continue;
2505
2506
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
2507
2508
			if (isset($tag['validate']))
2509
				$tag['validate']($tag, $data, $disabled, $params);
2510
2511
			// Fix after, for disabled code mainly.
2512 View Code Duplication
			foreach ($data as $k => $d)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
2513
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
2514
2515
			$open_tags[] = $tag;
2516
2517
			// Replace them out, $1, $2, $3, $4, etc.
2518
			$code = $tag['before'];
2519 View Code Duplication
			foreach ($data as $k => $d)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

Loading history...
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
			$data = substr($message, $pos1, $pos2 - $pos1);
2545
2546
			// Validation for my parking, please!
2547
			if (isset($tag['validate']))
2548
				$tag['validate']($tag, $data, $disabled, $params);
2549
2550
			// For parsed content, we must recurse to avoid security problems.
2551
			if ($tag['type'] != 'unparsed_equals')
2552
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
2553
2554
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
2555
2556
			$open_tags[] = $tag;
2557
2558
			$code = strtr($tag['before'], array('$1' => $data));
2559
			$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...
2560
			$pos += strlen($code) - 1 + 2;
2561
		}
2562
2563
		// If this is block level, eat any breaks after it.
2564 View Code Duplication
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

Loading history...
2765
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
2766
2767
	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'])))
2768
	{
2769
		if (defined('SID') && SID != '')
2770
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '/') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
2771
				function ($m) use ($scripturl)
2772
				{
2773
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID. (isset($m[2]) ? "$m[2]" : "");
2774
				}, $setLocation);
2775 View Code Duplication
		else
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

Loading history...
2973
		cache_put_data('url_image_size-' . md5($url), $size, 240);
2974
2975
	// Didn't work.
2976
	return $size;
2977
}
2978
2979
/**
2980
 * Sets up the basic theme context stuff.
2981
 * @param bool $forceload Whether to load the theme even if it's already loaded
2982
 */
2983
function setupThemeContext($forceload = false)
2984
{
2985
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
2986
	global $smcFunc;
2987
	static $loaded = false;
2988
2989
	// Under SSI this function can be called more then once.  That can cause some problems.
2990
	//   So only run the function once unless we are forced to run it again.
2991
	if ($loaded && !$forceload)
2992
		return;
2993
2994
	$loaded = true;
2995
2996
	$context['in_maintenance'] = !empty($maintenance);
2997
	$context['current_time'] = timeformat(time(), false);
2998
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
2999
3000
	// Get some news...
3001
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3002
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3003
	{
3004
		if (trim($context['news_lines'][$i]) == '')
3005
			continue;
3006
3007
		// Clean it up for presentation ;).
3008
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3009
	}
3010
	if (!empty($context['news_lines']))
3011
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3012
3013
	if (!$user_info['is_guest'])
3014
	{
3015
		$context['user']['messages'] = &$user_info['messages'];
3016
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3017
		$context['user']['alerts'] = &$user_info['alerts'];
3018
3019
		// Personal message popup...
3020
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3021
			$context['user']['popup_messages'] = true;
3022
		else
3023
			$context['user']['popup_messages'] = false;
3024
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3025
3026
		if (allowedTo('moderate_forum'))
3027
			$context['unapproved_members'] = (!empty($modSettings['registration_method']) && ($modSettings['registration_method'] == 2 || (!empty($modSettings['coppaType']) && $modSettings['coppaType'] == 2))) || !empty($modSettings['approveAccountDeletion']) ? $modSettings['unapprovedMembers'] : 0;
3028
3029
		$context['user']['avatar'] = array();
3030
3031
		// Check for gravatar first since we might be forcing them...
3032
		if (($modSettings['gravatarEnabled'] && substr($user_info['avatar']['url'], 0, 11) == 'gravatar://') || !empty($modSettings['gravatarOverride']))
3033
		{
3034
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3035
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3036
			else
3037
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3038
		}
3039
		// Uploaded?
3040
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3041
			$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';
3042
		// Full URL?
3043
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3044
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3045
		// Otherwise we assume it's server stored.
3046
		elseif ($user_info['avatar']['url'] != '')
3047
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3048
		// No avatar at all? Fine, we have a big fat default avatar ;)
3049
		else
3050
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3051
3052
		if (!empty($context['user']['avatar']))
3053
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3054
3055
		// Figure out how long they've been logged in.
3056
		$context['user']['total_time_logged_in'] = array(
3057
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3058
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3059
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3060
		);
3061
	}
3062
	else
3063
	{
3064
		$context['user']['messages'] = 0;
3065
		$context['user']['unread_messages'] = 0;
3066
		$context['user']['avatar'] = array();
3067
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3068
		$context['user']['popup_messages'] = false;
3069
3070
		if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
3071
			$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
3072
3073
		// If we've upgraded recently, go easy on the passwords.
3074
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3075
			$context['disable_login_hashing'] = true;
3076
	}
3077
3078
	// Setup the main menu items.
3079
	setupMenuContext();
3080
3081
	// This is here because old index templates might still use it.
3082
	$context['show_news'] = !empty($settings['enable_news']);
3083
3084
	// This is done to allow theme authors to customize it as they want.
3085
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3086
3087
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3088
	if ($context['show_pm_popup'])
3089
		addInlineJavaScript('
3090
		jQuery(document).ready(function($) {
3091
			new smc_Popup({
3092
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3093
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3094
				icon_class: \'generic_icons mail_new\'
3095
			});
3096
		});');
3097
3098
	// Add a generic "Are you sure?" confirmation message.
3099
	addInlineJavaScript('
3100
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) .';');
3101
3102
	// Now add the capping code for avatars.
3103
	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')
3104
		addInlineCss('
3105
img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3106
3107
	// This looks weird, but it's because BoardIndex.php references the variable.
3108
	$context['common_stats']['latest_member'] = array(
3109
		'id' => $modSettings['latestMember'],
3110
		'name' => $modSettings['latestRealName'],
3111
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3112
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3113
	);
3114
	$context['common_stats'] = array(
3115
		'total_posts' => comma_format($modSettings['totalMessages']),
3116
		'total_topics' => comma_format($modSettings['totalTopics']),
3117
		'total_members' => comma_format($modSettings['totalMembers']),
3118
		'latest_member' => $context['common_stats']['latest_member'],
3119
	);
3120
	$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']);
3121
3122
	if (empty($settings['theme_version']))
3123
		addJavaScriptVar('smf_scripturl', $scripturl);
3124
3125
	if (!isset($context['page_title']))
3126
		$context['page_title'] = '';
3127
3128
	// Set some specific vars.
3129
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3130
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3131
3132
	// Content related meta tags, including Open Graph
3133
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3134
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3135
3136
	if (!empty($context['meta_keywords']))
3137
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3138
3139
	if (!empty($context['canonical_url']))
3140
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3141
3142
	if (!empty($settings['og_image']))
3143
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3144
3145
	if (!empty($context['meta_description']))
3146
	{
3147
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3148
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3149
	}
3150
	else
3151
	{
3152
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3153
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3154
	}
3155
3156
	call_integration_hook('integrate_theme_context');
3157
}
3158
3159
/**
3160
 * Helper function to set the system memory to a needed value
3161
 * - If the needed memory is greater than current, will attempt to get more
3162
 * - if in_use is set to true, will also try to take the current memory usage in to account
3163
 *
3164
 * @param string $needed The amount of memory to request, if needed, like 256M
3165
 * @param bool $in_use Set to true to account for current memory usage of the script
3166
 * @return boolean True if we have at least the needed memory
3167
 */
3168
function setMemoryLimit($needed, $in_use = false)
3169
{
3170
	// everything in bytes
3171
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3172
	$memory_needed = memoryReturnBytes($needed);
3173
3174
	// should we account for how much is currently being used?
3175
	if ($in_use)
3176
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3177
3178
	// if more is needed, request it
3179
	if ($memory_current < $memory_needed)
3180
	{
3181
		@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...
3182
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3183
	}
3184
3185
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
3186
3187
	// return success or not
3188
	return (bool) ($memory_current >= $memory_needed);
3189
}
3190
3191
/**
3192
 * Helper function to convert memory string settings to bytes
3193
 *
3194
 * @param string $val The byte string, like 256M or 1G
3195
 * @return integer The string converted to a proper integer in bytes
3196
 */
3197
function memoryReturnBytes($val)
3198
{
3199
	if (is_integer($val))
3200
		return $val;
3201
3202
	// Separate the number from the designator
3203
	$val = trim($val);
3204
	$num = intval(substr($val, 0, strlen($val) - 1));
3205
	$last = strtolower(substr($val, -1));
3206
3207
	// convert to bytes
3208
	switch ($last)
3209
	{
3210
		case 'g':
3211
			$num *= 1024;
3212
		case 'm':
3213
			$num *= 1024;
3214
		case 'k':
3215
			$num *= 1024;
3216
	}
3217
	return $num;
3218
}
3219
3220
/**
3221
 * The header template
3222
 */
3223
function template_header()
3224
{
3225
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir;
3226
3227
	setupThemeContext();
3228
3229
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
3230
	if (empty($context['no_last_modified']))
3231
	{
3232
		header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
3233
		header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
3234
3235
		// Are we debugging the template/html content?
3236
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
3237
			header('Content-Type: application/xhtml+xml');
3238 View Code Duplication
		elseif (!isset($_REQUEST['xml']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

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

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

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

Loading history...
3468
		{
3469
			echo '
3470
<script>';
3471
3472
			foreach ($context['javascript_inline']['defer'] as $js_code)
3473
				echo $js_code;
3474
3475
			echo '
3476
</script>';
3477
		}
3478
3479 View Code Duplication
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
3480
		{
3481
			echo '
3482
	<script>';
3483
3484
			foreach ($context['javascript_inline']['standard'] as $js_code)
3485
				echo $js_code;
3486
3487
			echo '
3488
	</script>';
3489
		}
3490
	}
3491
}
3492
3493
/**
3494
 * Output the CSS files
3495
 *
3496
 */
3497
function template_css()
3498
{
3499
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
3500
3501
	// Use this hook to minify/optimize CSS files
3502
	call_integration_hook('integrate_pre_css_output');
3503
3504
	$toMinify = array();
3505
	$normal = array();
3506
3507
	foreach ($context['css_files'] as $id => $file)
3508
	{
3509
		// Last minute call! allow theme authors to disable single files.
3510
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
3511
			continue;
3512
3513
		// By default all files don't get minimized unless the file explicitly says so!
3514
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']))
3515
		{
3516
			$toMinify[] = $file;
3517
3518
			// Grab a random seed.
3519
			if (!isset($minSeed))
3520
				$minSeed = $file['options']['seed'];
3521
		}
3522
3523
		else
3524
			$normal[] = $file['fileUrl'];
3525
	}
3526
3527
	if (!empty($toMinify))
3528
	{
3529
		$result = custMinify($toMinify, 'css');
3530
3531
		// Minify process couldn't work, print each individual files.
3532
		if (!empty($result) && is_array($result))
3533
			foreach ($result as $minFailedFile)
3534
				echo '
3535
	<link rel="stylesheet" href="', $minFailedFile['fileUrl'], '">';
3536
3537
		else
3538
			echo '
3539
	<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...
3540
	}
3541
3542
	// Print the rest after the minified files.
3543
	if (!empty($normal))
3544
		foreach ($normal as $nf)
3545
			echo '
3546
	<link rel="stylesheet" href="', $nf ,'">';
3547
3548
	if ($db_show_debug === true)
3549
	{
3550
		// Try to keep only what's useful.
3551
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
3552
		foreach ($context['css_files'] as $file)
3553
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
3554
	}
3555
3556
	if (!empty($context['css_header']))
3557
	{
3558
		echo '
3559
	<style>';
3560
3561
		foreach ($context['css_header'] as $css)
3562
			echo $css .'
3563
	';
3564
3565
		echo'
3566
	</style>';
3567
	}
3568
}
3569
3570
/**
3571
 * Get an array of previously defined files and adds them to our main minified file.
3572
 * Sets a one day cache to avoid re-creating a file on every request.
3573
 *
3574
 * @param array $data The files to minify.
3575
 * @param string $type either css or js.
3576
 * @param bool $do_deferred use for type js to indicate if the minified file will be deferred, IE, put at the closing </body> tag.
3577
 * @return bool|array If an array the minify process failed and the data is returned intact.
3578
 */
3579
function custMinify($data, $type, $do_deferred = false)
3580
{
3581
	global $sourcedir, $settings, $txt;
3582
3583
	$types = array('css', 'js');
3584
	$type = !empty($type) && in_array($type, $types) ? $type : false;
3585
	$data = !empty($data) ? $data : false;
3586
3587
	if (empty($type) || empty($data))
3588
		return false;
3589
3590
	// Did we already did this?
3591
	$toCache = cache_get_data('minimized_'. $settings['theme_id'] .'_'. $type, 86400);
3592
3593
	// Already done?
3594
	if (!empty($toCache))
3595
		return true;
3596
3597
	// No namespaces, sorry!
3598
	$classType = 'MatthiasMullie\\Minify\\'. strtoupper($type);
3599
3600
	// Temp path.
3601
	$cTempPath = $settings['theme_dir'] .'/'. ($type == 'css' ? 'css' : 'scripts') .'/';
3602
3603
	// What kind of file are we going to create?
3604
	$toCreate = $cTempPath .'minified'. ($do_deferred ? '_deferred' : '') .'.'. $type;
3605
3606
	// File has to exists, if it isn't try to create it.
3607
	if ((!file_exists($toCreate) && @fopen($toCreate, 'w') === false) || !smf_chmod($toCreate))
3608
	{
3609
		loadLanguage('Errors');
3610
		log_error(sprintf($txt['file_not_created'], $toCreate), 'general');
3611
		cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, null);
3612
3613
		// The process failed so roll back to print each individual file.
3614
		return $data;
3615
	}
3616
3617
	$minifier = new $classType();
3618
3619
	foreach ($data as $file)
3620
	{
3621
		$tempFile = str_replace($file['options']['seed'], '', $file['filePath']);
3622
		$toAdd = file_exists($tempFile) ? $tempFile : false;
3623
3624
		// The file couldn't be located so it won't be added, log this error.
3625
		if (empty($toAdd))
3626
		{
3627
			loadLanguage('Errors');
3628
			log_error(sprintf($txt['file_minimize_fail'], $file['fileName']), 'general');
3629
			continue;
3630
		}
3631
3632
		// Add this file to the list.
3633
		$minifier->add($toAdd);
3634
	}
3635
3636
	// Create the file.
3637
	$minifier->minify($toCreate);
3638
	unset($minifier);
3639
	clearstatcache();
3640
3641
	// Minify process failed.
3642
	if (!filesize($toCreate))
3643
	{
3644
		loadLanguage('Errors');
3645
		log_error(sprintf($txt['file_not_created'], $toCreate), 'general');
3646
		cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, null);
3647
3648
		// The process failed so roll back to print each individual file.
3649
		return $data;
3650
	}
3651
3652
	// And create a long lived cache entry.
3653
	cache_put_data('minimized_'. $settings['theme_id'] .'_'. $type, $toCreate, 86400);
3654
3655
	return true;
3656
}
3657
3658
/**
3659
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
3660
 * @todo this currently returns the hash if new, and the full filename otherwise.
3661
 * Something messy like that.
3662
 * @todo and of course everything relies on this behavior and work around it. :P.
3663
 * Converters included.
3664
 *
3665
 * @param string $filename The name of the file
3666
 * @param int $attachment_id The ID of the attachment
3667
 * @param string $dir Which directory it should be in (null to use current one)
3668
 * @param bool $new Whether this is a new attachment
3669
 * @param string $file_hash The file hash
3670
 * @return string The path to the file
3671
 */
3672
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
3673
{
3674
	global $modSettings, $smcFunc;
3675
3676
	// Just make up a nice hash...
3677
	if ($new)
3678
		return sha1(md5($filename . time()) . mt_rand());
3679
3680
	// Grab the file hash if it wasn't added.
3681
	// Left this for legacy.
3682
	if ($file_hash === '')
3683
	{
3684
		$request = $smcFunc['db_query']('', '
3685
			SELECT file_hash
3686
			FROM {db_prefix}attachments
3687
			WHERE id_attach = {int:id_attach}',
3688
			array(
3689
				'id_attach' => $attachment_id,
3690
			));
3691
3692
		if ($smcFunc['db_num_rows']($request) === 0)
3693
			return false;
3694
3695
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
3696
		$smcFunc['db_free_result']($request);
3697
	}
3698
3699
	// Still no hash? mmm...
3700
	if (empty($file_hash))
3701
		$file_hash = sha1(md5($filename . time()) . mt_rand());
3702
3703
	// Are we using multiple directories?
3704 View Code Duplication
	if (!empty($modSettings['currentAttachmentUploadDir']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
3705
		$path = $modSettings['attachmentUploadDir'][$dir];
3706
3707
	else
3708
		$path = $modSettings['attachmentUploadDir'];
3709
3710
	return $path . '/' . $attachment_id . '_' . $file_hash .'.dat';
3711
}
3712
3713
/**
3714
 * Convert a single IP to a ranged IP.
3715
 * internal function used to convert a user-readable format to a format suitable for the database.
3716
 *
3717
 * @param string $fullip The full IP
3718
 * @return array An array of IP parts
3719
 */
3720
function ip2range($fullip)
3721
{
3722
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
3723
	if ($fullip == 'unknown')
3724
		$fullip = '255.255.255.255';
3725
3726
	$ip_parts = explode('-', $fullip);
3727
	$ip_array = array();
3728
3729
	// if ip 22.12.31.21
3730
	if (count($ip_parts) == 1 && isValidIP($fullip))
3731
	{
3732
		$ip_array['low'] = $fullip;
3733
		$ip_array['high'] = $fullip;
3734
		return $ip_array;
3735
	} // if ip 22.12.* -> 22.12.* - 22.12.*
3736
	elseif (count($ip_parts) == 1)
3737
	{
3738
		$ip_parts[0] = $fullip;
3739
		$ip_parts[1] = $fullip;
3740
	}
3741
3742
	// if ip 22.12.31.21-12.21.31.21
3743
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
3744
	{
3745
		$ip_array['low'] = $ip_parts[0];
3746
		$ip_array['high'] = $ip_parts[1];
3747
		return $ip_array;
3748
	}
3749
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
3750
	{
3751
		$valid_low = isValidIP($ip_parts[0]);
3752
		$valid_high = isValidIP($ip_parts[1]);
3753
		$count = 0;
3754
		$mode = (preg_match('/:/',$ip_parts[0]) > 0 ? ':' : '.');
3755
		$max = ($mode == ':' ? 'ffff' : '255');
3756
		$min = 0;
3757 View Code Duplication
		if(!$valid_low)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
3758
		{
3759
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
3760
			$valid_low = isValidIP($ip_parts[0]);
3761
			while (!$valid_low)
3762
			{
3763
				$ip_parts[0] .= $mode . $min;
3764
				$valid_low = isValidIP($ip_parts[0]);
3765
				$count++;
3766
				if ($count > 9) break;
3767
			}
3768
		}
3769
3770
		$count = 0;
3771 View Code Duplication
		if(!$valid_high)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

Loading history...
3844
		cache_put_data('hostlookup-' . $ip, $host, 600);
3845
3846
	return $host;
3847
}
3848
3849
/**
3850
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
3851
 *
3852
 * @param string $text The text to split into words
3853
 * @param int $max_chars The maximum number of characters per word
3854
 * @param bool $encrypt Whether to encrypt the results
3855
 * @return array An array of ints or words depending on $encrypt
3856
 */
3857
function text2words($text, $max_chars = 20, $encrypt = false)
3858
{
3859
	global $smcFunc, $context;
3860
3861
	// Step 1: Remove entities/things we don't consider words:
3862
	$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>' => ' ')));
3863
3864
	// Step 2: Entities we left to letters, where applicable, lowercase.
3865
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
3866
3867
	// Step 3: Ready to split apart and index!
3868
	$words = explode(' ', $words);
3869
3870
	if ($encrypt)
3871
	{
3872
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
3873
		$returned_ints = array();
3874
		foreach ($words as $word)
3875
		{
3876
			if (($word = trim($word, '-_\'')) !== '')
3877
			{
3878
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
3879
				$total = 0;
3880
				for ($i = 0; $i < $max_chars; $i++)
3881
					$total += $possible_chars[ord($encrypted{$i})] * pow(63, $i);
3882
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
3883
			}
3884
		}
3885
		return array_unique($returned_ints);
3886
	}
3887
	else
3888
	{
3889
		// Trim characters before and after and add slashes for database insertion.
3890
		$returned_words = array();
3891
		foreach ($words as $word)
3892
			if (($word = trim($word, '-_\'')) !== '')
3893
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
3894
3895
		// Filter out all words that occur more than once.
3896
		return array_unique($returned_words);
3897
	}
3898
}
3899
3900
/**
3901
 * Creates an image/text button
3902
 *
3903
 * @param string $name The name of the button (should be a generic_icons class or the name of an image)
3904
 * @param string $alt The alt text
3905
 * @param string $label The $txt string to use as the label
3906
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
3907
 * @param boolean $force_use Whether to force use of this when template_create_button is available
3908
 * @return string The HTML to display the button
3909
 */
3910
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
3911
{
3912
	global $settings, $txt;
3913
3914
	// Does the current loaded theme have this and we are not forcing the usage of this function?
3915
	if (function_exists('template_create_button') && !$force_use)
3916
		return template_create_button($name, $alt, $label = '', $custom = '');
3917
3918
	if (!$settings['use_image_buttons'])
3919
		return $txt[$alt];
3920
	elseif (!empty($settings['use_buttons']))
3921
		return '<span class="generic_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
3922
	else
3923
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
3924
}
3925
3926
/**
3927
 * Sets up all of the top menu buttons
3928
 * Saves them in the cache if it is available and on
3929
 * Places the results in $context
3930
 *
3931
 */
3932
function setupMenuContext()
3933
{
3934
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings;
3935
3936
	// Set up the menu privileges.
3937
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
3938
	$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'));
3939
3940
	$context['allow_memberlist'] = allowedTo('view_mlist');
3941
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
3942
	$context['allow_moderation_center'] = $context['user']['can_mod'];
3943
	$context['allow_pm'] = allowedTo('pm_read');
3944
3945
	$cacheTime = $modSettings['lastActive'] * 60;
3946
3947
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
3948
	if (!isset($context['allow_calendar_event']))
3949
	{
3950
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
3951
3952
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
3953 View Code Duplication
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
3954
		{
3955
			$boards_can_post = boardsAllowedTo('post_new');
3956
			$context['allow_calendar_event'] &= !empty($boards_can_post);
3957
		}
3958
	}
3959
3960
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
3961
	if (!$context['user']['is_guest'])
3962
	{
3963
		addInlineJavaScript('
3964
	var user_menus = new smc_PopupMenu();
3965
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
3966
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u='. $context['user']['id'] .'");', true);
3967
		if ($context['allow_pm'])
3968
			addInlineJavaScript('
3969
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
3970
3971
		if (!empty($modSettings['enable_ajax_alerts']))
3972
		{
3973
			require_once($sourcedir . '/Subs-Notify.php');
3974
3975
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
3976
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
3977
3978
			addInlineJavaScript('
3979
	var new_alert_title = "' . $context['forum_name'] . '";
3980
	var alert_timeout = ' . $timeout . ';');
3981
			loadJavaScriptFile('alerts.js', array(), 'smf_alerts');
3982
		}
3983
	}
3984
3985
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
3986
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
3987
	{
3988
		$buttons = array(
3989
			'home' => array(
3990
				'title' => $txt['home'],
3991
				'href' => $scripturl,
3992
				'show' => true,
3993
				'sub_buttons' => array(
3994
				),
3995
				'is_last' => $context['right_to_left'],
3996
			),
3997
			'search' => array(
3998
				'title' => $txt['search'],
3999
				'href' => $scripturl . '?action=search',
4000
				'show' => $context['allow_search'],
4001
				'sub_buttons' => array(
4002
				),
4003
			),
4004
			'admin' => array(
4005
				'title' => $txt['admin'],
4006
				'href' => $scripturl . '?action=admin',
4007
				'show' => $context['allow_admin'],
4008
				'sub_buttons' => array(
4009
					'featuresettings' => array(
4010
						'title' => $txt['modSettings_title'],
4011
						'href' => $scripturl . '?action=admin;area=featuresettings',
4012
						'show' => allowedTo('admin_forum'),
4013
					),
4014
					'packages' => array(
4015
						'title' => $txt['package'],
4016
						'href' => $scripturl . '?action=admin;area=packages',
4017
						'show' => allowedTo('admin_forum'),
4018
					),
4019
					'errorlog' => array(
4020
						'title' => $txt['errlog'],
4021
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
4022
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
4023
					),
4024
					'permissions' => array(
4025
						'title' => $txt['edit_permissions'],
4026
						'href' => $scripturl . '?action=admin;area=permissions',
4027
						'show' => allowedTo('manage_permissions'),
4028
					),
4029
					'memberapprove' => array(
4030
						'title' => $txt['approve_members_waiting'],
4031
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
4032
						'show' => !empty($context['unapproved_members']),
4033
						'is_last' => true,
4034
					),
4035
				),
4036
			),
4037
			'moderate' => array(
4038
				'title' => $txt['moderate'],
4039
				'href' => $scripturl . '?action=moderate',
4040
				'show' => $context['allow_moderation_center'],
4041
				'sub_buttons' => array(
4042
					'modlog' => array(
4043
						'title' => $txt['modlog_view'],
4044
						'href' => $scripturl . '?action=moderate;area=modlog',
4045
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4046
					),
4047
					'poststopics' => array(
4048
						'title' => $txt['mc_unapproved_poststopics'],
4049
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
4050
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4051
					),
4052
					'attachments' => array(
4053
						'title' => $txt['mc_unapproved_attachments'],
4054
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
4055
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4056
					),
4057
					'reports' => array(
4058
						'title' => $txt['mc_reported_posts'],
4059
						'href' => $scripturl . '?action=moderate;area=reportedposts',
4060
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4061
					),
4062
					'reported_members' => array(
4063
						'title' => $txt['mc_reported_members'],
4064
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
4065
						'show' => allowedTo('moderate_forum'),
4066
						'is_last' => true,
4067
					)
4068
				),
4069
			),
4070
			'calendar' => array(
4071
				'title' => $txt['calendar'],
4072
				'href' => $scripturl . '?action=calendar',
4073
				'show' => $context['allow_calendar'],
4074
				'sub_buttons' => array(
4075
					'view' => array(
4076
						'title' => $txt['calendar_menu'],
4077
						'href' => $scripturl . '?action=calendar',
4078
						'show' => $context['allow_calendar_event'],
4079
					),
4080
					'post' => array(
4081
						'title' => $txt['calendar_post_event'],
4082
						'href' => $scripturl . '?action=calendar;sa=post',
4083
						'show' => $context['allow_calendar_event'],
4084
						'is_last' => true,
4085
					),
4086
				),
4087
			),
4088
			'mlist' => array(
4089
				'title' => $txt['members_title'],
4090
				'href' => $scripturl . '?action=mlist',
4091
				'show' => $context['allow_memberlist'],
4092
				'sub_buttons' => array(
4093
					'mlist_view' => array(
4094
						'title' => $txt['mlist_menu_view'],
4095
						'href' => $scripturl . '?action=mlist',
4096
						'show' => true,
4097
					),
4098
					'mlist_search' => array(
4099
						'title' => $txt['mlist_search'],
4100
						'href' => $scripturl . '?action=mlist;sa=search',
4101
						'show' => true,
4102
						'is_last' => true,
4103
					),
4104
				),
4105
			),
4106
			'signup' => array(
4107
				'title' => $txt['register'],
4108
				'href' => $scripturl . '?action=signup',
4109
				'show' => $user_info['is_guest'] && $context['can_register'],
4110
				'sub_buttons' => array(
4111
				),
4112
				'is_last' => !$context['right_to_left'],
4113
			),
4114
			'logout' => array(
4115
				'title' => $txt['logout'],
4116
				'href' => $scripturl . '?action=logout;%1$s=%2$s',
4117
				'show' => !$user_info['is_guest'],
4118
				'sub_buttons' => array(
4119
				),
4120
				'is_last' => !$context['right_to_left'],
4121
			),
4122
		);
4123
4124
		// Allow editing menu buttons easily.
4125
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
4126
4127
		// Now we put the buttons in the context so the theme can use them.
4128
		$menu_buttons = array();
4129
		foreach ($buttons as $act => $button)
4130
			if (!empty($button['show']))
4131
			{
4132
				$button['active_button'] = false;
4133
4134
				// This button needs some action.
4135
				if (isset($button['action_hook']))
4136
					$needs_action_hook = true;
4137
4138
				// Make sure the last button truly is the last button.
4139
				if (!empty($button['is_last']))
4140
				{
4141
					if (isset($last_button))
4142
						unset($menu_buttons[$last_button]['is_last']);
4143
					$last_button = $act;
4144
				}
4145
4146
				// Go through the sub buttons if there are any.
4147
				if (!empty($button['sub_buttons']))
4148
					foreach ($button['sub_buttons'] as $key => $subbutton)
4149
					{
4150
						if (empty($subbutton['show']))
4151
							unset($button['sub_buttons'][$key]);
4152
4153
						// 2nd level sub buttons next...
4154
						if (!empty($subbutton['sub_buttons']))
4155
						{
4156
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
4157
							{
4158
								if (empty($sub_button2['show']))
4159
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
4160
							}
4161
						}
4162
					}
4163
4164
				// Does this button have its own icon?
4165
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
4166
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
4167
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
4168
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
4169
				elseif (isset($button['icon']))
4170
					$button['icon'] = '<span class="generic_icons ' . $button['icon'] . '"></span>';
4171
				else
4172
					$button['icon'] = '<span class="generic_icons ' . $act . '"></span>';
4173
4174
				$menu_buttons[$act] = $button;
4175
			}
4176
4177
		if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2)
4178
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
4179
	}
4180
4181
	$context['menu_buttons'] = $menu_buttons;
4182
4183
	// Logging out requires the session id in the url.
4184
	if (isset($context['menu_buttons']['logout']))
4185
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
4186
4187
	// Figure out which action we are doing so we can set the active tab.
4188
	// Default to home.
4189
	$current_action = 'home';
4190
4191
	if (isset($context['menu_buttons'][$context['current_action']]))
4192
		$current_action = $context['current_action'];
4193
	elseif ($context['current_action'] == 'search2')
4194
		$current_action = 'search';
4195
	elseif ($context['current_action'] == 'theme')
4196
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
4197
	elseif ($context['current_action'] == 'register2')
4198
		$current_action = 'register';
4199
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
4200
		$current_action = 'login';
4201
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
4202
		$current_action = 'moderate';
4203
4204
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
4205
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
4206
	{
4207
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
4208
		$context[$current_action] = true;
4209
	}
4210
	elseif ($context['current_action'] == 'pm')
4211
	{
4212
		$current_action = 'self_pm';
4213
		$context['self_pm'] = true;
4214
	}
4215
4216
	$total_mod_reports = 0;
4217
4218
	if (!empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1' && !empty($context['open_mod_reports']))
4219
	{
4220
		$total_mod_reports = $context['open_mod_reports'];
4221
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['title'] .= ' <span class="amt">' . $context['open_mod_reports'] . '</span>';
4222
	}
4223
4224
	// Show how many errors there are
4225 View Code Duplication
	if (!empty($context['num_errors']) && allowedTo('admin_forum'))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
4226
	{
4227
		$context['menu_buttons']['admin']['title'] .= ' <span class="amt">' . $context['num_errors'] . '</span>';
4228
		$context['menu_buttons']['admin']['sub_buttons']['errorlog']['title'] .= ' <span class="amt">' . $context['num_errors'] . '</span>';
4229
	}
4230
4231
	/**
4232
	 * @todo For some reason, $context['open_member_reports'] isn't getting set
4233
	 */
4234
	if (!empty($context['open_member_reports']) && allowedTo('moderate_forum'))
4235
	{
4236
		$total_mod_reports += $context['open_member_reports'];
4237
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['title'] .= ' <span class="amt">' . $context['open_member_reports'] . '</span>';
4238
	}
4239
4240 View Code Duplication
	if (!empty($context['unapproved_members']))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

Loading history...
4241
	{
4242
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['title'] .= ' <span class="amt">' . $context['unapproved_members'] . '</span>';
4243
		$context['menu_buttons']['admin']['title'] .= ' <span class="amt">' . $context['unapproved_members'] . '</span>';
4244
	}
4245
4246
	// Do we have any open reports?
4247
	if ($total_mod_reports > 0)
4248
	{
4249
		$context['menu_buttons']['moderate']['title'] .= ' <span class="amt">' . $total_mod_reports . '</span>';
4250
	}
4251
4252
	// Not all actions are simple.
4253
	if (!empty($needs_action_hook))
4254
		call_integration_hook('integrate_current_action', array(&$current_action));
4255
4256
	if (isset($context['menu_buttons'][$current_action]))
4257
		$context['menu_buttons'][$current_action]['active_button'] = true;
4258
}
4259
4260
/**
4261
 * Generate a random seed and ensure it's stored in settings.
4262
 */
4263
function smf_seed_generator()
4264
{
4265
	updateSettings(array('rand_seed' => microtime() * 1000000));
4266
}
4267
4268
/**
4269
 * Process functions of an integration hook.
4270
 * calls all functions of the given hook.
4271
 * supports static class method calls.
4272
 *
4273
 * @param string $hook The hook name
4274
 * @param array $parameters An array of parameters this hook implements
4275
 * @return array The results of the functions
4276
 */
4277
function call_integration_hook($hook, $parameters = array())
4278
{
4279
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
4280
	global $context, $txt;
4281
4282
	if ($db_show_debug === true)
4283
		$context['debug']['hooks'][] = $hook;
4284
4285
	// Need to have some control.
4286
	if (!isset($context['instances']))
4287
		$context['instances'] = array();
4288
4289
	$results = array();
4290
	if (empty($modSettings[$hook]))
4291
		return $results;
4292
4293
	// Define some needed vars.
4294
	$function = false;
4295
4296
	$functions = explode(',', $modSettings[$hook]);
4297
	// Loop through each function.
4298
	foreach ($functions as $function)
4299
	{
4300
		// Hook has been marked as "disabled". Skip it!
4301
		if (strpos($function, '!') !== false)
4302
			continue;
4303
4304
		$call = call_helper($function, true);
4305
4306
		// Is it valid?
4307
		if (!empty($call))
4308
			$results[$function] = call_user_func_array($call, $parameters);
4309
4310
		// Whatever it was suppose to call, it failed :(
4311
		elseif (!empty($function))
4312
		{
4313
			loadLanguage('Errors');
4314
4315
			// Get a full path to show on error.
4316
			if (strpos($function, '|') !== false)
4317
			{
4318
				list ($file, $string) = explode('|', $function);
4319
				$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'])));
4320
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
4321
			}
4322
4323
			// "Assume" the file resides on $boarddir somewhere...
4324
			else
4325
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
4326
		}
4327
	}
4328
4329
	return $results;
4330
}
4331
4332
/**
4333
 * Add a function for integration hook.
4334
 * does nothing if the function is already added.
4335
 *
4336
 * @param string $hook The complete hook name.
4337
 * @param string $function The function name. Can be a call to a method via Class::method.
4338
 * @param bool $permanent If true, updates the value in settings table.
4339
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4340
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4341
 */
4342
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
4343
{
4344
	global $smcFunc, $modSettings;
4345
4346
	// Any objects?
4347
	if ($object)
4348
		$function = $function . '#';
4349
4350
	// Any files  to load?
4351
	if (!empty($file) && is_string($file))
4352
		$function = $file . (!empty($function) ? '|' . $function : '');
4353
4354
	// Get the correct string.
4355
	$integration_call = $function;
4356
4357
	// Is it going to be permanent?
4358
	if ($permanent)
4359
	{
4360
		$request = $smcFunc['db_query']('', '
4361
			SELECT value
4362
			FROM {db_prefix}settings
4363
			WHERE variable = {string:variable}',
4364
			array(
4365
				'variable' => $hook,
4366
			)
4367
		);
4368
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
4369
		$smcFunc['db_free_result']($request);
4370
4371
		if (!empty($current_functions))
4372
		{
4373
			$current_functions = explode(',', $current_functions);
4374
			if (in_array($integration_call, $current_functions))
4375
				return;
4376
4377
			$permanent_functions = array_merge($current_functions, array($integration_call));
4378
		}
4379
		else
4380
			$permanent_functions = array($integration_call);
4381
4382
		updateSettings(array($hook => implode(',', $permanent_functions)));
4383
	}
4384
4385
	// Make current function list usable.
4386
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4387
4388
	// Do nothing, if it's already there.
4389
	if (in_array($integration_call, $functions))
4390
		return;
4391
4392
	$functions[] = $integration_call;
4393
	$modSettings[$hook] = implode(',', $functions);
4394
}
4395
4396
/**
4397
 * Remove an integration hook function.
4398
 * Removes the given function from the given hook.
4399
 * Does nothing if the function is not available.
4400
 *
4401
 * @param string $hook The complete hook name.
4402
 * @param string $function The function name. Can be a call to a method via Class::method.
4403
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
4404
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4405
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4406
 * @see add_integration_function
4407
 */
4408
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...
4409
{
4410
	global $smcFunc, $modSettings;
4411
4412
	// Any objects?
4413
	if ($object)
4414
		$function = $function . '#';
4415
4416
	// Any files  to load?
4417
	if (!empty($file) && is_string($file))
4418
		$function = $file . '|' . $function;
4419
4420
	// Get the correct string.
4421
	$integration_call = $function;
4422
4423
	// Get the permanent functions.
4424
	$request = $smcFunc['db_query']('', '
4425
		SELECT value
4426
		FROM {db_prefix}settings
4427
		WHERE variable = {string:variable}',
4428
		array(
4429
			'variable' => $hook,
4430
		)
4431
	);
4432
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
4433
	$smcFunc['db_free_result']($request);
4434
4435
	if (!empty($current_functions))
4436
	{
4437
		$current_functions = explode(',', $current_functions);
4438
4439
		if (in_array($integration_call, $current_functions))
4440
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
4441
	}
4442
4443
	// Turn the function list into something usable.
4444
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4445
4446
	// You can only remove it if it's available.
4447
	if (!in_array($integration_call, $functions))
4448
		return;
4449
4450
	$functions = array_diff($functions, array($integration_call));
4451
	$modSettings[$hook] = implode(',', $functions);
4452
}
4453
4454
/**
4455
 * Receives a string and tries to figure it out if its a method or a function.
4456
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
4457
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
4458
 * Prepare and returns a callable depending on the type of method/function found.
4459
 *
4460
 * @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)
4461
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
4462
 * @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.
4463
 */
4464
function call_helper($string, $return = false)
4465
{
4466
	global $context, $smcFunc, $txt, $db_show_debug;
4467
4468
	// Really?
4469
	if (empty($string))
4470
		return false;
4471
4472
	// An array? should be a "callable" array IE array(object/class, valid_callable).
4473
	// A closure? should be a callable one.
4474
	if (is_array($string) || $string instanceof Closure)
4475
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
4476
4477
	// No full objects, sorry! pass a method or a property instead!
4478
	if (is_object($string))
4479
		return false;
4480
4481
	// Stay vitaminized my friends...
4482
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
4483
4484
	// Is there a file to load?
4485
	$string = load_file($string);
4486
4487
	// Loaded file failed
4488
	if (empty($string))
4489
		return false;
4490
4491
	// Found a method.
4492
	if (strpos($string, '::') !== false)
4493
	{
4494
		list ($class, $method) = explode('::', $string);
4495
4496
		// Check if a new object will be created.
4497
		if (strpos($method, '#') !== false)
4498
		{
4499
			// Need to remove the # thing.
4500
			$method = str_replace('#', '', $method);
4501
4502
			// Don't need to create a new instance for every method.
4503
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
4504
			{
4505
				$context['instances'][$class] = new $class;
4506
4507
				// Add another one to the list.
4508
				if ($db_show_debug === true)
4509
				{
4510
					if (!isset($context['debug']['instances']))
4511
						$context['debug']['instances'] = array();
4512
4513
					$context['debug']['instances'][$class] = $class;
4514
				}
4515
			}
4516
4517
			$func = array($context['instances'][$class], $method);
4518
		}
4519
4520
		// Right then. This is a call to a static method.
4521
		else
4522
			$func = array($class, $method);
4523
	}
4524
4525
	// Nope! just a plain regular function.
4526
	else
4527
		$func = $string;
4528
4529
	// Right, we got what we need, time to do some checks.
4530
	if (!is_callable($func, false, $callable_name))
4531
	{
4532
		loadLanguage('Errors');
4533
		log_error(sprintf($txt['subAction_fail'], $callable_name), 'general');
4534
4535
		// Gotta tell everybody.
4536
		return false;
4537
	}
4538
4539
	// Everything went better than expected.
4540
	else
4541
	{
4542
		// What are we gonna do about it?
4543
		if ($return)
4544
			return $func;
4545
4546
		// If this is a plain function, avoid the heat of calling call_user_func().
4547
		else
4548
		{
4549
			if (is_array($func))
4550
				call_user_func($func);
4551
4552
			else
4553
				$func();
4554
		}
4555
	}
4556
}
4557
4558
/**
4559
 * Receives a string and tries to figure it out if it contains info to load a file.
4560
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
4561
 * 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.
4562
 *
4563
 * @param string $string The string containing a valid format.
4564
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
4565
 */
4566
function load_file($string)
4567
{
4568
	global $sourcedir, $txt, $boarddir, $settings;
4569
4570
	if (empty($string))
4571
		return false;
4572
4573
	if (strpos($string, '|') !== false)
4574
	{
4575
		list ($file, $string) = explode('|', $string);
4576
4577
		// Match the wildcards to their regular vars.
4578
		if (empty($settings['theme_dir']))
4579
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
4580
4581
		else
4582
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
4583
4584
		// Load the file if it can be loaded.
4585
		if (file_exists($absPath))
4586
			require_once($absPath);
4587
4588
		// No? try a fallback to $sourcedir
4589
		else
4590
		{
4591
			$absPath = $sourcedir .'/'. $file;
4592
4593
			if (file_exists($absPath))
4594
				require_once($absPath);
4595
4596
			// Sorry, can't do much for you at this point.
4597
			else
4598
			{
4599
				loadLanguage('Errors');
4600
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
4601
4602
				// File couldn't be loaded.
4603
				return false;
4604
			}
4605
		}
4606
	}
4607
4608
	return $string;
4609
}
4610
4611
/**
4612
 * Prepares an array of "likes" info for the topic specified by $topic
4613
 * @param integer $topic The topic ID to fetch the info from.
4614
 * @return array An array of IDs of messages in the specified topic that the current user likes
4615
 */
4616
function prepareLikesContext($topic)
4617
{
4618
	global $user_info, $smcFunc;
4619
4620
	// Make sure we have something to work with.
4621
	if (empty($topic))
4622
		return array();
4623
4624
4625
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
4626
	$user = $user_info['id'];
4627
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
4628
	$ttl = 180;
4629
4630
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
4631
	{
4632
		$temp = array();
4633
		$request = $smcFunc['db_query']('', '
4634
			SELECT content_id
4635
			FROM {db_prefix}user_likes AS l
4636
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
4637
			WHERE l.id_member = {int:current_user}
4638
				AND l.content_type = {literal:msg}
4639
				AND m.id_topic = {int:topic}',
4640
			array(
4641
				'current_user' => $user,
4642
				'topic' => $topic,
4643
			)
4644
		);
4645
		while ($row = $smcFunc['db_fetch_assoc']($request))
4646
			$temp[] = (int) $row['content_id'];
4647
4648
		cache_put_data($cache_key, $temp, $ttl);
4649
	}
4650
4651
	return $temp;
4652
}
4653
4654
/**
4655
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
4656
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
4657
 * that are not normally displayable.  This converts the popular ones that
4658
 * appear from a cut and paste from windows.
4659
 *
4660
 * @param string $string The string
4661
 * @return string The sanitized string
4662
 */
4663
function sanitizeMSCutPaste($string)
4664
{
4665
	global $context;
4666
4667
	if (empty($string))
4668
		return $string;
4669
4670
	// UTF-8 occurences of MS special characters
4671
	$findchars_utf8 = array(
4672
		"\xe2\80\x9a",	// single low-9 quotation mark
4673
		"\xe2\80\x9e",	// double low-9 quotation mark
4674
		"\xe2\80\xa6",	// horizontal ellipsis
4675
		"\xe2\x80\x98",	// left single curly quote
4676
		"\xe2\x80\x99",	// right single curly quote
4677
		"\xe2\x80\x9c",	// left double curly quote
4678
		"\xe2\x80\x9d",	// right double curly quote
4679
		"\xe2\x80\x93",	// en dash
4680
		"\xe2\x80\x94",	// em dash
4681
	);
4682
4683
	// windows 1252 / iso equivalents
4684
	$findchars_iso = array(
4685
		chr(130),
4686
		chr(132),
4687
		chr(133),
4688
		chr(145),
4689
		chr(146),
4690
		chr(147),
4691
		chr(148),
4692
		chr(150),
4693
		chr(151),
4694
	);
4695
4696
	// safe replacements
4697
	$replacechars = array(
4698
		',',	// &sbquo;
4699
		',,',	// &bdquo;
4700
		'...',	// &hellip;
4701
		"'",	// &lsquo;
4702
		"'",	// &rsquo;
4703
		'"',	// &ldquo;
4704
		'"',	// &rdquo;
4705
		'-',	// &ndash;
4706
		'--',	// &mdash;
4707
	);
4708
4709
	if ($context['utf8'])
4710
		$string = str_replace($findchars_utf8, $replacechars, $string);
4711
	else
4712
		$string = str_replace($findchars_iso, $replacechars, $string);
4713
4714
	return $string;
4715
}
4716
4717
/**
4718
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
4719
 *
4720
 * Callback function for preg_replace_callback in subs-members
4721
 * Uses capture group 2 in the supplied array
4722
 * Does basic scan to ensure characters are inside a valid range
4723
 *
4724
 * @param array $matches An array of matches (relevant info should be the 3rd item)
4725
 * @return string A fixed string
4726
 */
4727
function replaceEntities__callback($matches)
4728
{
4729
	global $context;
4730
4731
	if (!isset($matches[2]))
4732
		return '';
4733
4734
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
4735
4736
	// remove left to right / right to left overrides
4737
	if ($num === 0x202D || $num === 0x202E)
4738
		return '';
4739
4740
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
4741
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
4742
		return '&#' . $num . ';';
4743
4744
	if (empty($context['utf8']))
4745
	{
4746
		// no control characters
4747
		if ($num < 0x20)
4748
			return '';
4749
		// text is text
4750
		elseif ($num < 0x80)
4751
			return chr($num);
4752
		// all others get html-ised
4753
		else
4754
			return '&#' . $matches[2] . ';';
4755
	}
4756
	else
4757
	{
4758
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
4759
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
4760
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
4761
			return '';
4762
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
4763
		elseif ($num < 0x80)
4764
			return chr($num);
4765
		// <0x800 (2048)
4766 View Code Duplication
		elseif ($num < 0x800)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

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

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

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

Loading history...
4773
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4774
	}
4775
}
4776
4777
/**
4778
 * Converts html entities to utf8 equivalents
4779
 *
4780
 * Callback function for preg_replace_callback
4781
 * Uses capture group 1 in the supplied array
4782
 * Does basic checks to keep characters inside a viewable range.
4783
 *
4784
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
4785
 * @return string The fixed string
4786
 */
4787
function fixchar__callback($matches)
4788
{
4789
	if (!isset($matches[1]))
4790
		return '';
4791
4792
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
4793
4794
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
4795
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
4796
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
4797
		return '';
4798
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
4799
	elseif ($num < 0x80)
4800
		return chr($num);
4801
	// <0x800 (2048)
4802 View Code Duplication
	elseif ($num < 0x800)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

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

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

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

Loading history...
4809
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
4810
}
4811
4812
/**
4813
 * Strips out invalid html entities, replaces others with html style &#123; codes
4814
 *
4815
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
4816
 * strpos, strlen, substr etc
4817
 *
4818
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
4819
 * @return string The fixed string
4820
 */
4821
function entity_fix__callback($matches)
4822
{
4823
	if (!isset($matches[2]))
4824
		return '';
4825
4826
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
4827
4828
	// we don't allow control characters, characters out of range, byte markers, etc
4829
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
4830
		return '';
4831
	else
4832
		return '&#' . $num . ';';
4833
}
4834
4835
/**
4836
 * Return a Gravatar URL based on
4837
 * - the supplied email address,
4838
 * - the global maximum rating,
4839
 * - the global default fallback,
4840
 * - maximum sizes as set in the admin panel.
4841
 *
4842
 * It is SSL aware, and caches most of the parameters.
4843
 *
4844
 * @param string $email_address The user's email address
4845
 * @return string The gravatar URL
4846
 */
4847
function get_gravatar_url($email_address)
4848
{
4849
	global $modSettings, $smcFunc;
4850
	static $url_params = null;
4851
4852
	if ($url_params === null)
4853
	{
4854
		$ratings = array('G', 'PG', 'R', 'X');
4855
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
4856
		$url_params = array();
4857 View Code Duplication
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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

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

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

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

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

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

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

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

Loading history...
5832