Passed
Push — release-2.1 ( 2e48b8...01120d )
by Mathias
06:33
created

call_helper()   D

Complexity

Conditions 19

Size

Total Lines 93
Code Lines 40

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 19
eloc 40
c 1
b 1
f 0
nop 2
dl 0
loc 93
rs 4.5166

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

720
	return number_format($number, /** @scrutinizer ignore-type */ (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
Loading history...
721
}
722
723
/**
724
 * Format a time to make it look purdy.
725
 *
726
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
727
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
728
 * - if todayMod is set and show_today was not not specified or true, an
729
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
730
 * - performs localization (more than just strftime would do alone.)
731
 *
732
 * @param int $log_time A timestamp
733
 * @param bool $show_today Whether to show "Today"/"Yesterday" or just a date
734
 * @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.
735
 * @param bool $process_safe Activate setlocale check for changes at runtime. Slower, but safer.
736
 * @return string A formatted timestamp
737
 */
738
function timeformat($log_time, $show_today = true, $offset_type = false, $process_safe = false)
739
{
740
	global $context, $user_info, $txt, $modSettings;
741
	static $non_twelve_hour, $locale_cache, $now;
742
	static $unsupportedFormats, $finalizedFormats;
743
744
	$unsupportedFormatsWindows = array('z', 'Z');
745
746
	// Ensure required values are set
747
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
748
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
749
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
750
751
	// Offset the time.
752
	if (!$offset_type)
753
		$time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
754
	// Just the forum offset?
755
	elseif ($offset_type == 'forum')
756
		$time = $log_time + $modSettings['time_offset'] * 3600;
757
	else
758
		$time = $log_time;
759
760
	// We can't have a negative date (on Windows, at least.)
761
	if ($log_time < 0)
762
		$log_time = 0;
763
764
	// Today and Yesterday?
765
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
766
	{
767
		$then = @getdate($time);
768
		$now = (!empty($now) ? $now : @getdate(forum_time()));
769
770
		// Try to make something of a time format string...
771
		$s = strpos($user_info['time_format'], '%S') === false ? '' : ':%S';
772
		if (strpos($user_info['time_format'], '%H') === false && strpos($user_info['time_format'], '%T') === false)
773
		{
774
			$h = strpos($user_info['time_format'], '%l') === false ? '%I' : '%l';
775
			$today_fmt = $h . ':%M' . $s . ' %p';
776
		}
777
		else
778
			$today_fmt = '%H:%M' . $s;
779
780
		// Same day of the year, same year.... Today!
781
		if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
782
			return $txt['today'] . timeformat($log_time, $today_fmt, $offset_type);
0 ignored issues
show
Bug introduced by
$today_fmt of type string is incompatible with the type boolean expected by parameter $show_today of timeformat(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

782
			return $txt['today'] . timeformat($log_time, /** @scrutinizer ignore-type */ $today_fmt, $offset_type);
Loading history...
783
784
		// 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...
785
		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))
786
			return $txt['yesterday'] . timeformat($log_time, $today_fmt, $offset_type);
787
	}
788
789
	$str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
0 ignored issues
show
introduced by
The condition is_bool($show_today) is always true.
Loading history...
790
791
	// Use the cached formats if available
792
	if (is_null($finalizedFormats))
793
		$finalizedFormats = (array) cache_get_data('timeformatstrings', 86400);
794
795
	// Make a supported version for this format if we don't already have one
796
	if (empty($finalizedFormats[$str]))
797
	{
798
		$timeformat = $str;
799
800
		// Not all systems support all formats, and Windows fails altogether if unsupported ones are
801
		// used, so let's prevent that. Some substitutions go to the nearest reasonable fallback, some
802
		// turn into static strings, some (i.e. %a, %A, $b, %B, %p) have special handling below.
803
		$strftimeFormatSubstitutions = array(
804
			// Day
805
			'a' => '%a', 'A' => '%A', 'e' => '%d', 'd' => '&#37;d', 'j' => '&#37;j', 'u' => '%w', 'w' => '&#37;w',
806
			// Week
807
			'U' => '&#37;U', 'V' => '%U', 'W' => '%U',
808
			// Month
809
			'b' => '%b', 'B' => '%B', 'h' => '%b', 'm' => '%b',
810
			// Year
811
			'C' => '&#37;C', 'g' => '%y', 'G' => '%Y', 'y' => '&#37;y', 'Y' => '&#37;Y',
812
			// Time
813
			'H' => '&#37;H', 'k' => '%H', 'I' => '%H', 'l' => '%I', 'M' => '&#37;M', 'p' => '%p', 'P' => '%p',
814
			'r' => '%I:%M:%S %p', 'R' => '%H:%M', 'S' => '&#37;S', 'T' => '%H:%M:%S', 'X' => '%T', 'z' => '&#37;z', 'Z' => '&#37;Z',
815
			// Time and Date Stamps
816
			'c' => '%F %T', 'D' => '%m/%d/%y', 'F' => '%Y-%m-%d', 's' => '&#37;s', 'x' => '%F',
817
			// Miscellaneous
818
			'n' => "\n", 't' => "\t", '%' => '&#37;',
819
		);
820
821
		// No need to do this part again if we already did it once
822
		if (is_null($unsupportedFormats))
823
			$unsupportedFormats = (array) cache_get_data('unsupportedtimeformats', 86400);
824
		if (empty($unsupportedFormats))
825
		{
826
			foreach ($strftimeFormatSubstitutions as $format => $substitution)
827
			{
828
				// Avoid a crashing bug with PHP 7 on certain versions of Windows
829
				if ($context['server']['is_windows'] && in_array($format, $unsupportedFormatsWindows))
830
				{
831
					$unsupportedFormats[] = $format;
832
					continue;
833
				}
834
835
				$value = @strftime('%' . $format);
836
837
				// Windows will return false for unsupported formats
838
				// Other operating systems return the format string as a literal
839
				if ($value === false || $value === $format)
840
					$unsupportedFormats[] = $format;
841
			}
842
			cache_put_data('unsupportedtimeformats', $unsupportedFormats, 86400);
843
		}
844
845
		// Windows needs extra help if $timeformat contains something completely invalid, e.g. '%Q'
846
		if (DIRECTORY_SEPARATOR === '\\')
847
			$timeformat = preg_replace('~%(?!' . implode('|', array_keys($strftimeFormatSubstitutions)) . ')~', '&#37;', $timeformat);
848
849
		// Substitute unsupported formats with supported ones
850
		if (!empty($unsupportedFormats))
851
			while (preg_match('~%(' . implode('|', $unsupportedFormats) . ')~', $timeformat, $matches))
852
				$timeformat = str_replace($matches[0], $strftimeFormatSubstitutions[$matches[1]], $timeformat);
853
854
		// Remember this so we don't need to do it again
855
		$finalizedFormats[$str] = $timeformat;
856
		cache_put_data('timeformatstrings', $finalizedFormats, 86400);
857
	}
858
859
	$str = $finalizedFormats[$str];
860
861
	if (!isset($locale_cache))
862
		$locale_cache = setlocale(LC_TIME, $txt['lang_locale'] . !empty($modSettings['global_character_set']) ? '.' . $modSettings['global_character_set'] : '');
863
864
	if ($locale_cache !== false)
865
	{
866
		// Check if another process changed the locale
867
		if ($process_safe === true && setlocale(LC_TIME, '0') != $locale_cache)
868
			setlocale(LC_TIME, $txt['lang_locale'] . !empty($modSettings['global_character_set']) ? '.' . $modSettings['global_character_set'] : '');
869
870
		if (!isset($non_twelve_hour))
871
			$non_twelve_hour = trim(strftime('%p')) === '';
872
		if ($non_twelve_hour && strpos($str, '%p') !== false)
873
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
874
875
		foreach (array('%a', '%A', '%b', '%B') as $token)
876
			if (strpos($str, $token) !== false)
877
				$str = str_replace($token, strftime($token, $time), $str);
878
	}
879
	else
880
	{
881
		// Do-it-yourself time localization.  Fun.
882
		foreach (array('%a' => 'days_short', '%A' => 'days', '%b' => 'months_short', '%B' => 'months') as $token => $text_label)
883
			if (strpos($str, $token) !== false)
884
				$str = str_replace($token, $txt[$text_label][(int) strftime($token === '%a' || $token === '%A' ? '%w' : '%m', $time)], $str);
885
886
		if (strpos($str, '%p') !== false)
887
			$str = str_replace('%p', (strftime('%H', $time) < 12 ? $txt['time_am'] : $txt['time_pm']), $str);
888
	}
889
890
	// Format the time and then restore any literal percent characters
891
	return str_replace('&#37;', '%', strftime($str, $time));
892
}
893
894
/**
895
 * Replaces special entities in strings with the real characters.
896
 *
897
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
898
 * replaces '&nbsp;' with a simple space character.
899
 *
900
 * @param string $string A string
901
 * @return string The string without entities
902
 */
903
function un_htmlspecialchars($string)
904
{
905
	global $context;
906
	static $translation = array();
907
908
	// Determine the character set... Default to UTF-8
909
	if (empty($context['character_set']))
910
		$charset = 'UTF-8';
911
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
912
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
913
		$charset = 'ISO-8859-1';
914
	else
915
		$charset = $context['character_set'];
916
917
	if (empty($translation))
918
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
919
920
	return strtr($string, $translation);
921
}
922
923
/**
924
 * Shorten a subject + internationalization concerns.
925
 *
926
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
927
 * - respects internationalization characters and entities as one character.
928
 * - avoids trailing entities.
929
 * - returns the shortened string.
930
 *
931
 * @param string $subject The subject
932
 * @param int $len How many characters to limit it to
933
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
934
 */
935
function shorten_subject($subject, $len)
936
{
937
	global $smcFunc;
938
939
	// It was already short enough!
940
	if ($smcFunc['strlen']($subject) <= $len)
941
		return $subject;
942
943
	// Shorten it by the length it was too long, and strip off junk from the end.
944
	return $smcFunc['substr']($subject, 0, $len) . '...';
945
}
946
947
/**
948
 * Gets the current time with offset.
949
 *
950
 * - always applies the offset in the time_offset setting.
951
 *
952
 * @param bool $use_user_offset Whether to apply the user's offset as well
953
 * @param int $timestamp A timestamp (null to use current time)
954
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
955
 */
956
function forum_time($use_user_offset = true, $timestamp = null)
957
{
958
	global $user_info, $modSettings;
959
960
	if ($timestamp === null)
961
		$timestamp = time();
962
	elseif ($timestamp == 0)
963
		return 0;
964
965
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
966
}
967
968
/**
969
 * Calculates all the possible permutations (orders) of array.
970
 * should not be called on huge arrays (bigger than like 10 elements.)
971
 * returns an array containing each permutation.
972
 *
973
 * @deprecated since 2.1
974
 * @param array $array An array
975
 * @return array An array containing each permutation
976
 */
977
function permute($array)
978
{
979
	$orders = array($array);
980
981
	$n = count($array);
982
	$p = range(0, $n);
983
	for ($i = 1; $i < $n; null)
984
	{
985
		$p[$i]--;
986
		$j = $i % 2 != 0 ? $p[$i] : 0;
987
988
		$temp = $array[$i];
989
		$array[$i] = $array[$j];
990
		$array[$j] = $temp;
991
992
		for ($i = 1; $p[$i] == 0; $i++)
993
			$p[$i] = 1;
994
995
		$orders[] = $array;
996
	}
997
998
	return $orders;
999
}
1000
1001
/**
1002
 * Parse bulletin board code in a string, as well as smileys optionally.
1003
 *
1004
 * - only parses bbc tags which are not disabled in disabledBBC.
1005
 * - handles basic HTML, if enablePostHTML is on.
1006
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1007
 * - only parses smileys if smileys is true.
1008
 * - does nothing if the enableBBC setting is off.
1009
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1010
 * - returns the modified message.
1011
 *
1012
 * @param string $message The message
1013
 * @param bool $smileys Whether to parse smileys as well
1014
 * @param string $cache_id The cache ID
1015
 * @param array $parse_tags If set, only parses these tags rather than all of them
1016
 * @return string The parsed message
1017
 */
1018
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1019
{
1020
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir;
1021
	static $bbc_codes = array(), $itemcodes = array(), $no_autolink_tags = array();
1022
	static $disabled, $alltags_regex = '', $param_regexes = array();
1023
1024
	// Don't waste cycles
1025
	if ($message === '')
1026
		return '';
1027
1028
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1029
	if (!isset($context['utf8']))
1030
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1031
1032
	// Clean up any cut/paste issues we may have
1033
	$message = sanitizeMSCutPaste($message);
1034
1035
	// If the load average is too high, don't parse the BBC.
1036
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1037
	{
1038
		$context['disabled_parse_bbc'] = true;
1039
		return $message;
1040
	}
1041
1042
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1043
		$smileys = (bool) $smileys;
1044
1045
	if (empty($modSettings['enableBBC']) && $message !== false)
1046
	{
1047
		if ($smileys === true)
1048
			parsesmileys($message);
1049
1050
		return $message;
1051
	}
1052
1053
	// If we are not doing every tag then we don't cache this run.
1054
	if (!empty($parse_tags) && !empty($bbc_codes))
1055
	{
1056
		$temp_bbc = $bbc_codes;
1057
		$bbc_codes = array();
1058
	}
1059
1060
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1061
	if (!empty($modSettings['autoLinkUrls']))
1062
		set_tld_regex();
1063
1064
	// Allow mods access before entering the main parse_bbc loop
1065
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1066
1067
	// Sift out the bbc for a performance improvement.
1068
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1069
	{
1070
		if (!empty($modSettings['disabledBBC']))
1071
		{
1072
			$disabled = array();
1073
1074
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1075
1076
			foreach ($temp as $tag)
1077
				$disabled[trim($tag)] = true;
1078
1079
			if (in_array('color', $disabled))
1080
				$disabled = array_merge($disabled, array(
1081
					'black' => true,
1082
					'white' => true,
1083
					'red' => true,
1084
					'green' => true,
1085
					'blue' => true,
1086
					)
1087
				);
1088
		}
1089
1090
		// The YouTube bbc needs this for its origin parameter
1091
		$scripturl_parts = parse_url($scripturl);
1092
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1093
1094
		/* The following bbc are formatted as an array, with keys as follows:
1095
1096
			tag: the tag's name - should be lowercase!
1097
1098
			type: one of...
1099
				- (missing): [tag]parsed content[/tag]
1100
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1101
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1102
				- unparsed_content: [tag]unparsed content[/tag]
1103
				- closed: [tag], [tag/], [tag /]
1104
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1105
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1106
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1107
1108
			parameters: an optional array of parameters, for the form
1109
			  [tag abc=123]content[/tag].  The array is an associative array
1110
			  where the keys are the parameter names, and the values are an
1111
			  array which may contain the following:
1112
				- match: a regular expression to validate and match the value.
1113
				- quoted: true if the value should be quoted.
1114
				- validate: callback to evaluate on the data, which is $data.
1115
				- value: a string in which to replace $1 with the data.
1116
					Either value or validate may be used, not both.
1117
				- optional: true if the parameter is optional.
1118
				- default: a default value for missing optional parameters.
1119
1120
			test: a regular expression to test immediately after the tag's
1121
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1122
			  Optional.
1123
1124
			content: only available for unparsed_content, closed,
1125
			  unparsed_commas_content, and unparsed_equals_content.
1126
			  $1 is replaced with the content of the tag.  Parameters
1127
			  are replaced in the form {param}.  For unparsed_commas_content,
1128
			  $2, $3, ..., $n are replaced.
1129
1130
			before: only when content is not used, to go before any
1131
			  content.  For unparsed_equals, $1 is replaced with the value.
1132
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1133
1134
			after: similar to before in every way, except that it is used
1135
			  when the tag is closed.
1136
1137
			disabled_content: used in place of content when the tag is
1138
			  disabled.  For closed, default is '', otherwise it is '$1' if
1139
			  block_level is false, '<div>$1</div>' elsewise.
1140
1141
			disabled_before: used in place of before when disabled.  Defaults
1142
			  to '<div>' if block_level, '' if not.
1143
1144
			disabled_after: used in place of after when disabled.  Defaults
1145
			  to '</div>' if block_level, '' if not.
1146
1147
			block_level: set to true the tag is a "block level" tag, similar
1148
			  to HTML.  Block level tags cannot be nested inside tags that are
1149
			  not block level, and will not be implicitly closed as easily.
1150
			  One break following a block level tag may also be removed.
1151
1152
			trim: if set, and 'inside' whitespace after the begin tag will be
1153
			  removed.  If set to 'outside', whitespace after the end tag will
1154
			  meet the same fate.
1155
1156
			validate: except when type is missing or 'closed', a callback to
1157
			  validate the data as $data.  Depending on the tag's type, $data
1158
			  may be a string or an array of strings (corresponding to the
1159
			  replacement.)
1160
1161
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1162
			  may be not set, 'optional', or 'required' corresponding to if
1163
			  the content may be quoted.  This allows the parser to read
1164
			  [tag="abc]def[esdf]"] properly.
1165
1166
			require_parents: an array of tag names, or not set.  If set, the
1167
			  enclosing tag *must* be one of the listed tags, or parsing won't
1168
			  occur.
1169
1170
			require_children: similar to require_parents, if set children
1171
			  won't be parsed if they are not in the list.
1172
1173
			disallow_children: similar to, but very different from,
1174
			  require_children, if it is set the listed tags will not be
1175
			  parsed inside the tag.
1176
1177
			parsed_tags_allowed: an array restricting what BBC can be in the
1178
			  parsed_equals parameter, if desired.
1179
		*/
1180
1181
		$codes = array(
1182
			array(
1183
				'tag' => 'abbr',
1184
				'type' => 'unparsed_equals',
1185
				'before' => '<abbr title="$1">',
1186
				'after' => '</abbr>',
1187
				'quoted' => 'optional',
1188
				'disabled_after' => ' ($1)',
1189
			),
1190
			// Legacy (and just an alias for [abbr] even when enabled)
1191
			array(
1192
				'tag' => 'acronym',
1193
				'type' => 'unparsed_equals',
1194
				'before' => '<abbr title="$1">',
1195
				'after' => '</abbr>',
1196
				'quoted' => 'optional',
1197
				'disabled_after' => ' ($1)',
1198
			),
1199
			array(
1200
				'tag' => 'anchor',
1201
				'type' => 'unparsed_equals',
1202
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1203
				'before' => '<span id="post_$1">',
1204
				'after' => '</span>',
1205
			),
1206
			array(
1207
				'tag' => 'attach',
1208
				'type' => 'unparsed_content',
1209
				'parameters' => array(
1210
					'id' => array('match' => '(\d+)'),
1211
					'alt' => array('optional' => true),
1212
					'width' => array('optional' => true, 'match' => '(\d+)'),
1213
					'height' => array('optional' => true, 'match' => '(\d+)'),
1214
				),
1215
				'content' => '$1',
1216
				'validate' => function(&$tag, &$data, $disabled, $params) use ($modSettings, $context, $sourcedir, $txt, $smcFunc)
0 ignored issues
show
Unused Code introduced by
The import $context is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
1217
				{
1218
					$returnContext = '';
1219
1220
					// BBC or the entire attachments feature is disabled
1221
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1222
						return $data;
1223
1224
					// Save the attach ID.
1225
					$attachID = $params['{id}'];
1226
1227
					// Kinda need this.
1228
					require_once($sourcedir . '/Subs-Attachments.php');
1229
1230
					$currentAttachment = parseAttachBBC($attachID);
1231
1232
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1233
					if (is_string($currentAttachment))
1234
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1235
1236
					if (!empty($currentAttachment['is_image']) && (!isset($param['{type}']) || strpos($param['{type}'], 'image') === 0))
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $param does not exist. Did you maybe mean $params?
Loading history...
1237
					{
1238
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1239
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1240
1241
						$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1242
						$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1243
1244
						if (empty($width) && empty($height))
1245
						{
1246
							$width = ' width="' . $currentAttachment['width'] . '"';
1247
							$height = ' height="' . $currentAttachment['height'] . '"';
1248
						}
1249
1250
						if ($currentAttachment['thumbnail']['has_thumb'] && empty($params['{width}']) && empty($params['{height}']))
1251
							$returnContext .= '<a href="' . $currentAttachment['href'] . ';image" id="link_' . $currentAttachment['id'] . '" onclick="' . $currentAttachment['thumbnail']['javascript'] . '"><img src="' . $currentAttachment['thumbnail']['href'] . '"' . $alt . $title . ' id="thumb_' . $currentAttachment['id'] . '" class="atc_img"></a>';
1252
						else
1253
							$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img"/>';
1254
					}
1255
1256
					// No image. Show a link.
1257
					else
1258
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1259
1260
					// Gotta append what we just did.
1261
					$data = $returnContext;
1262
				},
1263
			),
1264
			array(
1265
				'tag' => 'b',
1266
				'before' => '<b>',
1267
				'after' => '</b>',
1268
			),
1269
			// Legacy (equivalent to [ltr] or [rtl])
1270
			array(
1271
				'tag' => 'bdo',
1272
				'type' => 'unparsed_equals',
1273
				'before' => '<bdo dir="$1">',
1274
				'after' => '</bdo>',
1275
				'test' => '(rtl|ltr)\]',
1276
				'block_level' => true,
1277
			),
1278
			// Legacy (alias of [color=black])
1279
			array(
1280
				'tag' => 'black',
1281
				'before' => '<span style="color: black;" class="bbc_color">',
1282
				'after' => '</span>',
1283
			),
1284
			// Legacy (alias of [color=blue])
1285
			array(
1286
				'tag' => 'blue',
1287
				'before' => '<span style="color: blue;" class="bbc_color">',
1288
				'after' => '</span>',
1289
			),
1290
			array(
1291
				'tag' => 'br',
1292
				'type' => 'closed',
1293
				'content' => '<br>',
1294
			),
1295
			array(
1296
				'tag' => 'center',
1297
				'before' => '<div class="centertext">',
1298
				'after' => '</div>',
1299
				'block_level' => true,
1300
			),
1301
			array(
1302
				'tag' => 'code',
1303
				'type' => 'unparsed_content',
1304
				'content' => '<div class="codeheader"><span class="code floatleft">' . $txt['code'] . '</span> <a class="codeoperation smf_select_text">' . $txt['code_select'] . '</a> <a class="codeoperation smf_expand_code hidden" data-shrink-txt="' . $txt['code_shrink'] . '" data-expand-txt="' . $txt['code_expand'] . '">' . $txt['code_expand'] . '</a></div><code class="bbc_code">$1</code>',
1305
				// @todo Maybe this can be simplified?
1306
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1307
				{
1308
					if (!isset($disabled['code']))
1309
					{
1310
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1311
1312
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1312
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1313
						{
1314
							// Do PHP code coloring?
1315
							if ($php_parts[$php_i] != '&lt;?php')
1316
								continue;
1317
1318
							$php_string = '';
1319
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1320
							{
1321
								$php_string .= $php_parts[$php_i];
1322
								$php_parts[$php_i++] = '';
1323
							}
1324
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1325
						}
1326
1327
						// Fix the PHP code stuff...
1328
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1328
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1329
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1330
1331
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1332
						if (!empty($context['browser']['is_opera']))
1333
							$data .= '&nbsp;';
1334
					}
1335
				},
1336
				'block_level' => true,
1337
			),
1338
			array(
1339
				'tag' => 'code',
1340
				'type' => 'unparsed_equals_content',
1341
				'content' => '<div class="codeheader"><span class="code floatleft">' . $txt['code'] . '</span> ($2) <a class="codeoperation smf_select_text">' . $txt['code_select'] . '</a> <a class="codeoperation smf_expand_code hidden" data-shrink-txt="' . $txt['code_shrink'] . '" data-expand-txt="' . $txt['code_expand'] . '">' . $txt['code_expand'] . '</a></div><code class="bbc_code">$1</code>',
1342
				// @todo Maybe this can be simplified?
1343
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1344
				{
1345
					if (!isset($disabled['code']))
1346
					{
1347
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1348
1349
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1349
						for ($php_i = 0, $php_n = count(/** @scrutinizer ignore-type */ $php_parts); $php_i < $php_n; $php_i++)
Loading history...
1350
						{
1351
							// Do PHP code coloring?
1352
							if ($php_parts[$php_i] != '&lt;?php')
1353
								continue;
1354
1355
							$php_string = '';
1356
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1357
							{
1358
								$php_string .= $php_parts[$php_i];
1359
								$php_parts[$php_i++] = '';
1360
							}
1361
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1362
						}
1363
1364
						// Fix the PHP code stuff...
1365
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
0 ignored issues
show
Bug introduced by
It seems like $php_parts can also be of type false; however, parameter $pieces of implode() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1365
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1366
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1367
1368
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1369
						if (!empty($context['browser']['is_opera']))
1370
							$data[0] .= '&nbsp;';
1371
					}
1372
				},
1373
				'block_level' => true,
1374
			),
1375
			array(
1376
				'tag' => 'color',
1377
				'type' => 'unparsed_equals',
1378
				'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]?)\))\]',
1379
				'before' => '<span style="color: $1;" class="bbc_color">',
1380
				'after' => '</span>',
1381
			),
1382
			array(
1383
				'tag' => 'email',
1384
				'type' => 'unparsed_content',
1385
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1386
				// @todo Should this respect guest_hideContacts?
1387
				'validate' => function(&$tag, &$data, $disabled)
1388
				{
1389
					$data = strtr($data, array('<br>' => ''));
1390
				},
1391
			),
1392
			array(
1393
				'tag' => 'email',
1394
				'type' => 'unparsed_equals',
1395
				'before' => '<a href="mailto:$1" class="bbc_email">',
1396
				'after' => '</a>',
1397
				// @todo Should this respect guest_hideContacts?
1398
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1399
				'disabled_after' => ' ($1)',
1400
			),
1401
			// Legacy (and just a link even when not disabled)
1402
			array(
1403
				'tag' => 'flash',
1404
				'type' => 'unparsed_commas_content',
1405
				'test' => '\d+,\d+\]',
1406
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1407
				'validate' => function (&$tag, &$data, $disabled)
1408
				{
1409
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1410
					if (empty($scheme))
1411
						$data[0] = '//' . ltrim($data[0], ':/');
1412
				},
1413
			),
1414
			array(
1415
				'tag' => 'float',
1416
				'type' => 'unparsed_equals',
1417
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1418
				'before' => '<div $1>',
1419
				'after' => '</div>',
1420
				'validate' => function(&$tag, &$data, $disabled)
1421
				{
1422
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1423
1424
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1425
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1426
					else
1427
						$css = '';
1428
1429
					$data = $class . $css;
1430
				},
1431
				'trim' => 'outside',
1432
				'block_level' => true,
1433
			),
1434
			// Legacy (alias of [url] with an FTP URL)
1435
			array(
1436
				'tag' => 'ftp',
1437
				'type' => 'unparsed_content',
1438
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1439
				'validate' => function(&$tag, &$data, $disabled)
1440
				{
1441
					$data = strtr($data, array('<br>' => ''));
1442
					$scheme = parse_url($data, PHP_URL_SCHEME);
1443
					if (empty($scheme))
1444
						$data = 'ftp://' . ltrim($data, ':/');
1445
				},
1446
			),
1447
			// Legacy (alias of [url] with an FTP URL)
1448
			array(
1449
				'tag' => 'ftp',
1450
				'type' => 'unparsed_equals',
1451
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1452
				'after' => '</a>',
1453
				'validate' => function(&$tag, &$data, $disabled)
1454
				{
1455
					$scheme = parse_url($data, PHP_URL_SCHEME);
1456
					if (empty($scheme))
1457
						$data = 'ftp://' . ltrim($data, ':/');
1458
				},
1459
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1460
				'disabled_after' => ' ($1)',
1461
			),
1462
			array(
1463
				'tag' => 'font',
1464
				'type' => 'unparsed_equals',
1465
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1466
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1467
				'after' => '</span>',
1468
			),
1469
			// Legacy (one of those things that should not be done)
1470
			array(
1471
				'tag' => 'glow',
1472
				'type' => 'unparsed_commas',
1473
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1474
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1475
				'after' => '</span>',
1476
			),
1477
			// Legacy (alias of [color=green])
1478
			array(
1479
				'tag' => 'green',
1480
				'before' => '<span style="color: green;" class="bbc_color">',
1481
				'after' => '</span>',
1482
			),
1483
			array(
1484
				'tag' => 'html',
1485
				'type' => 'unparsed_content',
1486
				'content' => '<div>$1</div>',
1487
				'block_level' => true,
1488
				'disabled_content' => '$1',
1489
			),
1490
			array(
1491
				'tag' => 'hr',
1492
				'type' => 'closed',
1493
				'content' => '<hr>',
1494
				'block_level' => true,
1495
			),
1496
			array(
1497
				'tag' => 'i',
1498
				'before' => '<i>',
1499
				'after' => '</i>',
1500
			),
1501
			array(
1502
				'tag' => 'img',
1503
				'type' => 'unparsed_content',
1504
				'parameters' => array(
1505
					'alt' => array('optional' => true),
1506
					'title' => array('optional' => true),
1507
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1508
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1509
				),
1510
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized">',
1511
				'validate' => function(&$tag, &$data, $disabled)
1512
				{
1513
					global $image_proxy_enabled, $user_info;
1514
1515
					$data = strtr($data, array('<br>' => ''));
1516
					$scheme = parse_url($data, PHP_URL_SCHEME);
1517
					if ($image_proxy_enabled)
1518
					{
1519
						if (!empty($user_info['possibly_robot']))
1520
							return;
1521
1522
						if (empty($scheme))
1523
							$data = 'http://' . ltrim($data, ':/');
1524
1525
						if ($scheme != 'https')
1526
							$data = get_proxied_url($data);
1527
					}
1528
					elseif (empty($scheme))
1529
						$data = '//' . ltrim($data, ':/');
1530
				},
1531
				'disabled_content' => '($1)',
1532
			),
1533
			array(
1534
				'tag' => 'img',
1535
				'type' => 'unparsed_content',
1536
				'content' => '<img src="$1" alt="" class="bbc_img">',
1537
				'validate' => function(&$tag, &$data, $disabled)
1538
				{
1539
					global $image_proxy_enabled, $user_info;
1540
1541
					$data = strtr($data, array('<br>' => ''));
1542
					$scheme = parse_url($data, PHP_URL_SCHEME);
1543
					if ($image_proxy_enabled)
1544
					{
1545
						if (!empty($user_info['possibly_robot']))
1546
							return;
1547
1548
						if (empty($scheme))
1549
							$data = 'http://' . ltrim($data, ':/');
1550
1551
						if ($scheme != 'https')
1552
							$data = get_proxied_url($data);
1553
					}
1554
					elseif (empty($scheme))
1555
						$data = '//' . ltrim($data, ':/');
1556
				},
1557
				'disabled_content' => '($1)',
1558
			),
1559
			array(
1560
				'tag' => 'iurl',
1561
				'type' => 'unparsed_content',
1562
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1563
				'validate' => function(&$tag, &$data, $disabled)
1564
				{
1565
					$data = strtr($data, array('<br>' => ''));
1566
					$scheme = parse_url($data, PHP_URL_SCHEME);
1567
					if (empty($scheme))
1568
						$data = '//' . ltrim($data, ':/');
1569
				},
1570
			),
1571
			array(
1572
				'tag' => 'iurl',
1573
				'type' => 'unparsed_equals',
1574
				'quoted' => 'optional',
1575
				'before' => '<a href="$1" class="bbc_link">',
1576
				'after' => '</a>',
1577
				'validate' => function(&$tag, &$data, $disabled)
1578
				{
1579
					if (substr($data, 0, 1) == '#')
1580
						$data = '#post_' . substr($data, 1);
1581
					else
1582
					{
1583
						$scheme = parse_url($data, PHP_URL_SCHEME);
1584
						if (empty($scheme))
1585
							$data = '//' . ltrim($data, ':/');
1586
					}
1587
				},
1588
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1589
				'disabled_after' => ' ($1)',
1590
			),
1591
			array(
1592
				'tag' => 'justify',
1593
				'before' => '<div style="text-align: justify;">',
1594
				'after' => '</div>',
1595
				'block_level' => true,
1596
			),
1597
			array(
1598
				'tag' => 'left',
1599
				'before' => '<div style="text-align: left;">',
1600
				'after' => '</div>',
1601
				'block_level' => true,
1602
			),
1603
			array(
1604
				'tag' => 'li',
1605
				'before' => '<li>',
1606
				'after' => '</li>',
1607
				'trim' => 'outside',
1608
				'require_parents' => array('list'),
1609
				'block_level' => true,
1610
				'disabled_before' => '',
1611
				'disabled_after' => '<br>',
1612
			),
1613
			array(
1614
				'tag' => 'list',
1615
				'before' => '<ul class="bbc_list">',
1616
				'after' => '</ul>',
1617
				'trim' => 'inside',
1618
				'require_children' => array('li', 'list'),
1619
				'block_level' => true,
1620
			),
1621
			array(
1622
				'tag' => 'list',
1623
				'parameters' => array(
1624
					'type' => array('match' => '(none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|upper-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'),
1625
				),
1626
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1627
				'after' => '</ul>',
1628
				'trim' => 'inside',
1629
				'require_children' => array('li'),
1630
				'block_level' => true,
1631
			),
1632
			array(
1633
				'tag' => 'ltr',
1634
				'before' => '<bdo dir="ltr">',
1635
				'after' => '</bdo>',
1636
				'block_level' => true,
1637
			),
1638
			array(
1639
				'tag' => 'me',
1640
				'type' => 'unparsed_equals',
1641
				'before' => '<div class="meaction">* $1 ',
1642
				'after' => '</div>',
1643
				'quoted' => 'optional',
1644
				'block_level' => true,
1645
				'disabled_before' => '/me ',
1646
				'disabled_after' => '<br>',
1647
			),
1648
			array(
1649
				'tag' => 'member',
1650
				'type' => 'unparsed_equals',
1651
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1652
				'after' => '</a>',
1653
			),
1654
			// Legacy (horrible memories of the 1990s)
1655
			array(
1656
				'tag' => 'move',
1657
				'before' => '<marquee>',
1658
				'after' => '</marquee>',
1659
				'block_level' => true,
1660
				'disallow_children' => array('move'),
1661
			),
1662
			array(
1663
				'tag' => 'nobbc',
1664
				'type' => 'unparsed_content',
1665
				'content' => '$1',
1666
			),
1667
			array(
1668
				'tag' => 'php',
1669
				'type' => 'unparsed_content',
1670
				'content' => '<span class="phpcode">$1</span>',
1671
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
1672
				{
1673
					if (!isset($disabled['php']))
1674
					{
1675
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1676
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1677
						if ($add_begin)
1678
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1679
					}
1680
				},
1681
				'block_level' => false,
1682
				'disabled_content' => '$1',
1683
			),
1684
			array(
1685
				'tag' => 'pre',
1686
				'before' => '<pre>',
1687
				'after' => '</pre>',
1688
			),
1689
			array(
1690
				'tag' => 'quote',
1691
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1692
				'after' => '</blockquote>',
1693
				'trim' => 'both',
1694
				'block_level' => true,
1695
			),
1696
			array(
1697
				'tag' => 'quote',
1698
				'parameters' => array(
1699
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1700
				),
1701
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1702
				'after' => '</blockquote>',
1703
				'trim' => 'both',
1704
				'block_level' => true,
1705
			),
1706
			array(
1707
				'tag' => 'quote',
1708
				'type' => 'parsed_equals',
1709
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1710
				'after' => '</blockquote>',
1711
				'trim' => 'both',
1712
				'quoted' => 'optional',
1713
				// Don't allow everything to be embedded with the author name.
1714
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1715
				'block_level' => true,
1716
			),
1717
			array(
1718
				'tag' => 'quote',
1719
				'parameters' => array(
1720
					'author' => array('match' => '([^<>]{1,192}?)'),
1721
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1722
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1723
				),
1724
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1725
				'after' => '</blockquote>',
1726
				'trim' => 'both',
1727
				'block_level' => true,
1728
			),
1729
			array(
1730
				'tag' => 'quote',
1731
				'parameters' => array(
1732
					'author' => array('match' => '(.{1,192}?)'),
1733
				),
1734
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1735
				'after' => '</blockquote>',
1736
				'trim' => 'both',
1737
				'block_level' => true,
1738
			),
1739
			// Legacy (alias of [color=red])
1740
			array(
1741
				'tag' => 'red',
1742
				'before' => '<span style="color: red;" class="bbc_color">',
1743
				'after' => '</span>',
1744
			),
1745
			array(
1746
				'tag' => 'right',
1747
				'before' => '<div style="text-align: right;">',
1748
				'after' => '</div>',
1749
				'block_level' => true,
1750
			),
1751
			array(
1752
				'tag' => 'rtl',
1753
				'before' => '<bdo dir="rtl">',
1754
				'after' => '</bdo>',
1755
				'block_level' => true,
1756
			),
1757
			array(
1758
				'tag' => 's',
1759
				'before' => '<s>',
1760
				'after' => '</s>',
1761
			),
1762
			// Legacy (never a good idea)
1763
			array(
1764
				'tag' => 'shadow',
1765
				'type' => 'unparsed_commas',
1766
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
1767
				'before' => '<span style="text-shadow: $1 $2">',
1768
				'after' => '</span>',
1769
				'validate' => function(&$tag, &$data, $disabled)
1770
				{
1771
1772
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
1773
						$data[1] = '0 -2px 1px';
1774
1775
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
1776
						$data[1] = '2px 0 1px';
1777
1778
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
1779
						$data[1] = '0 2px 1px';
1780
1781
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
1782
						$data[1] = '-2px 0 1px';
1783
1784
					else
1785
						$data[1] = '1px 1px 1px';
1786
				},
1787
			),
1788
			array(
1789
				'tag' => 'size',
1790
				'type' => 'unparsed_equals',
1791
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
1792
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1793
				'after' => '</span>',
1794
			),
1795
			array(
1796
				'tag' => 'size',
1797
				'type' => 'unparsed_equals',
1798
				'test' => '[1-7]\]',
1799
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1800
				'after' => '</span>',
1801
				'validate' => function(&$tag, &$data, $disabled)
1802
				{
1803
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
1804
					$data = $sizes[$data] . 'em';
1805
				},
1806
			),
1807
			array(
1808
				'tag' => 'sub',
1809
				'before' => '<sub>',
1810
				'after' => '</sub>',
1811
			),
1812
			array(
1813
				'tag' => 'sup',
1814
				'before' => '<sup>',
1815
				'after' => '</sup>',
1816
			),
1817
			array(
1818
				'tag' => 'table',
1819
				'before' => '<table class="bbc_table">',
1820
				'after' => '</table>',
1821
				'trim' => 'inside',
1822
				'require_children' => array('tr'),
1823
				'block_level' => true,
1824
			),
1825
			array(
1826
				'tag' => 'td',
1827
				'before' => '<td>',
1828
				'after' => '</td>',
1829
				'require_parents' => array('tr'),
1830
				'trim' => 'outside',
1831
				'block_level' => true,
1832
				'disabled_before' => '',
1833
				'disabled_after' => '',
1834
			),
1835
			array(
1836
				'tag' => 'time',
1837
				'type' => 'unparsed_content',
1838
				'content' => '$1',
1839
				'validate' => function(&$tag, &$data, $disabled)
1840
				{
1841
					if (is_numeric($data))
1842
						$data = timeformat($data);
1843
					else
1844
						$tag['content'] = '[time]$1[/time]';
1845
				},
1846
			),
1847
			array(
1848
				'tag' => 'tr',
1849
				'before' => '<tr>',
1850
				'after' => '</tr>',
1851
				'require_parents' => array('table'),
1852
				'require_children' => array('td'),
1853
				'trim' => 'both',
1854
				'block_level' => true,
1855
				'disabled_before' => '',
1856
				'disabled_after' => '',
1857
			),
1858
			// Legacy (the <tt> element is dead)
1859
			array(
1860
				'tag' => 'tt',
1861
				'before' => '<span class="monospace">',
1862
				'after' => '</span>',
1863
			),
1864
			array(
1865
				'tag' => 'u',
1866
				'before' => '<u>',
1867
				'after' => '</u>',
1868
			),
1869
			array(
1870
				'tag' => 'url',
1871
				'type' => 'unparsed_content',
1872
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1873
				'validate' => function(&$tag, &$data, $disabled)
1874
				{
1875
					$data = strtr($data, array('<br>' => ''));
1876
					$scheme = parse_url($data, PHP_URL_SCHEME);
1877
					if (empty($scheme))
1878
						$data = '//' . ltrim($data, ':/');
1879
				},
1880
			),
1881
			array(
1882
				'tag' => 'url',
1883
				'type' => 'unparsed_equals',
1884
				'quoted' => 'optional',
1885
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1886
				'after' => '</a>',
1887
				'validate' => function(&$tag, &$data, $disabled)
1888
				{
1889
					$scheme = parse_url($data, PHP_URL_SCHEME);
1890
					if (empty($scheme))
1891
						$data = '//' . ltrim($data, ':/');
1892
				},
1893
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1894
				'disabled_after' => ' ($1)',
1895
			),
1896
			// Legacy (alias of [color=white])
1897
			array(
1898
				'tag' => 'white',
1899
				'before' => '<span style="color: white;" class="bbc_color">',
1900
				'after' => '</span>',
1901
			),
1902
			array(
1903
				'tag' => 'youtube',
1904
				'type' => 'unparsed_content',
1905
				'content' => '<div class="videocontainer"><div><iframe frameborder="0" src="https://www.youtube.com/embed/$1?origin=' . $hosturl . '&wmode=opaque" data-youtube-id="$1" allowfullscreen></iframe></div></div>',
1906
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
1907
				'block_level' => true,
1908
			),
1909
		);
1910
1911
		// Inside these tags autolink is not recommendable.
1912
		$no_autolink_tags = array(
1913
			'url',
1914
			'iurl',
1915
			'email',
1916
			'img',
1917
			'html',
1918
		);
1919
1920
		// Let mods add new BBC without hassle.
1921
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
1922
1923
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
1924
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
1925
		{
1926
			if (isset($temp_bbc))
1927
				$bbc_codes = $temp_bbc;
1928
			usort($codes, function($a, $b)
1929
			{
1930
				return strcmp($a['tag'], $b['tag']);
1931
			});
1932
			return $codes;
1933
		}
1934
1935
		// So the parser won't skip them.
1936
		$itemcodes = array(
1937
			'*' => 'disc',
1938
			'@' => 'disc',
1939
			'+' => 'square',
1940
			'x' => 'square',
1941
			'#' => 'square',
1942
			'o' => 'circle',
1943
			'O' => 'circle',
1944
			'0' => 'circle',
1945
		);
1946
		if (!isset($disabled['li']) && !isset($disabled['list']))
1947
		{
1948
			foreach ($itemcodes as $c => $dummy)
1949
				$bbc_codes[$c] = array();
1950
		}
1951
1952
		// Shhhh!
1953
		if (!isset($disabled['color']))
1954
		{
1955
			$codes[] = array(
1956
				'tag' => 'chrissy',
1957
				'before' => '<span style="color: #cc0099;">',
1958
				'after' => ' :-*</span>',
1959
			);
1960
			$codes[] = array(
1961
				'tag' => 'kissy',
1962
				'before' => '<span style="color: #cc0099;">',
1963
				'after' => ' :-*</span>',
1964
			);
1965
		}
1966
		$codes[] = array(
1967
			'tag' => 'cowsay',
1968
			'parameters' => array(
1969
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
1970
					{
1971
						static $css_added;
1972
1973
						if (empty($css_added))
1974
						{
1975
							$css = base64_decode('cHJlW2RhdGEtZV1bZGF0YS10XXt3aGl0ZS1zcGFjZTpwcmUtd3JhcDtsaW5lLWhlaWdodDppbml0aWFsO31wcmVbZGF0YS1lXVtkYXRhLXRdID4gZGl2e2Rpc3BsYXk6dGFibGU7Ym9yZGVyOjFweCBzb2xpZDtib3JkZXItcmFkaXVzOjAuNWVtO3BhZGRpbmc6MWNoO21heC13aWR0aDo4MGNoO21pbi13aWR0aDoxMmNoO31wcmVbZGF0YS1lXVtkYXRhLXRdOjphZnRlcntkaXNwbGF5OmlubGluZS1ibG9jazttYXJnaW4tbGVmdDo4Y2g7bWluLXdpZHRoOjIwY2g7ZGlyZWN0aW9uOmx0cjtjb250ZW50OidcNUMgICBeX19eXEEgIFw1QyAgKCcgYXR0cihkYXRhLWUpICcpXDVDX19fX19fX1xBICAgIChfXylcNUMgICAgICAgIClcNUMvXDVDXEEgICAgICcgYXR0cihkYXRhLXQpICcgfHwtLS0tdyB8XEEgICAgICAgIHx8ICAgICB8fCc7fQ==');
1976
1977
							addInlineJavaScript('
1978
								$("head").append("<style>" + ' . JavaScriptEscape($css) . ' + "</style>");', true);
1979
1980
							$css_added = true;
1981
						}
1982
1983
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
1984
					},
1985
				),
1986
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
1987
					{
1988
						return $smcFunc['substr']($tongue . '  ', 0, 2);
1989
					},
1990
				),
1991
			),
1992
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
1993
			'after' => '</div></pre>',
1994
			'block_level' => true,
1995
		);
1996
1997
		foreach ($codes as $code)
1998
		{
1999
			// Make it easier to process parameters later
2000
			if (!empty($code['parameters']))
2001
				ksort($code['parameters'], SORT_STRING);
2002
2003
			// If we are not doing every tag only do ones we are interested in.
2004
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2005
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2006
		}
2007
		$codes = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
2008
	}
2009
2010
	// Shall we take the time to cache this?
2011
	if ($cache_id != '' && !empty($modSettings['cache_enable']) && (($modSettings['cache_enable'] >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2012
	{
2013
		// It's likely this will change if the message is modified.
2014
		$cache_key = 'parse:' . $cache_id . '-' . md5(md5($message) . '-' . $smileys . (empty($disabled) ? '' : implode(',', array_keys($disabled))) . $smcFunc['json_encode']($context['browser']) . $txt['lang_locale'] . $user_info['time_offset'] . $user_info['time_format']);
2015
2016
		if (($temp = cache_get_data($cache_key, 240)) != null)
2017
			return $temp;
2018
2019
		$cache_t = microtime(true);
2020
	}
2021
2022
	if ($smileys === 'print')
0 ignored issues
show
introduced by
The condition $smileys === 'print' is always false.
Loading history...
2023
	{
2024
		// [glow], [shadow], and [move] can't really be printed.
2025
		$disabled['glow'] = true;
2026
		$disabled['shadow'] = true;
2027
		$disabled['move'] = true;
2028
2029
		// Colors can't well be displayed... supposed to be black and white.
2030
		$disabled['color'] = true;
2031
		$disabled['black'] = true;
2032
		$disabled['blue'] = true;
2033
		$disabled['white'] = true;
2034
		$disabled['red'] = true;
2035
		$disabled['green'] = true;
2036
		$disabled['me'] = true;
2037
2038
		// Color coding doesn't make sense.
2039
		$disabled['php'] = true;
2040
2041
		// Links are useless on paper... just show the link.
2042
		$disabled['ftp'] = true;
2043
		$disabled['url'] = true;
2044
		$disabled['iurl'] = true;
2045
		$disabled['email'] = true;
2046
		$disabled['flash'] = true;
2047
2048
		// @todo Change maybe?
2049
		if (!isset($_GET['images']))
2050
			$disabled['img'] = true;
2051
2052
		// Maybe some custom BBC need to be disabled for printing.
2053
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2054
	}
2055
2056
	$open_tags = array();
2057
	$message = strtr($message, array("\n" => '<br>'));
2058
2059
	if (!empty($parse_tags))
2060
	{
2061
		$real_alltags_regex = $alltags_regex;
2062
		$alltags_regex = '';
2063
	}
2064
	if (empty($alltags_regex))
2065
	{
2066
		$alltags = array();
2067
		foreach ($bbc_codes as $section)
2068
		{
2069
			foreach ($section as $code)
2070
				$alltags[] = $code['tag'];
2071
		}
2072
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
2073
	}
2074
2075
	$pos = -1;
2076
	while ($pos !== false)
2077
	{
2078
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2079
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2080
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2081
2082
		// Failsafe.
2083
		if ($pos === false || $last_pos > $pos)
2084
			$pos = strlen($message) + 1;
2085
2086
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2087
		if ($last_pos < $pos - 1)
2088
		{
2089
			// Make sure the $last_pos is not negative.
2090
			$last_pos = max($last_pos, 0);
2091
2092
			// Pick a block of data to do some raw fixing on.
2093
			$data = substr($message, $last_pos, $pos - $last_pos);
2094
2095
			$placeholders = array();
2096
			$placeholders_counter = 0;
2097
2098
			// Take care of some HTML!
2099
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2100
			{
2101
				$data = preg_replace('~&lt;a\s+href=((?:&quot;)?)((?:https?://|ftps?://|mailto:|tel:)\S+?)\\1&gt;(.*?)&lt;/a&gt;~i', '[url=&quot;$2&quot;]$3[/url]', $data);
2102
2103
				// <br> should be empty.
2104
				$empty_tags = array('br', 'hr');
2105
				foreach ($empty_tags as $tag)
2106
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2107
2108
				// b, u, i, s, pre... basic tags.
2109
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2110
				foreach ($closable_tags as $tag)
2111
				{
2112
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2113
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2114
2115
					if ($diff > 0)
2116
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2117
				}
2118
2119
				// Do <img ...> - with security... action= -> action-.
2120
				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);
2121
				if (!empty($matches[0]))
2122
				{
2123
					$replaces = array();
2124
					foreach ($matches[2] as $match => $imgtag)
2125
					{
2126
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2127
2128
						// Remove action= from the URL - no funny business, now.
2129
						// @todo Testing this preg_match seems pointless
2130
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2131
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2132
2133
						$placeholder = '<placeholder ' . ++$placeholders_counter . '>';
2134
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2135
2136
						$replaces[$matches[0][$match]] = $placeholder;
2137
					}
2138
2139
					$data = strtr($data, $replaces);
2140
				}
2141
			}
2142
2143
			if (!empty($modSettings['autoLinkUrls']))
2144
			{
2145
				// Are we inside tags that should be auto linked?
2146
				$no_autolink_area = false;
2147
				if (!empty($open_tags))
2148
				{
2149
					foreach ($open_tags as $open_tag)
2150
						if (in_array($open_tag['tag'], $no_autolink_tags))
2151
							$no_autolink_area = true;
2152
				}
2153
2154
				// Don't go backwards.
2155
				// @todo Don't think is the real solution....
2156
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2157
				if ($pos < $lastAutoPos)
2158
					$no_autolink_area = true;
2159
				$lastAutoPos = $pos;
2160
2161
				if (!$no_autolink_area)
2162
				{
2163
					// An &nbsp; right after a URL can break the autolinker
2164
					if (strpos($data, '&nbsp;') !== false)
2165
					{
2166
						$placeholders['<placeholder non-breaking-space>'] = '&nbsp;';
2167
						$data = strtr($data, array('&nbsp;' => '<placeholder non-breaking-space>'));
2168
					}
2169
2170
					// Parse any URLs
2171
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2172
					{
2173
						$url_regex = '
2174
						(?:
2175
							# IRIs with a scheme (or at least an opening "//")
2176
							(?:
2177
								# URI scheme (or lack thereof for schemeless URLs)
2178
								(?:
2179
									# URL scheme and colon
2180
									\b[a-z][\w\-]+:
2181
									| # or
2182
									# A boundary followed by two slashes for schemeless URLs
2183
									(?<=^|\W)(?=//)
2184
								)
2185
2186
								# IRI "authority" chunk
2187
								(?:
2188
									# 2 slashes for IRIs with an "authority"
2189
									//
2190
									# then a domain name
2191
									(?:
2192
										# Either the reserved "localhost" domain name
2193
										localhost
2194
										| # or
2195
										# a run of Unicode domain name characters and a dot
2196
										[\p{L}\p{M}\p{N}\-.:@]+\.
2197
										# and then a TLD valid in the DNS or the reserved "local" TLD
2198
										(?:' . $modSettings['tld_regex'] . '|local)
2199
									)
2200
									# followed by a non-domain character or end of line
2201
									(?=[^\p{L}\p{N}\-.]|$)
2202
2203
									| # Or, if there is no "authority" per se (e.g. mailto: URLs) ...
2204
2205
									# a run of IRI characters
2206
									[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]
2207
									# and then a dot and a closing IRI label
2208
									\.[\p{L}\p{M}\p{N}\-]+
2209
								)
2210
							)
2211
2212
							| # or
2213
2214
							# Naked domains (e.g. "example.com" in "Go to example.com for an example.")
2215
							(?:
2216
								# Preceded by start of line or a non-domain character
2217
								(?<=^|[^\p{L}\p{M}\p{N}\-:@])
2218
2219
								# A run of Unicode domain name characters (excluding [:@])
2220
								[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]
2221
								# and then a dot and a valid TLD
2222
								\.' . $modSettings['tld_regex'] . '
2223
2224
								# Followed by either:
2225
								(?=
2226
									# end of line or a non-domain character (excluding [.:@])
2227
									$|[^\p{L}\p{N}\-]
2228
									| # or
2229
									# a dot followed by end of line or a non-domain character (excluding [.:@])
2230
									\.(?=$|[^\p{L}\p{N}\-])
2231
								)
2232
							)
2233
						)
2234
2235
						# IRI path, query, and fragment (if present)
2236
						(?:
2237
							# If any of these parts exist, must start with a single /
2238
							/
2239
2240
							# And then optionally:
2241
							(?:
2242
								# One or more of:
2243
								(?:
2244
									# a run of non-space, non-()<>
2245
									[^\s()<>]+
2246
									| # or
2247
									# balanced parens, up to 2 levels
2248
									\(([^\s()<>]+|(\([^\s()<>]+\)))*\)
2249
								)+
2250
2251
								# End with:
2252
								(?:
2253
									# balanced parens, up to 2 levels
2254
									\(([^\s()<>]+|(\([^\s()<>]+\)))*\)
2255
									| # or
2256
									# not a space or one of these punct char
2257
									[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]
2258
									| # or
2259
									# a trailing slash (but not two in a row)
2260
									(?<!/)/
2261
								)
2262
							)?
2263
						)?
2264
						';
2265
2266
						$data = preg_replace_callback('~' . $url_regex . '~xi' . ($context['utf8'] ? 'u' : ''), function($matches)
2267
						{
2268
							$url = array_shift($matches);
2269
2270
							// If this isn't a clean URL, bail out
2271
							if ($url != sanitize_iri($url))
2272
								return $url;
2273
2274
							$scheme = parse_url($url, PHP_URL_SCHEME);
2275
2276
							if ($scheme == 'mailto')
2277
							{
2278
								$email_address = str_replace('mailto:', '', $url);
2279
								if (!isset($disabled['email']) && filter_var($email_address, FILTER_VALIDATE_EMAIL) !== false)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $disabled seems to never exist and therefore isset should always be false.
Loading history...
2280
									return '[email=' . $email_address . ']' . $url . '[/email]';
2281
								else
2282
									return $url;
2283
							}
2284
2285
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2286
							if (empty($scheme))
2287
								$fullUrl = '//' . ltrim($url, ':/');
2288
							else
2289
								$fullUrl = $url;
2290
2291
							// Make sure that $fullUrl really is valid
2292
							if (validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false)
2293
								return $url;
2294
2295
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2296
						}, $data);
2297
					}
2298
2299
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
2300
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
2301
					{
2302
						$email_regex = '
2303
						# Preceded by a non-domain character or start of line
2304
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
2305
2306
						# An email address
2307
						[\p{L}\p{M}\p{N}_\-.]{1,80}
2308
						@
2309
						[\p{L}\p{M}\p{N}\-.]+
2310
						\.
2311
						' . $modSettings['tld_regex'] . '
2312
2313
						# Followed by either:
2314
						(?=
2315
							# end of line or a non-domain character (excluding the dot)
2316
							$|[^\p{L}\p{M}\p{N}\-]
2317
							| # or
2318
							# a dot followed by end of line or a non-domain character
2319
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2320
						)';
2321
2322
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2323
					}
2324
				}
2325
			}
2326
2327
			// Restore any placeholders
2328
			$data = strtr($data, $placeholders);
2329
2330
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2331
2332
			// If it wasn't changed, no copying or other boring stuff has to happen!
2333
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2334
			{
2335
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2336
2337
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2338
				$old_pos = strlen($data) + $last_pos;
2339
				$pos = strpos($message, '[', $last_pos);
2340
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2341
			}
2342
		}
2343
2344
		// Are we there yet?  Are we there yet?
2345
		if ($pos >= strlen($message) - 1)
2346
			break;
2347
2348
		$tag_character = strtolower($message[$pos + 1]);
2349
2350
		if ($tag_character == '/' && !empty($open_tags))
2351
		{
2352
			$pos2 = strpos($message, ']', $pos + 1);
2353
			if ($pos2 == $pos + 2)
2354
				continue;
2355
2356
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2357
2358
			// A closing tag that doesn't match any open tags? Skip it.
2359
			if (!in_array($look_for, array_map(function($code)
2360
			{
2361
				return $code['tag'];
2362
			}, $open_tags)))
2363
				continue;
2364
2365
			$to_close = array();
2366
			$block_level = null;
2367
2368
			do
2369
			{
2370
				$tag = array_pop($open_tags);
2371
				if (!$tag)
2372
					break;
2373
2374
				if (!empty($tag['block_level']))
2375
				{
2376
					// Only find out if we need to.
2377
					if ($block_level === false)
2378
					{
2379
						array_push($open_tags, $tag);
2380
						break;
2381
					}
2382
2383
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2384
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2385
					{
2386
						foreach ($bbc_codes[$look_for[0]] as $temp)
2387
							if ($temp['tag'] == $look_for)
2388
							{
2389
								$block_level = !empty($temp['block_level']);
2390
								break;
2391
							}
2392
					}
2393
2394
					if ($block_level !== true)
2395
					{
2396
						$block_level = false;
2397
						array_push($open_tags, $tag);
2398
						break;
2399
					}
2400
				}
2401
2402
				$to_close[] = $tag;
2403
			}
2404
			while ($tag['tag'] != $look_for);
2405
2406
			// Did we just eat through everything and not find it?
2407
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2408
			{
2409
				$open_tags = $to_close;
2410
				continue;
2411
			}
2412
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2413
			{
2414
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2415
				{
2416
					foreach ($bbc_codes[$look_for[0]] as $temp)
2417
						if ($temp['tag'] == $look_for)
2418
						{
2419
							$block_level = !empty($temp['block_level']);
2420
							break;
2421
						}
2422
				}
2423
2424
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2425
				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...
2426
				{
2427
					foreach ($to_close as $tag)
2428
						array_push($open_tags, $tag);
2429
					continue;
2430
				}
2431
			}
2432
2433
			foreach ($to_close as $tag)
2434
			{
2435
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2436
				$pos += strlen($tag['after']) + 2;
2437
				$pos2 = $pos - 1;
2438
2439
				// See the comment at the end of the big loop - just eating whitespace ;).
2440
				$whitespace_regex = '';
2441
				if (!empty($tag['block_level']))
2442
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2443
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2444
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2445
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2446
2447
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2448
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2449
			}
2450
2451
			if (!empty($to_close))
2452
			{
2453
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
2454
				$pos--;
2455
			}
2456
2457
			continue;
2458
		}
2459
2460
		// No tags for this character, so just keep going (fastest possible course.)
2461
		if (!isset($bbc_codes[$tag_character]))
2462
			continue;
2463
2464
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2465
		$tag = null;
2466
		foreach ($bbc_codes[$tag_character] as $possible)
2467
		{
2468
			$pt_strlen = strlen($possible['tag']);
2469
2470
			// Not a match?
2471
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2472
				continue;
2473
2474
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
2475
2476
			// A tag is the last char maybe
2477
			if ($next_c == '')
2478
				break;
2479
2480
			// A test validation?
2481
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2482
				continue;
2483
			// Do we want parameters?
2484
			elseif (!empty($possible['parameters']))
2485
			{
2486
				// Are all the parameters optional?
2487
				$param_required = false;
2488
				foreach ($possible['parameters'] as $param)
2489
				{
2490
					if (empty($param['optional']))
2491
					{
2492
						$param_required = true;
2493
						break;
2494
					}
2495
				}
2496
2497
				if ($param_required && $next_c != ' ')
2498
					continue;
2499
			}
2500
			elseif (isset($possible['type']))
2501
			{
2502
				// Do we need an equal sign?
2503
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2504
					continue;
2505
				// Maybe we just want a /...
2506
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2507
					continue;
2508
				// An immediate ]?
2509
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2510
					continue;
2511
			}
2512
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2513
			elseif ($next_c != ']')
2514
				continue;
2515
2516
			// Check allowed tree?
2517
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2518
				continue;
2519
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2520
				continue;
2521
			// If this is in the list of disallowed child tags, don't parse it.
2522
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2523
				continue;
2524
2525
			$pos1 = $pos + 1 + $pt_strlen + 1;
2526
2527
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2528
			if ($possible['tag'] == 'quote')
2529
			{
2530
				// Start with standard
2531
				$quote_alt = false;
2532
				foreach ($open_tags as $open_quote)
2533
				{
2534
					// Every parent quote this quote has flips the styling
2535
					if ($open_quote['tag'] == 'quote')
2536
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
2537
				}
2538
				// Add a class to the quote to style alternating blockquotes
2539
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2540
			}
2541
2542
			// This is long, but it makes things much easier and cleaner.
2543
			if (!empty($possible['parameters']))
2544
			{
2545
				// Build a regular expression for each parameter for the current tag.
2546
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
2547
				if (!isset($params_regexes[$regex_key]))
2548
				{
2549
					$params_regexes[$regex_key] = '';
2550
2551
					foreach ($possible['parameters'] as $p => $info)
2552
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
2553
				}
2554
2555
				// Extract the string that potentially holds our parameters.
2556
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
2557
				$blobs = preg_split('~\]~i', $blob[1]);
2558
2559
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
2560
2561
				// Progressively append more blobs until we find our parameters or run out of blobs
2562
				$blob_counter = 1;
2563
				while ($blob_counter <= count($blobs))
0 ignored issues
show
Bug introduced by
It seems like $blobs can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2563
				while ($blob_counter <= count(/** @scrutinizer ignore-type */ $blobs))
Loading history...
2564
				{
2565
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
0 ignored issues
show
Bug introduced by
It seems like $blobs can also be of type false; however, parameter $array of array_slice() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2565
					$given_param_string = implode(']', array_slice(/** @scrutinizer ignore-type */ $blobs, 0, $blob_counter++));
Loading history...
2566
2567
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2568
					sort($given_params, SORT_STRING);
0 ignored issues
show
Bug introduced by
It seems like $given_params can also be of type false; however, parameter $array of sort() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2568
					sort(/** @scrutinizer ignore-type */ $given_params, SORT_STRING);
Loading history...
2569
2570
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
2571
2572
					if ($match)
2573
						break;
2574
				}
2575
2576
				// Didn't match our parameter list, try the next possible.
2577
				if (!$match)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $match does not seem to be defined for all execution paths leading up to this point.
Loading history...
2578
					continue;
2579
2580
				$params = array();
2581
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2582
				{
2583
					$key = strtok(ltrim($matches[$i]), '=');
2584
					if ($key === false)
2585
						continue;
2586
					elseif (isset($possible['parameters'][$key]['value']))
2587
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2588
					elseif (isset($possible['parameters'][$key]['validate']))
2589
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2590
					else
2591
						$params['{' . $key . '}'] = $matches[$i + 1];
2592
2593
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2594
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2595
				}
2596
2597
				foreach ($possible['parameters'] as $p => $info)
2598
				{
2599
					if (!isset($params['{' . $p . '}']))
2600
					{
2601
						if (!isset($info['default']))
2602
							$params['{' . $p . '}'] = '';
2603
						elseif (isset($possible['parameters'][$p]['value']))
2604
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
2605
						elseif (isset($possible['parameters'][$p]['validate']))
2606
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
2607
						else
2608
							$params['{' . $p . '}'] = $info['default'];
2609
					}
2610
				}
2611
2612
				$tag = $possible;
2613
2614
				// Put the parameters into the string.
2615
				if (isset($tag['before']))
2616
					$tag['before'] = strtr($tag['before'], $params);
2617
				if (isset($tag['after']))
2618
					$tag['after'] = strtr($tag['after'], $params);
2619
				if (isset($tag['content']))
2620
					$tag['content'] = strtr($tag['content'], $params);
2621
2622
				$pos1 += strlen($given_param_string);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $given_param_string does not seem to be defined for all execution paths leading up to this point.
Loading history...
2623
			}
2624
			else
2625
			{
2626
				$tag = $possible;
2627
				$params = array();
2628
			}
2629
			break;
2630
		}
2631
2632
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2633
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2634
		{
2635
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2636
				continue;
2637
2638
			$tag = $itemcodes[$message[$pos + 1]];
2639
2640
			// First let's set up the tree: it needs to be in a list, or after an li.
2641
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2642
			{
2643
				$open_tags[] = array(
2644
					'tag' => 'list',
2645
					'after' => '</ul>',
2646
					'block_level' => true,
2647
					'require_children' => array('li'),
2648
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2649
				);
2650
				$code = '<ul class="bbc_list">';
2651
			}
2652
			// We're in a list item already: another itemcode?  Close it first.
2653
			elseif ($inside['tag'] == 'li')
2654
			{
2655
				array_pop($open_tags);
2656
				$code = '</li>';
2657
			}
2658
			else
2659
				$code = '';
2660
2661
			// Now we open a new tag.
2662
			$open_tags[] = array(
2663
				'tag' => 'li',
2664
				'after' => '</li>',
2665
				'trim' => 'outside',
2666
				'block_level' => true,
2667
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2668
			);
2669
2670
			// First, open the tag...
2671
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2672
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2673
			$pos += strlen($code) - 1 + 2;
2674
2675
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2676
			$pos2 = strpos($message, '<br>', $pos);
2677
			$pos3 = strpos($message, '[/', $pos);
2678
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2679
			{
2680
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2681
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2682
2683
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2684
			}
2685
			// Tell the [list] that it needs to close specially.
2686
			else
2687
			{
2688
				// Move the li over, because we're not sure what we'll hit.
2689
				$open_tags[count($open_tags) - 1]['after'] = '';
2690
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2691
			}
2692
2693
			continue;
2694
		}
2695
2696
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2697
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2698
		{
2699
			array_pop($open_tags);
2700
2701
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2702
			$pos += strlen($inside['after']) - 1 + 2;
2703
		}
2704
2705
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2706
		if ($tag === null)
2707
			continue;
2708
2709
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2710
		if (isset($inside['disallow_children']))
2711
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2712
2713
		// Is this tag disabled?
2714
		if (isset($disabled[$tag['tag']]))
2715
		{
2716
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2717
			{
2718
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2719
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2720
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2721
			}
2722
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2723
			{
2724
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2725
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2726
			}
2727
			else
2728
				$tag['content'] = $tag['disabled_content'];
2729
		}
2730
2731
		// we use this a lot
2732
		$tag_strlen = strlen($tag['tag']);
2733
2734
		// The only special case is 'html', which doesn't need to close things.
2735
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2736
		{
2737
			$n = count($open_tags) - 1;
2738
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2739
				$n--;
2740
2741
			// Close all the non block level tags so this tag isn't surrounded by them.
2742
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2743
			{
2744
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2745
				$ot_strlen = strlen($open_tags[$i]['after']);
2746
				$pos += $ot_strlen + 2;
2747
				$pos1 += $ot_strlen + 2;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $pos1 does not seem to be defined for all execution paths leading up to this point.
Loading history...
2748
2749
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2750
				$whitespace_regex = '';
2751
				if (!empty($tag['block_level']))
2752
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2753
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2754
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2755
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2756
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2757
2758
				array_pop($open_tags);
2759
			}
2760
		}
2761
2762
		// Can't read past the end of the message
2763
		$pos1 = min(strlen($message), $pos1);
2764
2765
		// No type means 'parsed_content'.
2766
		if (!isset($tag['type']))
2767
		{
2768
			$open_tags[] = $tag;
2769
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2770
			$pos += strlen($tag['before']) - 1 + 2;
2771
		}
2772
		// Don't parse the content, just skip it.
2773
		elseif ($tag['type'] == 'unparsed_content')
2774
		{
2775
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2776
			if ($pos2 === false)
2777
				continue;
2778
2779
			$data = substr($message, $pos1, $pos2 - $pos1);
2780
2781
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2782
				$data = substr($data, 4);
2783
2784
			if (isset($tag['validate']))
2785
				$tag['validate']($tag, $data, $disabled, $params);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $params does not seem to be defined for all execution paths leading up to this point.
Loading history...
2786
2787
			$code = strtr($tag['content'], array('$1' => $data));
2788
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2789
2790
			$pos += strlen($code) - 1 + 2;
2791
			$last_pos = $pos + 1;
2792
		}
2793
		// Don't parse the content, just skip it.
2794
		elseif ($tag['type'] == 'unparsed_equals_content')
2795
		{
2796
			// The value may be quoted for some tags - check.
2797
			if (isset($tag['quoted']))
2798
			{
2799
				$quoted = substr($message, $pos1, 6) == '&quot;';
2800
				if ($tag['quoted'] != 'optional' && !$quoted)
2801
					continue;
2802
2803
				if ($quoted)
2804
					$pos1 += 6;
2805
			}
2806
			else
2807
				$quoted = false;
2808
2809
			$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...
2810
			if ($pos2 === false)
2811
				continue;
2812
2813
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2814
			if ($pos3 === false)
2815
				continue;
2816
2817
			$data = array(
2818
				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...
2819
				substr($message, $pos1, $pos2 - $pos1)
2820
			);
2821
2822
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
2823
				$data[0] = substr($data[0], 4);
2824
2825
			// Validation for my parking, please!
2826
			if (isset($tag['validate']))
2827
				$tag['validate']($tag, $data, $disabled, $params);
2828
2829
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
2830
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2831
			$pos += strlen($code) - 1 + 2;
2832
		}
2833
		// A closed tag, with no content or value.
2834
		elseif ($tag['type'] == 'closed')
2835
		{
2836
			$pos2 = strpos($message, ']', $pos);
2837
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
2838
			$pos += strlen($tag['content']) - 1 + 2;
2839
		}
2840
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
2841
		elseif ($tag['type'] == 'unparsed_commas_content')
2842
		{
2843
			$pos2 = strpos($message, ']', $pos1);
2844
			if ($pos2 === false)
2845
				continue;
2846
2847
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
2848
			if ($pos3 === false)
2849
				continue;
2850
2851
			// We want $1 to be the content, and the rest to be csv.
2852
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
2853
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
2854
2855
			if (isset($tag['validate']))
2856
				$tag['validate']($tag, $data, $disabled, $params);
2857
2858
			$code = $tag['content'];
2859
			foreach ($data as $k => $d)
2860
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2861
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
2862
			$pos += strlen($code) - 1 + 2;
2863
		}
2864
		// This has parsed content, and a csv value which is unparsed.
2865
		elseif ($tag['type'] == 'unparsed_commas')
2866
		{
2867
			$pos2 = strpos($message, ']', $pos1);
2868
			if ($pos2 === false)
2869
				continue;
2870
2871
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
2872
2873
			if (isset($tag['validate']))
2874
				$tag['validate']($tag, $data, $disabled, $params);
2875
2876
			// Fix after, for disabled code mainly.
2877
			foreach ($data as $k => $d)
2878
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
2879
2880
			$open_tags[] = $tag;
2881
2882
			// Replace them out, $1, $2, $3, $4, etc.
2883
			$code = $tag['before'];
2884
			foreach ($data as $k => $d)
2885
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
2886
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
2887
			$pos += strlen($code) - 1 + 2;
2888
		}
2889
		// A tag set to a value, parsed or not.
2890
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
2891
		{
2892
			// The value may be quoted for some tags - check.
2893
			if (isset($tag['quoted']))
2894
			{
2895
				$quoted = substr($message, $pos1, 6) == '&quot;';
2896
				if ($tag['quoted'] != 'optional' && !$quoted)
2897
					continue;
2898
2899
				if ($quoted)
2900
					$pos1 += 6;
2901
			}
2902
			else
2903
				$quoted = false;
2904
2905
			$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...
2906
			if ($pos2 === false)
2907
				continue;
2908
2909
			$data = substr($message, $pos1, $pos2 - $pos1);
2910
2911
			// Validation for my parking, please!
2912
			if (isset($tag['validate']))
2913
				$tag['validate']($tag, $data, $disabled, $params);
2914
2915
			// For parsed content, we must recurse to avoid security problems.
2916
			if ($tag['type'] != 'unparsed_equals')
2917
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
2918
2919
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
2920
2921
			$open_tags[] = $tag;
2922
2923
			$code = strtr($tag['before'], array('$1' => $data));
2924
			$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...
2925
			$pos += strlen($code) - 1 + 2;
2926
		}
2927
2928
		// If this is block level, eat any breaks after it.
2929
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
2930
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
2931
2932
		// Are we trimming outside this tag?
2933
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
2934
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
2935
	}
2936
2937
	// Close any remaining tags.
2938
	while ($tag = array_pop($open_tags))
2939
		$message .= "\n" . $tag['after'] . "\n";
2940
2941
	// Parse the smileys within the parts where it can be done safely.
2942
	if ($smileys === true)
2943
	{
2944
		$message_parts = explode("\n", $message);
2945
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
2946
			parsesmileys($message_parts[$i]);
2947
2948
		$message = implode('', $message_parts);
2949
	}
2950
2951
	// No smileys, just get rid of the markers.
2952
	else
2953
		$message = strtr($message, array("\n" => ''));
2954
2955
	if ($message !== '' && $message[0] === ' ')
2956
		$message = '&nbsp;' . substr($message, 1);
2957
2958
	// Cleanup whitespace.
2959
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
2960
2961
	// Allow mods access to what parse_bbc created
2962
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
2963
2964
	// Cache the output if it took some time...
2965
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
2966
		cache_put_data($cache_key, $message, 240);
2967
2968
	// If this was a force parse revert if needed.
2969
	if (!empty($parse_tags))
2970
	{
2971
		if (empty($temp_bbc))
2972
			$bbc_codes = array();
2973
		else
2974
		{
2975
			$bbc_codes = $temp_bbc;
2976
			unset($temp_bbc);
2977
		}
2978
2979
		if (empty($real_alltags_regex))
2980
			$alltags_regex = '';
2981
		else
2982
		{
2983
			$alltags_regex = $real_alltags_regex;
2984
			unset($real_alltags_regex);
2985
		}
2986
	}
2987
2988
	return $message;
2989
}
2990
2991
/**
2992
 * Parse smileys in the passed message.
2993
 *
2994
 * The smiley parsing function which makes pretty faces appear :).
2995
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
2996
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
2997
 * Caches the smileys from the database or array in memory.
2998
 * Doesn't return anything, but rather modifies message directly.
2999
 *
3000
 * @param string &$message The message to parse smileys in
3001
 */
3002
function parsesmileys(&$message)
3003
{
3004
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3005
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3006
3007
	// No smiley set at all?!
3008
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3009
		return;
3010
3011
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3012
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3013
3014
	// If smileyPregSearch hasn't been set, do it now.
3015
	if (empty($smileyPregSearch))
3016
	{
3017
		// Cache for longer when customized smiley codes aren't enabled
3018
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3019
3020
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3021
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3022
		{
3023
			$result = $smcFunc['db_query']('', '
3024
				SELECT s.code, f.filename, s.description
3025
				FROM {db_prefix}smileys AS s
3026
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3027
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3028
					AND s.code IN ({array_string:default_codes})' : '') . '
3029
				ORDER BY LENGTH(s.code) DESC',
3030
				array(
3031
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3032
					'smiley_set' => $user_info['smiley_set'],
3033
				)
3034
			);
3035
			$smileysfrom = array();
3036
			$smileysto = array();
3037
			$smileysdescs = array();
3038
			while ($row = $smcFunc['db_fetch_assoc']($result))
3039
			{
3040
				$smileysfrom[] = $row['code'];
3041
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3042
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3043
			}
3044
			$smcFunc['db_free_result']($result);
3045
3046
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3047
		}
3048
		else
3049
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3050
3051
		// The non-breaking-space is a complex thing...
3052
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3053
3054
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3055
		$smileyPregReplacements = array();
3056
		$searchParts = array();
3057
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3058
3059
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3060
		{
3061
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3062
			$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">';
3063
3064
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3065
3066
			$searchParts[] = $smileysfrom[$i];
3067
			if ($smileysfrom[$i] != $specialChars)
3068
			{
3069
				$smileyPregReplacements[$specialChars] = $smileyCode;
3070
				$searchParts[] = $specialChars;
3071
3072
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3073
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3074
				if ($specialChars2 != $specialChars)
3075
				{
3076
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3077
					$searchParts[] = $specialChars2;
3078
				}
3079
			}
3080
		}
3081
3082
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
3083
	}
3084
3085
	// Replace away!
3086
	$message = preg_replace_callback($smileyPregSearch, function($matches) use ($smileyPregReplacements)
3087
		{
3088
			return $smileyPregReplacements[$matches[1]];
3089
		}, $message);
3090
}
3091
3092
/**
3093
 * Highlight any code.
3094
 *
3095
 * Uses PHP's highlight_string() to highlight PHP syntax
3096
 * does special handling to keep the tabs in the code available.
3097
 * used to parse PHP code from inside [code] and [php] tags.
3098
 *
3099
 * @param string $code The code
3100
 * @return string The code with highlighted HTML.
3101
 */
3102
function highlight_php_code($code)
3103
{
3104
	// Remove special characters.
3105
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3106
3107
	$oldlevel = error_reporting(0);
3108
3109
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3110
3111
	error_reporting($oldlevel);
3112
3113
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3114
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3115
3116
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3117
}
3118
3119
/**
3120
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3121
 *
3122
 * The returned URL may or may not be a proxied URL, depending on the situation.
3123
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3124
 *
3125
 * @param string $url The original URL of the requested resource
3126
 * @return string The URL to use
3127
 */
3128
function get_proxied_url($url)
3129
{
3130
	global $boardurl, $image_proxy_enabled, $image_proxy_secret;
3131
3132
	// Only use the proxy if enabled and necessary
3133
	if (empty($image_proxy_enabled) || parse_url($url, PHP_URL_SCHEME) === 'https')
3134
		return $url;
3135
3136
	// We don't need to proxy our own resources
3137
	if (strpos(strtr($url, array('http://' => 'https://')), strtr($boardurl, array('http://' => 'https://'))) === 0)
3138
		return strtr($url, array('http://' => 'https://'));
3139
3140
	// By default, use SMF's own image proxy script
3141
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . md5($url . $image_proxy_secret);
3142
3143
	// Allow mods to easily implement an alternative proxy
3144
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
3145
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
3146
3147
	return $proxied_url;
3148
}
3149
3150
/**
3151
 * Make sure the browser doesn't come back and repost the form data.
3152
 * Should be used whenever anything is posted.
3153
 *
3154
 * @param string $setLocation The URL to redirect them to
3155
 * @param bool $refresh Whether to use a meta refresh instead
3156
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
3157
 */
3158
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
3159
{
3160
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
3161
3162
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
3163
	if (!empty($context['flush_mail']))
3164
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3165
		AddMailQueue(true);
3166
3167
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
3168
3169
	if ($add)
3170
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
3171
3172
	// Put the session ID in.
3173
	if (defined('SID') && SID != '')
3174
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
3175
	// Keep that debug in their for template debugging!
3176
	elseif (isset($_GET['debug']))
3177
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
3178
3179
	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'])))
3180
	{
3181
		if (defined('SID') && SID != '')
3182
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
3183
				function($m) use ($scripturl)
3184
				{
3185
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
3186
				}, $setLocation);
3187
		else
3188
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
3189
				function($m) use ($scripturl)
3190
				{
3191
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
3192
				}, $setLocation);
3193
	}
3194
3195
	// Maybe integrations want to change where we are heading?
3196
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
3197
3198
	// Set the header.
3199
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
3200
3201
	// Debugging.
3202
	if (isset($db_show_debug) && $db_show_debug === true)
3203
		$_SESSION['debug_redirect'] = $db_cache;
3204
3205
	obExit(false);
3206
}
3207
3208
/**
3209
 * Ends execution.  Takes care of template loading and remembering the previous URL.
3210
 *
3211
 * @param bool $header Whether to do the header
3212
 * @param bool $do_footer Whether to do the footer
3213
 * @param bool $from_index Whether we're coming from the board index
3214
 * @param bool $from_fatal_error Whether we're coming from a fatal error
3215
 */
3216
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
3217
{
3218
	global $context, $settings, $modSettings, $txt, $smcFunc;
3219
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
3220
3221
	// Attempt to prevent a recursive loop.
3222
	++$level;
3223
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
3224
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
3225
	if ($from_fatal_error)
3226
		$has_fatal_error = true;
3227
3228
	// Clear out the stat cache.
3229
	trackStats();
3230
3231
	// If we have mail to send, send it.
3232
	if (!empty($context['flush_mail']))
3233
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3234
		AddMailQueue(true);
3235
3236
	$do_header = $header === null ? !$header_done : $header;
3237
	if ($do_footer === null)
3238
		$do_footer = $do_header;
3239
3240
	// Has the template/header been done yet?
3241
	if ($do_header)
3242
	{
3243
		// Was the page title set last minute? Also update the HTML safe one.
3244
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
3245
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3246
3247
		// Start up the session URL fixer.
3248
		ob_start('ob_sessrewrite');
3249
3250
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
3251
			$buffers = explode(',', $settings['output_buffers']);
3252
		elseif (!empty($settings['output_buffers']))
3253
			$buffers = $settings['output_buffers'];
3254
		else
3255
			$buffers = array();
3256
3257
		if (isset($modSettings['integrate_buffer']))
3258
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
3259
3260
		if (!empty($buffers))
3261
			foreach ($buffers as $function)
3262
			{
3263
				$call = call_helper($function, true);
3264
3265
				// Is it valid?
3266
				if (!empty($call))
3267
					ob_start($call);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $output_callback of ob_start() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3267
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
3268
			}
3269
3270
		// Display the screen in the logical order.
3271
		template_header();
3272
		$header_done = true;
3273
	}
3274
	if ($do_footer)
3275
	{
3276
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
3277
3278
		// Anything special to put out?
3279
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
3280
			echo $context['insert_after_template'];
3281
3282
		// Just so we don't get caught in an endless loop of errors from the footer...
3283
		if (!$footer_done)
3284
		{
3285
			$footer_done = true;
3286
			template_footer();
3287
3288
			// (since this is just debugging... it's okay that it's after </html>.)
3289
			if (!isset($_REQUEST['xml']))
3290
				displayDebug();
3291
		}
3292
	}
3293
3294
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
3295
	if (strpos($_SERVER['REQUEST_URL'], 'action=dlattach') === false && strpos($_SERVER['REQUEST_URL'], 'action=viewsmfile') === false)
3296
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
3297
3298
	// For session check verification.... don't switch browsers...
3299
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
3300
3301
	// Hand off the output to the portal, etc. we're integrated with.
3302
	call_integration_hook('integrate_exit', array($do_footer));
3303
3304
	// Don't exit if we're coming from index.php; that will pass through normally.
3305
	if (!$from_index)
3306
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
3307
}
3308
3309
/**
3310
 * Get the size of a specified image with better error handling.
3311
 *
3312
 * @todo see if it's better in Subs-Graphics, but one step at the time.
3313
 * Uses getimagesize() to determine the size of a file.
3314
 * Attempts to connect to the server first so it won't time out.
3315
 *
3316
 * @param string $url The URL of the image
3317
 * @return array|false The image size as array (width, height), or false on failure
3318
 */
3319
function url_image_size($url)
3320
{
3321
	global $sourcedir;
3322
3323
	// Make sure it is a proper URL.
3324
	$url = str_replace(' ', '%20', $url);
3325
3326
	// Can we pull this from the cache... please please?
3327
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
0 ignored issues
show
introduced by
The condition $temp = cache_get_data('...d5($url), 240) !== null is always true.
Loading history...
3328
		return $temp;
3329
	$t = microtime(true);
3330
3331
	// Get the host to pester...
3332
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3333
3334
	// Can't figure it out, just try the image size.
3335
	if ($url == '' || $url == 'http://' || $url == 'https://')
3336
	{
3337
		return false;
3338
	}
3339
	elseif (!isset($match[1]))
3340
	{
3341
		$size = @getimagesize($url);
3342
	}
3343
	else
3344
	{
3345
		// Try to connect to the server... give it half a second.
3346
		$temp = 0;
3347
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
3348
3349
		// Successful?  Continue...
3350
		if ($fp != false)
3351
		{
3352
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3353
			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");
3354
3355
			// Read in the HTTP/1.1 or whatever.
3356
			$test = substr(fgets($fp, 11), -1);
3357
			fclose($fp);
3358
3359
			// See if it returned a 404/403 or something.
3360
			if ($test < 4)
3361
			{
3362
				$size = @getimagesize($url);
3363
3364
				// This probably means allow_url_fopen is off, let's try GD.
3365
				if ($size === false && function_exists('imagecreatefromstring'))
3366
				{
3367
					// It's going to hate us for doing this, but another request...
3368
					$image = @imagecreatefromstring(fetch_web_data($url));
3369
					if ($image !== false)
3370
					{
3371
						$size = array(imagesx($image), imagesy($image));
3372
						imagedestroy($image);
3373
					}
3374
				}
3375
			}
3376
		}
3377
	}
3378
3379
	// If we didn't get it, we failed.
3380
	if (!isset($size))
3381
		$size = false;
3382
3383
	// If this took a long time, we may never have to do it again, but then again we might...
3384
	if (microtime(true) - $t > 0.8)
3385
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3386
3387
	// Didn't work.
3388
	return $size;
3389
}
3390
3391
/**
3392
 * Sets up the basic theme context stuff.
3393
 *
3394
 * @param bool $forceload Whether to load the theme even if it's already loaded
3395
 */
3396
function setupThemeContext($forceload = false)
3397
{
3398
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3399
	global $smcFunc;
3400
	static $loaded = false;
3401
3402
	// Under SSI this function can be called more then once.  That can cause some problems.
3403
	//   So only run the function once unless we are forced to run it again.
3404
	if ($loaded && !$forceload)
3405
		return;
3406
3407
	$loaded = true;
3408
3409
	$context['in_maintenance'] = !empty($maintenance);
3410
	$context['current_time'] = timeformat(time(), false);
3411
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3412
3413
	// Get some news...
3414
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3415
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3416
	{
3417
		if (trim($context['news_lines'][$i]) == '')
3418
			continue;
3419
3420
		// Clean it up for presentation ;).
3421
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3422
	}
3423
	if (!empty($context['news_lines']))
3424
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3425
3426
	if (!$user_info['is_guest'])
3427
	{
3428
		$context['user']['messages'] = &$user_info['messages'];
3429
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3430
		$context['user']['alerts'] = &$user_info['alerts'];
3431
3432
		// Personal message popup...
3433
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3434
			$context['user']['popup_messages'] = true;
3435
		else
3436
			$context['user']['popup_messages'] = false;
3437
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3438
3439
		if (allowedTo('moderate_forum'))
3440
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
3441
3442
		$context['user']['avatar'] = array();
3443
3444
		// Check for gravatar first since we might be forcing them...
3445
		if (($modSettings['gravatarEnabled'] && substr($user_info['avatar']['url'], 0, 11) == 'gravatar://') || !empty($modSettings['gravatarOverride']))
3446
		{
3447
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3448
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3449
			else
3450
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3451
		}
3452
		// Uploaded?
3453
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3454
			$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';
3455
		// Full URL?
3456
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3457
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3458
		// Otherwise we assume it's server stored.
3459
		elseif ($user_info['avatar']['url'] != '')
3460
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3461
		// No avatar at all? Fine, we have a big fat default avatar ;)
3462
		else
3463
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3464
3465
		if (!empty($context['user']['avatar']))
3466
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3467
3468
		// Figure out how long they've been logged in.
3469
		$context['user']['total_time_logged_in'] = array(
3470
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3471
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3472
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3473
		);
3474
	}
3475
	else
3476
	{
3477
		$context['user']['messages'] = 0;
3478
		$context['user']['unread_messages'] = 0;
3479
		$context['user']['avatar'] = array();
3480
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3481
		$context['user']['popup_messages'] = false;
3482
3483
		if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
3484
			$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
3485
3486
		// If we've upgraded recently, go easy on the passwords.
3487
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3488
			$context['disable_login_hashing'] = true;
3489
	}
3490
3491
	// Setup the main menu items.
3492
	setupMenuContext();
3493
3494
	// This is here because old index templates might still use it.
3495
	$context['show_news'] = !empty($settings['enable_news']);
3496
3497
	// This is done to allow theme authors to customize it as they want.
3498
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3499
3500
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3501
	if ($context['show_pm_popup'])
3502
		addInlineJavaScript('
3503
		jQuery(document).ready(function($) {
3504
			new smc_Popup({
3505
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3506
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3507
				icon_class: \'main_icons mail_new\'
3508
			});
3509
		});');
3510
3511
	// Add a generic "Are you sure?" confirmation message.
3512
	addInlineJavaScript('
3513
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
3514
3515
	// Now add the capping code for avatars.
3516
	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')
3517
		addInlineCss('
3518
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3519
3520
	// Add max image limits
3521
	if (!empty($modSettings['max_image_width']))
3522
		addInlineCss('
3523
	.postarea .bbc_img { max-width: ' . $modSettings['max_image_width'] . 'px; }');
3524
3525
	if (!empty($modSettings['max_image_height']))
3526
		addInlineCss('
3527
	.postarea .bbc_img { max-height: ' . $modSettings['max_image_height'] . 'px; }');
3528
3529
	// This looks weird, but it's because BoardIndex.php references the variable.
3530
	$context['common_stats']['latest_member'] = array(
3531
		'id' => $modSettings['latestMember'],
3532
		'name' => $modSettings['latestRealName'],
3533
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3534
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3535
	);
3536
	$context['common_stats'] = array(
3537
		'total_posts' => comma_format($modSettings['totalMessages']),
3538
		'total_topics' => comma_format($modSettings['totalTopics']),
3539
		'total_members' => comma_format($modSettings['totalMembers']),
3540
		'latest_member' => $context['common_stats']['latest_member'],
3541
	);
3542
	$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']);
3543
3544
	if (empty($settings['theme_version']))
3545
		addJavaScriptVar('smf_scripturl', $scripturl);
3546
3547
	if (!isset($context['page_title']))
3548
		$context['page_title'] = '';
3549
3550
	// Set some specific vars.
3551
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3552
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3553
3554
	// Content related meta tags, including Open Graph
3555
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3556
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3557
3558
	if (!empty($context['meta_keywords']))
3559
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3560
3561
	if (!empty($context['canonical_url']))
3562
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3563
3564
	if (!empty($settings['og_image']))
3565
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3566
3567
	if (!empty($context['meta_description']))
3568
	{
3569
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3570
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3571
	}
3572
	else
3573
	{
3574
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3575
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3576
	}
3577
3578
	call_integration_hook('integrate_theme_context');
3579
}
3580
3581
/**
3582
 * Helper function to set the system memory to a needed value
3583
 * - If the needed memory is greater than current, will attempt to get more
3584
 * - if in_use is set to true, will also try to take the current memory usage in to account
3585
 *
3586
 * @param string $needed The amount of memory to request, if needed, like 256M
3587
 * @param bool $in_use Set to true to account for current memory usage of the script
3588
 * @return boolean True if we have at least the needed memory
3589
 */
3590
function setMemoryLimit($needed, $in_use = false)
3591
{
3592
	// everything in bytes
3593
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3594
	$memory_needed = memoryReturnBytes($needed);
3595
3596
	// should we account for how much is currently being used?
3597
	if ($in_use)
3598
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3599
3600
	// if more is needed, request it
3601
	if ($memory_current < $memory_needed)
3602
	{
3603
		@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 for ini_set(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

3603
		/** @scrutinizer ignore-unhandled */ @ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');

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...
3604
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3605
	}
3606
3607
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
3608
3609
	// return success or not
3610
	return (bool) ($memory_current >= $memory_needed);
3611
}
3612
3613
/**
3614
 * Helper function to convert memory string settings to bytes
3615
 *
3616
 * @param string $val The byte string, like 256M or 1G
3617
 * @return integer The string converted to a proper integer in bytes
3618
 */
3619
function memoryReturnBytes($val)
3620
{
3621
	if (is_integer($val))
0 ignored issues
show
introduced by
The condition is_integer($val) is always false.
Loading history...
3622
		return $val;
3623
3624
	// Separate the number from the designator
3625
	$val = trim($val);
3626
	$num = intval(substr($val, 0, strlen($val) - 1));
3627
	$last = strtolower(substr($val, -1));
3628
3629
	// convert to bytes
3630
	switch ($last)
3631
	{
3632
		case 'g':
3633
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
3634
		case 'm':
3635
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
3636
		case 'k':
3637
			$num *= 1024;
3638
	}
3639
	return $num;
3640
}
3641
3642
/**
3643
 * The header template
3644
 */
3645
function template_header()
3646
{
3647
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir;
3648
3649
	setupThemeContext();
3650
3651
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
3652
	if (empty($context['no_last_modified']))
3653
	{
3654
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
3655
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
3656
3657
		// Are we debugging the template/html content?
3658
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
3659
			header('content-type: application/xhtml+xml');
3660
		elseif (!isset($_REQUEST['xml']))
3661
			header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3662
	}
3663
3664
	header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3665
3666
	// We need to splice this in after the body layer, or after the main layer for older stuff.
3667
	if ($context['in_maintenance'] && $context['user']['is_admin'])
3668
	{
3669
		$position = array_search('body', $context['template_layers']);
3670
		if ($position === false)
3671
			$position = array_search('main', $context['template_layers']);
3672
3673
		if ($position !== false)
3674
		{
3675
			$before = array_slice($context['template_layers'], 0, $position + 1);
3676
			$after = array_slice($context['template_layers'], $position + 1);
3677
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
3678
		}
3679
	}
3680
3681
	$checked_securityFiles = false;
3682
	$showed_banned = false;
3683
	foreach ($context['template_layers'] as $layer)
3684
	{
3685
		loadSubTemplate($layer . '_above', true);
3686
3687
		// May seem contrived, but this is done in case the body and main layer aren't there...
3688
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
3689
		{
3690
			$checked_securityFiles = true;
3691
3692
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
3693
3694
			// Add your own files.
3695
			call_integration_hook('integrate_security_files', array(&$securityFiles));
3696
3697
			foreach ($securityFiles as $i => $securityFile)
3698
			{
3699
				if (!file_exists($boarddir . '/' . $securityFile))
3700
					unset($securityFiles[$i]);
3701
			}
3702
3703
			// We are already checking so many files...just few more doesn't make any difference! :P
3704
			if (!empty($modSettings['currentAttachmentUploadDir']))
3705
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
3706
3707
			else
3708
				$path = $modSettings['attachmentUploadDir'];
3709
3710
			secureDirectory($path, true);
3711
			secureDirectory($cachedir);
3712
3713
			// If agreement is enabled, at least the english version shall exists
3714
			if ($modSettings['requireAgreement'])
3715
				$agreement = !file_exists($boarddir . '/agreement.txt');
3716
3717
			if (!empty($securityFiles) || (!empty($modSettings['cache_enable']) && !is_writable($cachedir)) || !empty($agreement))
3718
			{
3719
				echo '
3720
		<div class="errorbox">
3721
			<p class="alert">!!</p>
3722
			<h3>', empty($securityFiles) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
3723
			<p>';
3724
3725
				foreach ($securityFiles as $securityFile)
3726
				{
3727
					echo '
3728
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
3729
3730
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
3731
						echo '
3732
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
3733
				}
3734
3735
				if (!empty($modSettings['cache_enable']) && !is_writable($cachedir))
3736
					echo '
3737
				<strong>', $txt['cache_writable'], '</strong><br>';
3738
3739
				if (!empty($agreement))
3740
					echo '
3741
				<strong>', $txt['agreement_missing'], '</strong><br>';
3742
3743
				echo '
3744
			</p>
3745
		</div>';
3746
			}
3747
		}
3748
		// If the user is banned from posting inform them of it.
3749
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
3750
		{
3751
			$showed_banned = true;
3752
			echo '
3753
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
3754
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
3755
3756
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
3757
				echo '
3758
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
3759
3760
			if (!empty($_SESSION['ban']['expire_time']))
3761
				echo '
3762
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
3763
			else
3764
				echo '
3765
					<div>', $txt['your_ban_expires_never'], '</div>';
3766
3767
			echo '
3768
				</div>';
3769
		}
3770
	}
3771
}
3772
3773
/**
3774
 * Show the copyright.
3775
 */
3776
function theme_copyright()
3777
{
3778
	global $forum_copyright;
3779
3780
	// Don't display copyright for things like SSI.
3781
	if (SMF !== 1)
0 ignored issues
show
introduced by
The condition SMF !== 1 is always true.
Loading history...
3782
		return;
3783
3784
	// Put in the version...
3785
	printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR);
3786
}
3787
3788
/**
3789
 * The template footer
3790
 */
3791
function template_footer()
3792
{
3793
	global $context, $modSettings, $time_start, $db_count;
3794
3795
	// Show the load time?  (only makes sense for the footer.)
3796
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
3797
	$context['load_time'] = round(microtime(true) - $time_start, 3);
3798
	$context['load_queries'] = $db_count;
3799
3800
	foreach (array_reverse($context['template_layers']) as $layer)
3801
		loadSubTemplate($layer . '_below', true);
3802
}
3803
3804
/**
3805
 * Output the Javascript files
3806
 * 	- tabbing in this function is to make the HTML source look good and proper
3807
 *  - if deferred is set function will output all JS set to load at page end
3808
 *
3809
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
3810
 */
3811
function template_javascript($do_deferred = false)
3812
{
3813
	global $context, $modSettings, $settings;
3814
3815
	// Use this hook to minify/optimize Javascript files and vars
3816
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
3817
3818
	$toMinify = array(
3819
		'standard' => array(),
3820
		'defer' => array(),
3821
		'async' => array(),
3822
	);
3823
3824
	// Ouput the declared Javascript variables.
3825
	if (!empty($context['javascript_vars']) && !$do_deferred)
3826
	{
3827
		echo '
3828
	<script>';
3829
3830
		foreach ($context['javascript_vars'] as $key => $value)
3831
		{
3832
			if (empty($value))
3833
			{
3834
				echo '
3835
		var ', $key, ';';
3836
			}
3837
			else
3838
			{
3839
				echo '
3840
		var ', $key, ' = ', $value, ';';
3841
			}
3842
		}
3843
3844
		echo '
3845
	</script>';
3846
	}
3847
3848
	// In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body.
3849
	// Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance.
3850
	if (!$do_deferred)
3851
	{
3852
		// While we have JavaScript files to place in the template.
3853
		foreach ($context['javascript_files'] as $id => $js_file)
3854
		{
3855
			// Last minute call! allow theme authors to disable single files.
3856
			if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
3857
				continue;
3858
3859
			// By default files don't get minimized unless the file explicitly says so!
3860
			if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
3861
			{
3862
				if (!empty($js_file['options']['async']))
3863
					$toMinify['async'][] = $js_file;
3864
				elseif (!empty($js_file['options']['defer']))
3865
					$toMinify['defer'][] = $js_file;
3866
				else
3867
					$toMinify['standard'][] = $js_file;
3868
3869
				// Grab a random seed.
3870
				if (!isset($minSeed) && isset($js_file['options']['seed']))
3871
					$minSeed = $js_file['options']['seed'];
3872
			}
3873
3874
			else
3875
				echo '
3876
	<script src="', $js_file['fileUrl'], isset($file['options']['seed']) ? $file['options']['seed'] : '', '"', !empty($js_file['options']['async']) ? ' async' : '', !empty($js_file['options']['defer']) ? ' defer' : '', '></script>';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $file seems to never exist and therefore isset should always be false.
Loading history...
3877
		}
3878
3879
		foreach ($toMinify as $js_files)
3880
		{
3881
			if (!empty($js_files))
3882
			{
3883
				$result = custMinify($js_files, 'js');
3884
3885
				$minSuccessful = array_keys($result) === array('smf_minified');
3886
3887
				foreach ($result as $minFile)
3888
					echo '
3889
	<script src="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '"', !empty($minFile['options']['async']) ? ' async' : '', !empty($minFile['options']['defer']) ? ' defer' : '', '></script>';
3890
			}
3891
		}
3892
	}
3893
3894
	// Inline JavaScript - Actually useful some times!
3895
	if (!empty($context['javascript_inline']))
3896
	{
3897
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
3898
		{
3899
			echo '
3900
<script>
3901
window.addEventListener("DOMContentLoaded", function() {';
3902
3903
			foreach ($context['javascript_inline']['defer'] as $js_code)
3904
				echo $js_code;
3905
3906
			echo '
3907
});
3908
</script>';
3909
		}
3910
3911
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
3912
		{
3913
			echo '
3914
	<script>';
3915
3916
			foreach ($context['javascript_inline']['standard'] as $js_code)
3917
				echo $js_code;
3918
3919
			echo '
3920
	</script>';
3921
		}
3922
	}
3923
}
3924
3925
/**
3926
 * Output the CSS files
3927
 */
3928
function template_css()
3929
{
3930
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
3931
3932
	// Use this hook to minify/optimize CSS files
3933
	call_integration_hook('integrate_pre_css_output');
3934
3935
	$toMinify = array();
3936
	$normal = array();
3937
3938
	ksort($context['css_files_order']);
3939
	$context['css_files'] = array_merge(array_flip($context['css_files_order']), $context['css_files']);
3940
3941
	foreach ($context['css_files'] as $id => $file)
3942
	{
3943
		// Last minute call! allow theme authors to disable single files.
3944
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
3945
			continue;
3946
3947
		// Files are minimized unless they explicitly opt out.
3948
		if (!isset($file['options']['minimize']))
3949
			$file['options']['minimize'] = true;
3950
3951
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss']))
3952
		{
3953
			$toMinify[] = $file;
3954
3955
			// Grab a random seed.
3956
			if (!isset($minSeed) && isset($file['options']['seed']))
3957
				$minSeed = $file['options']['seed'];
3958
		}
3959
		else
3960
			$normal[] = $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : '');
3961
	}
3962
3963
	if (!empty($toMinify))
3964
	{
3965
		$result = custMinify($toMinify, 'css');
3966
3967
		$minSuccessful = array_keys($result) === array('smf_minified');
3968
3969
		foreach ($result as $minFile)
3970
			echo '
3971
	<link rel="stylesheet" href="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '">';
3972
	}
3973
3974
	// Print the rest after the minified files.
3975
	if (!empty($normal))
3976
		foreach ($normal as $nf)
3977
			echo '
3978
	<link rel="stylesheet" href="', $nf, '">';
3979
3980
	if ($db_show_debug === true)
3981
	{
3982
		// Try to keep only what's useful.
3983
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
3984
		foreach ($context['css_files'] as $file)
3985
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
3986
	}
3987
3988
	if (!empty($context['css_header']))
3989
	{
3990
		echo '
3991
	<style>';
3992
3993
		foreach ($context['css_header'] as $css)
3994
			echo $css . '
3995
	';
3996
3997
		echo '
3998
	</style>';
3999
	}
4000
}
4001
4002
/**
4003
 * Get an array of previously defined files and adds them to our main minified files.
4004
 * Sets a one day cache to avoid re-creating a file on every request.
4005
 *
4006
 * @param array $data The files to minify.
4007
 * @param string $type either css or js.
4008
 * @return array Info about the minified file, or about the original files if the minify process failed.
4009
 */
4010
function custMinify($data, $type)
4011
{
4012
	global $settings, $txt;
4013
4014
	$types = array('css', 'js');
4015
	$type = !empty($type) && in_array($type, $types) ? $type : false;
4016
	$data = is_array($data) ? $data : array();
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
4017
4018
	if (empty($type) || empty($data))
4019
		return $data;
4020
4021
	// Different pages include different files, so we use a hash to label the different combinations
4022
	$hash = md5(implode(' ', array_map(function($file)
4023
	{
4024
		return $file['filePath'] . '-' . $file['mtime'];
4025
	}, $data)));
4026
4027
	// Is this a deferred or asynchronous JavaScript file?
4028
	$async = $type === 'js';
4029
	$defer = $type === 'js';
4030
	if ($type === 'js')
4031
	{
4032
		foreach ($data as $id => $file)
4033
		{
4034
			// A minified script should only be loaded asynchronously if all its components wanted to be.
4035
			if (empty($file['options']['async']))
4036
				$async = false;
4037
4038
			// A minified script should only be deferred if all its components wanted to be.
4039
			if (empty($file['options']['defer']))
4040
				$defer = false;
4041
		}
4042
	}
4043
4044
	// Did we already do this?
4045
	$minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type;
4046
	$already_exists = file_exists($minified_file);
4047
4048
	// Already done?
4049
	if ($already_exists)
4050
	{
4051
		return array('smf_minified' => array(
4052
			'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4053
			'filePath' => $minified_file,
4054
			'fileName' => basename($minified_file),
4055
			'options' => array('async' => !empty($async), 'defer' => !empty($defer)),
4056
		));
4057
	}
4058
	// File has to exist. If it doesn't, try to create it.
4059
	elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file))
4060
	{
4061
		loadLanguage('Errors');
4062
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4063
4064
		// The process failed, so roll back to print each individual file.
4065
		return $data;
4066
	}
4067
4068
	// No namespaces, sorry!
4069
	$classType = 'MatthiasMullie\\Minify\\' . strtoupper($type);
4070
4071
	$minifier = new $classType();
4072
4073
	foreach ($data as $id => $file)
4074
	{
4075
		$toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false;
4076
4077
		// The file couldn't be located so it won't be added. Log this error.
4078
		if (empty($toAdd))
4079
		{
4080
			loadLanguage('Errors');
4081
			log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general');
4082
			continue;
4083
		}
4084
4085
		// Add this file to the list.
4086
		$minifier->add($toAdd);
4087
	}
4088
4089
	// Create the file.
4090
	$minifier->minify($minified_file);
4091
	unset($minifier);
4092
	clearstatcache();
4093
4094
	// Minify process failed.
4095
	if (!filesize($minified_file))
4096
	{
4097
		loadLanguage('Errors');
4098
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4099
4100
		// The process failed so roll back to print each individual file.
4101
		return $data;
4102
	}
4103
4104
	return array('smf_minified' => array(
4105
		'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4106
		'filePath' => $minified_file,
4107
		'fileName' => basename($minified_file),
4108
		'options' => array('async' => $async, 'defer' => $defer),
4109
	));
4110
}
4111
4112
/**
4113
 * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date
4114
 */
4115
function deleteAllMinified()
4116
{
4117
	global $smcFunc, $txt, $modSettings;
4118
4119
	$not_deleted = array();
4120
	$most_recent = 0;
4121
4122
	// Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie.
4123
	$request = $smcFunc['db_query']('', '
4124
		SELECT id_theme AS id, value AS dir
4125
		FROM {db_prefix}themes
4126
		WHERE variable = {string:var}',
4127
		array(
4128
			'var' => 'theme_dir',
4129
		)
4130
	);
4131
	while ($theme = $smcFunc['db_fetch_assoc']($request))
4132
	{
4133
		foreach (array('css', 'js') as $type)
4134
		{
4135
			foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename)
4136
			{
4137
				// We want to find the most recent mtime of non-minified files
4138
				if (strpos(pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
4139
					$most_recent = max($modSettings['browser_cache'], (int) @filemtime($filename));
4140
4141
				// Try to delete minified files. Add them to our error list if that fails.
4142
				elseif (!@unlink($filename))
4143
					$not_deleted[] = $filename;
4144
			}
4145
		}
4146
	}
4147
	$smcFunc['db_free_result']($request);
4148
4149
	// This setting tracks the most recent modification time of any of our CSS and JS files
4150
	if ($most_recent > $modSettings['browser_cache'])
4151
		updateSettings(array('browser_cache' => $most_recent));
4152
4153
	// If any of the files could not be deleted, log an error about it.
4154
	if (!empty($not_deleted))
4155
	{
4156
		loadLanguage('Errors');
4157
		log_error(sprintf($txt['unlink_minimized_fail'], implode('<br>', $not_deleted)), 'general');
4158
	}
4159
}
4160
4161
/**
4162
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
4163
 *
4164
 * @todo this currently returns the hash if new, and the full filename otherwise.
4165
 * Something messy like that.
4166
 * @todo and of course everything relies on this behavior and work around it. :P.
4167
 * Converters included.
4168
 *
4169
 * @param string $filename The name of the file
4170
 * @param int $attachment_id The ID of the attachment
4171
 * @param string $dir Which directory it should be in (null to use current one)
4172
 * @param bool $new Whether this is a new attachment
4173
 * @param string $file_hash The file hash
4174
 * @return string The path to the file
4175
 */
4176
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
4177
{
4178
	global $modSettings, $smcFunc;
4179
4180
	// Just make up a nice hash...
4181
	if ($new)
4182
		return sha1(md5($filename . time()) . mt_rand());
4183
4184
	// Just make sure that attachment id is only a int
4185
	$attachment_id = (int) $attachment_id;
4186
4187
	// Grab the file hash if it wasn't added.
4188
	// Left this for legacy.
4189
	if ($file_hash === '')
4190
	{
4191
		$request = $smcFunc['db_query']('', '
4192
			SELECT file_hash
4193
			FROM {db_prefix}attachments
4194
			WHERE id_attach = {int:id_attach}',
4195
			array(
4196
				'id_attach' => $attachment_id,
4197
			)
4198
		);
4199
4200
		if ($smcFunc['db_num_rows']($request) === 0)
4201
			return false;
4202
4203
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
4204
		$smcFunc['db_free_result']($request);
4205
	}
4206
4207
	// Still no hash? mmm...
4208
	if (empty($file_hash))
4209
		$file_hash = sha1(md5($filename . time()) . mt_rand());
4210
4211
	// Are we using multiple directories?
4212
	if (is_array($modSettings['attachmentUploadDir']))
4213
		$path = $modSettings['attachmentUploadDir'][$dir];
4214
4215
	else
4216
		$path = $modSettings['attachmentUploadDir'];
4217
4218
	return $path . '/' . $attachment_id . '_' . $file_hash . '.dat';
4219
}
4220
4221
/**
4222
 * Convert a single IP to a ranged IP.
4223
 * internal function used to convert a user-readable format to a format suitable for the database.
4224
 *
4225
 * @param string $fullip The full IP
4226
 * @return array An array of IP parts
4227
 */
4228
function ip2range($fullip)
4229
{
4230
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
4231
	if ($fullip == 'unknown')
4232
		$fullip = '255.255.255.255';
4233
4234
	$ip_parts = explode('-', $fullip);
4235
	$ip_array = array();
4236
4237
	// if ip 22.12.31.21
4238
	if (count($ip_parts) == 1 && isValidIP($fullip))
4239
	{
4240
		$ip_array['low'] = $fullip;
4241
		$ip_array['high'] = $fullip;
4242
		return $ip_array;
4243
	} // if ip 22.12.* -> 22.12.* - 22.12.*
4244
	elseif (count($ip_parts) == 1)
4245
	{
4246
		$ip_parts[0] = $fullip;
4247
		$ip_parts[1] = $fullip;
4248
	}
4249
4250
	// if ip 22.12.31.21-12.21.31.21
4251
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
4252
	{
4253
		$ip_array['low'] = $ip_parts[0];
4254
		$ip_array['high'] = $ip_parts[1];
4255
		return $ip_array;
4256
	}
4257
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
4258
	{
4259
		$valid_low = isValidIP($ip_parts[0]);
4260
		$valid_high = isValidIP($ip_parts[1]);
4261
		$count = 0;
4262
		$mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.');
4263
		$max = ($mode == ':' ? 'ffff' : '255');
4264
		$min = 0;
4265
		if (!$valid_low)
4266
		{
4267
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
4268
			$valid_low = isValidIP($ip_parts[0]);
4269
			while (!$valid_low)
4270
			{
4271
				$ip_parts[0] .= $mode . $min;
4272
				$valid_low = isValidIP($ip_parts[0]);
4273
				$count++;
4274
				if ($count > 9) break;
4275
			}
4276
		}
4277
4278
		$count = 0;
4279
		if (!$valid_high)
4280
		{
4281
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
4282
			$valid_high = isValidIP($ip_parts[1]);
4283
			while (!$valid_high)
4284
			{
4285
				$ip_parts[1] .= $mode . $max;
4286
				$valid_high = isValidIP($ip_parts[1]);
4287
				$count++;
4288
				if ($count > 9) break;
4289
			}
4290
		}
4291
4292
		if ($valid_high && $valid_low)
4293
		{
4294
			$ip_array['low'] = $ip_parts[0];
4295
			$ip_array['high'] = $ip_parts[1];
4296
		}
4297
	}
4298
4299
	return $ip_array;
4300
}
4301
4302
/**
4303
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
4304
 *
4305
 * @param string $ip The IP to get the hostname from
4306
 * @return string The hostname
4307
 */
4308
function host_from_ip($ip)
4309
{
4310
	global $modSettings;
4311
4312
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
0 ignored issues
show
introduced by
The condition $host = cache_get_data('...-' . $ip, 600) !== null is always true.
Loading history...
4313
		return $host;
4314
	$t = microtime(true);
4315
4316
	// Try the Linux host command, perhaps?
4317
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
4318
	{
4319
		if (!isset($modSettings['host_to_dis']))
4320
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
4321
		else
4322
			$test = @shell_exec('host ' . @escapeshellarg($ip));
4323
4324
		// Did host say it didn't find anything?
4325
		if (strpos($test, 'not found') !== false)
4326
			$host = '';
4327
		// Invalid server option?
4328
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
4329
			updateSettings(array('host_to_dis' => 1));
4330
		// Maybe it found something, after all?
4331
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
4332
			$host = $match[1];
4333
	}
4334
4335
	// This is nslookup; usually only Windows, but possibly some Unix?
4336
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
4337
	{
4338
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
4339
		if (strpos($test, 'Non-existent domain') !== false)
4340
			$host = '';
4341
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
4342
			$host = $match[1];
4343
	}
4344
4345
	// This is the last try :/.
4346
	if (!isset($host) || $host === false)
4347
		$host = @gethostbyaddr($ip);
4348
4349
	// It took a long time, so let's cache it!
4350
	if (microtime(true) - $t > 0.5)
4351
		cache_put_data('hostlookup-' . $ip, $host, 600);
4352
4353
	return $host;
4354
}
4355
4356
/**
4357
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
4358
 *
4359
 * @param string $text The text to split into words
4360
 * @param int $max_chars The maximum number of characters per word
4361
 * @param bool $encrypt Whether to encrypt the results
4362
 * @return array An array of ints or words depending on $encrypt
4363
 */
4364
function text2words($text, $max_chars = 20, $encrypt = false)
4365
{
4366
	global $smcFunc, $context;
4367
4368
	// Step 1: Remove entities/things we don't consider words:
4369
	$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>' => ' ')));
4370
4371
	// Step 2: Entities we left to letters, where applicable, lowercase.
4372
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
4373
4374
	// Step 3: Ready to split apart and index!
4375
	$words = explode(' ', $words);
4376
4377
	if ($encrypt)
4378
	{
4379
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
4380
		$returned_ints = array();
4381
		foreach ($words as $word)
4382
		{
4383
			if (($word = trim($word, '-_\'')) !== '')
4384
			{
4385
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
4386
				$total = 0;
4387
				for ($i = 0; $i < $max_chars; $i++)
4388
					$total += $possible_chars[ord($encrypted{$i})] * pow(63, $i);
4389
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
4390
			}
4391
		}
4392
		return array_unique($returned_ints);
4393
	}
4394
	else
4395
	{
4396
		// Trim characters before and after and add slashes for database insertion.
4397
		$returned_words = array();
4398
		foreach ($words as $word)
4399
			if (($word = trim($word, '-_\'')) !== '')
4400
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
4401
4402
		// Filter out all words that occur more than once.
4403
		return array_unique($returned_words);
4404
	}
4405
}
4406
4407
/**
4408
 * Creates an image/text button
4409
 *
4410
 * @param string $name The name of the button (should be a main_icons class or the name of an image)
4411
 * @param string $alt The alt text
4412
 * @param string $label The $txt string to use as the label
4413
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
4414
 * @param boolean $force_use Whether to force use of this when template_create_button is available
4415
 * @return string The HTML to display the button
4416
 */
4417
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
4418
{
4419
	global $settings, $txt;
4420
4421
	// Does the current loaded theme have this and we are not forcing the usage of this function?
4422
	if (function_exists('template_create_button') && !$force_use)
4423
		return template_create_button($name, $alt, $label = '', $custom = '');
4424
4425
	if (!$settings['use_image_buttons'])
4426
		return $txt[$alt];
4427
	elseif (!empty($settings['use_buttons']))
4428
		return '<span class="main_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
4429
	else
4430
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
4431
}
4432
4433
/**
4434
 * Sets up all of the top menu buttons
4435
 * Saves them in the cache if it is available and on
4436
 * Places the results in $context
4437
 */
4438
function setupMenuContext()
4439
{
4440
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc;
4441
4442
	// Set up the menu privileges.
4443
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
4444
	$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'));
4445
4446
	$context['allow_memberlist'] = allowedTo('view_mlist');
4447
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
4448
	$context['allow_moderation_center'] = $context['user']['can_mod'];
4449
	$context['allow_pm'] = allowedTo('pm_read');
4450
4451
	$cacheTime = $modSettings['lastActive'] * 60;
4452
4453
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
4454
	if (!isset($context['allow_calendar_event']))
4455
	{
4456
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
4457
4458
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
4459
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
4460
		{
4461
			$boards_can_post = boardsAllowedTo('post_new');
4462
			$context['allow_calendar_event'] &= !empty($boards_can_post);
4463
		}
4464
	}
4465
4466
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
4467
	if (!$context['user']['is_guest'])
4468
	{
4469
		addInlineJavaScript('
4470
	var user_menus = new smc_PopupMenu();
4471
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
4472
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true);
4473
		if ($context['allow_pm'])
4474
			addInlineJavaScript('
4475
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
4476
4477
		if (!empty($modSettings['enable_ajax_alerts']))
4478
		{
4479
			require_once($sourcedir . '/Subs-Notify.php');
4480
4481
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
4482
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
4483
4484
			addInlineJavaScript('
4485
	var new_alert_title = "' . $context['forum_name'] . '";
4486
	var alert_timeout = ' . $timeout . ';');
4487
			loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts');
4488
		}
4489
	}
4490
4491
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
4492
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
4493
	{
4494
		$buttons = array(
4495
			'home' => array(
4496
				'title' => $txt['home'],
4497
				'href' => $scripturl,
4498
				'show' => true,
4499
				'sub_buttons' => array(
4500
				),
4501
				'is_last' => $context['right_to_left'],
4502
			),
4503
			'search' => array(
4504
				'title' => $txt['search'],
4505
				'href' => $scripturl . '?action=search',
4506
				'show' => $context['allow_search'],
4507
				'sub_buttons' => array(
4508
				),
4509
			),
4510
			'admin' => array(
4511
				'title' => $txt['admin'],
4512
				'href' => $scripturl . '?action=admin',
4513
				'show' => $context['allow_admin'],
4514
				'sub_buttons' => array(
4515
					'featuresettings' => array(
4516
						'title' => $txt['modSettings_title'],
4517
						'href' => $scripturl . '?action=admin;area=featuresettings',
4518
						'show' => allowedTo('admin_forum'),
4519
					),
4520
					'packages' => array(
4521
						'title' => $txt['package'],
4522
						'href' => $scripturl . '?action=admin;area=packages',
4523
						'show' => allowedTo('admin_forum'),
4524
					),
4525
					'errorlog' => array(
4526
						'title' => $txt['errorlog'],
4527
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
4528
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
4529
					),
4530
					'permissions' => array(
4531
						'title' => $txt['edit_permissions'],
4532
						'href' => $scripturl . '?action=admin;area=permissions',
4533
						'show' => allowedTo('manage_permissions'),
4534
					),
4535
					'memberapprove' => array(
4536
						'title' => $txt['approve_members_waiting'],
4537
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
4538
						'show' => !empty($context['unapproved_members']),
4539
						'is_last' => true,
4540
					),
4541
				),
4542
			),
4543
			'moderate' => array(
4544
				'title' => $txt['moderate'],
4545
				'href' => $scripturl . '?action=moderate',
4546
				'show' => $context['allow_moderation_center'],
4547
				'sub_buttons' => array(
4548
					'modlog' => array(
4549
						'title' => $txt['modlog_view'],
4550
						'href' => $scripturl . '?action=moderate;area=modlog',
4551
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4552
					),
4553
					'poststopics' => array(
4554
						'title' => $txt['mc_unapproved_poststopics'],
4555
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
4556
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4557
					),
4558
					'attachments' => array(
4559
						'title' => $txt['mc_unapproved_attachments'],
4560
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
4561
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4562
					),
4563
					'reports' => array(
4564
						'title' => $txt['mc_reported_posts'],
4565
						'href' => $scripturl . '?action=moderate;area=reportedposts',
4566
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4567
					),
4568
					'reported_members' => array(
4569
						'title' => $txt['mc_reported_members'],
4570
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
4571
						'show' => allowedTo('moderate_forum'),
4572
						'is_last' => true,
4573
					)
4574
				),
4575
			),
4576
			'calendar' => array(
4577
				'title' => $txt['calendar'],
4578
				'href' => $scripturl . '?action=calendar',
4579
				'show' => $context['allow_calendar'],
4580
				'sub_buttons' => array(
4581
					'view' => array(
4582
						'title' => $txt['calendar_menu'],
4583
						'href' => $scripturl . '?action=calendar',
4584
						'show' => $context['allow_calendar_event'],
4585
					),
4586
					'post' => array(
4587
						'title' => $txt['calendar_post_event'],
4588
						'href' => $scripturl . '?action=calendar;sa=post',
4589
						'show' => $context['allow_calendar_event'],
4590
						'is_last' => true,
4591
					),
4592
				),
4593
			),
4594
			'mlist' => array(
4595
				'title' => $txt['members_title'],
4596
				'href' => $scripturl . '?action=mlist',
4597
				'show' => $context['allow_memberlist'],
4598
				'sub_buttons' => array(
4599
					'mlist_view' => array(
4600
						'title' => $txt['mlist_menu_view'],
4601
						'href' => $scripturl . '?action=mlist',
4602
						'show' => true,
4603
					),
4604
					'mlist_search' => array(
4605
						'title' => $txt['mlist_search'],
4606
						'href' => $scripturl . '?action=mlist;sa=search',
4607
						'show' => true,
4608
						'is_last' => true,
4609
					),
4610
				),
4611
			),
4612
			'signup' => array(
4613
				'title' => $txt['register'],
4614
				'href' => $scripturl . '?action=signup',
4615
				'show' => $user_info['is_guest'] && $context['can_register'],
4616
				'sub_buttons' => array(
4617
				),
4618
				'is_last' => !$context['right_to_left'],
4619
			),
4620
			'logout' => array(
4621
				'title' => $txt['logout'],
4622
				'href' => $scripturl . '?action=logout;%1$s=%2$s',
4623
				'show' => !$user_info['is_guest'],
4624
				'sub_buttons' => array(
4625
				),
4626
				'is_last' => !$context['right_to_left'],
4627
			),
4628
		);
4629
4630
		// Allow editing menu buttons easily.
4631
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
4632
4633
		// Now we put the buttons in the context so the theme can use them.
4634
		$menu_buttons = array();
4635
		foreach ($buttons as $act => $button)
4636
			if (!empty($button['show']))
4637
			{
4638
				$button['active_button'] = false;
4639
4640
				// This button needs some action.
4641
				if (isset($button['action_hook']))
4642
					$needs_action_hook = true;
4643
4644
				// Make sure the last button truly is the last button.
4645
				if (!empty($button['is_last']))
4646
				{
4647
					if (isset($last_button))
4648
						unset($menu_buttons[$last_button]['is_last']);
4649
					$last_button = $act;
4650
				}
4651
4652
				// Go through the sub buttons if there are any.
4653
				if (!empty($button['sub_buttons']))
4654
					foreach ($button['sub_buttons'] as $key => $subbutton)
4655
					{
4656
						if (empty($subbutton['show']))
4657
							unset($button['sub_buttons'][$key]);
4658
4659
						// 2nd level sub buttons next...
4660
						if (!empty($subbutton['sub_buttons']))
4661
						{
4662
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
4663
							{
4664
								if (empty($sub_button2['show']))
4665
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
4666
							}
4667
						}
4668
					}
4669
4670
				// Does this button have its own icon?
4671
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
4672
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
4673
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
4674
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
4675
				elseif (isset($button['icon']))
4676
					$button['icon'] = '<span class="main_icons ' . $button['icon'] . '"></span>';
4677
				else
4678
					$button['icon'] = '<span class="main_icons ' . $act . '"></span>';
4679
4680
				$menu_buttons[$act] = $button;
4681
			}
4682
4683
		if (!empty($modSettings['cache_enable']) && $modSettings['cache_enable'] >= 2)
4684
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
4685
	}
4686
4687
	$context['menu_buttons'] = $menu_buttons;
4688
4689
	// Logging out requires the session id in the url.
4690
	if (isset($context['menu_buttons']['logout']))
4691
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
4692
4693
	// Figure out which action we are doing so we can set the active tab.
4694
	// Default to home.
4695
	$current_action = 'home';
4696
4697
	if (isset($context['menu_buttons'][$context['current_action']]))
4698
		$current_action = $context['current_action'];
4699
	elseif ($context['current_action'] == 'search2')
4700
		$current_action = 'search';
4701
	elseif ($context['current_action'] == 'theme')
4702
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
4703
	elseif ($context['current_action'] == 'register2')
4704
		$current_action = 'register';
4705
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
4706
		$current_action = 'login';
4707
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
4708
		$current_action = 'moderate';
4709
4710
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
4711
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
4712
	{
4713
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
4714
		$context[$current_action] = true;
4715
	}
4716
	elseif ($context['current_action'] == 'pm')
4717
	{
4718
		$current_action = 'self_pm';
4719
		$context['self_pm'] = true;
4720
	}
4721
4722
	$context['total_mod_reports'] = 0;
4723
	$context['total_admin_reports'] = 0;
4724
4725
	if (!empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1' && !empty($context['open_mod_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reports']))
4726
	{
4727
		$context['total_mod_reports'] = $context['open_mod_reports'];
4728
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports'];
4729
	}
4730
4731
	// Show how many errors there are
4732
	if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog']))
4733
	{
4734
		// Get an error count, if necessary
4735
		if (!isset($context['num_errors']))
4736
		{
4737
			$query = $smcFunc['db_query']('', '
4738
				SELECT COUNT(*)
4739
				FROM {db_prefix}log_errors',
4740
				array()
4741
			);
4742
4743
			list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
4744
			$smcFunc['db_free_result']($query);
4745
		}
4746
4747
		if (!empty($context['num_errors']))
4748
		{
4749
			$context['total_admin_reports'] += $context['num_errors'];
4750
			$context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors'];
4751
		}
4752
	}
4753
4754
	// Show number of reported members
4755
	if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members']))
4756
	{
4757
		$context['total_mod_reports'] += $context['open_member_reports'];
4758
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports'];
4759
	}
4760
4761
	if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin']))
4762
	{
4763
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members'];
4764
		$context['total_admin_reports'] += $context['unapproved_members'];
4765
	}
4766
4767
	if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin']))
4768
	{
4769
		$context['menu_buttons']['admin']['amt'] = $context['total_admin_reports'];
4770
	}
4771
4772
	// Do we have any open reports?
4773
	if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate']))
4774
	{
4775
		$context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports'];
4776
	}
4777
4778
	// Not all actions are simple.
4779
	if (!empty($needs_action_hook))
4780
		call_integration_hook('integrate_current_action', array(&$current_action));
4781
4782
	if (isset($context['menu_buttons'][$current_action]))
4783
		$context['menu_buttons'][$current_action]['active_button'] = true;
4784
}
4785
4786
/**
4787
 * Generate a random seed and ensure it's stored in settings.
4788
 */
4789
function smf_seed_generator()
4790
{
4791
	updateSettings(array('rand_seed' => microtime(true)));
4792
}
4793
4794
/**
4795
 * Process functions of an integration hook.
4796
 * calls all functions of the given hook.
4797
 * supports static class method calls.
4798
 *
4799
 * @param string $hook The hook name
4800
 * @param array $parameters An array of parameters this hook implements
4801
 * @return array The results of the functions
4802
 */
4803
function call_integration_hook($hook, $parameters = array())
4804
{
4805
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
4806
	global $context, $txt;
4807
4808
	if ($db_show_debug === true)
4809
		$context['debug']['hooks'][] = $hook;
4810
4811
	// Need to have some control.
4812
	if (!isset($context['instances']))
4813
		$context['instances'] = array();
4814
4815
	$results = array();
4816
	if (empty($modSettings[$hook]))
4817
		return $results;
4818
4819
	$functions = explode(',', $modSettings[$hook]);
4820
	// Loop through each function.
4821
	foreach ($functions as $function)
4822
	{
4823
		// Hook has been marked as "disabled". Skip it!
4824
		if (strpos($function, '!') !== false)
4825
			continue;
4826
4827
		$call = call_helper($function, true);
4828
4829
		// Is it valid?
4830
		if (!empty($call))
4831
			$results[$function] = call_user_func_array($call, $parameters);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $function of call_user_func_array() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4831
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
4832
		// This failed, but we want to do so silently.
4833
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
4834
			return $results;
4835
		// Whatever it was suppose to call, it failed :(
4836
		elseif (!empty($function))
4837
		{
4838
			loadLanguage('Errors');
4839
4840
			// Get a full path to show on error.
4841
			if (strpos($function, '|') !== false)
4842
			{
4843
				list ($file, $string) = explode('|', $function);
4844
				$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'])));
4845
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
4846
			}
4847
			// "Assume" the file resides on $boarddir somewhere...
4848
			else
4849
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
4850
		}
4851
	}
4852
4853
	return $results;
4854
}
4855
4856
/**
4857
 * Add a function for integration hook.
4858
 * does nothing if the function is already added.
4859
 *
4860
 * @param string $hook The complete hook name.
4861
 * @param string $function The function name. Can be a call to a method via Class::method.
4862
 * @param bool $permanent If true, updates the value in settings table.
4863
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4864
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4865
 */
4866
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
4867
{
4868
	global $smcFunc, $modSettings;
4869
4870
	// Any objects?
4871
	if ($object)
4872
		$function = $function . '#';
4873
4874
	// Any files  to load?
4875
	if (!empty($file) && is_string($file))
4876
		$function = $file . (!empty($function) ? '|' . $function : '');
4877
4878
	// Get the correct string.
4879
	$integration_call = $function;
4880
4881
	// Is it going to be permanent?
4882
	if ($permanent)
4883
	{
4884
		$request = $smcFunc['db_query']('', '
4885
			SELECT value
4886
			FROM {db_prefix}settings
4887
			WHERE variable = {string:variable}',
4888
			array(
4889
				'variable' => $hook,
4890
			)
4891
		);
4892
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
4893
		$smcFunc['db_free_result']($request);
4894
4895
		if (!empty($current_functions))
4896
		{
4897
			$current_functions = explode(',', $current_functions);
4898
			if (in_array($integration_call, $current_functions))
4899
				return;
4900
4901
			$permanent_functions = array_merge($current_functions, array($integration_call));
4902
		}
4903
		else
4904
			$permanent_functions = array($integration_call);
4905
4906
		updateSettings(array($hook => implode(',', $permanent_functions)));
4907
	}
4908
4909
	// Make current function list usable.
4910
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4911
4912
	// Do nothing, if it's already there.
4913
	if (in_array($integration_call, $functions))
4914
		return;
4915
4916
	$functions[] = $integration_call;
4917
	$modSettings[$hook] = implode(',', $functions);
4918
}
4919
4920
/**
4921
 * Remove an integration hook function.
4922
 * Removes the given function from the given hook.
4923
 * Does nothing if the function is not available.
4924
 *
4925
 * @param string $hook The complete hook name.
4926
 * @param string $function The function name. Can be a call to a method via Class::method.
4927
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
4928
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
4929
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
4930
 * @see add_integration_function
4931
 */
4932
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
4933
{
4934
	global $smcFunc, $modSettings;
4935
4936
	// Any objects?
4937
	if ($object)
4938
		$function = $function . '#';
4939
4940
	// Any files  to load?
4941
	if (!empty($file) && is_string($file))
4942
		$function = $file . '|' . $function;
4943
4944
	// Get the correct string.
4945
	$integration_call = $function;
4946
4947
	// Get the permanent functions.
4948
	$request = $smcFunc['db_query']('', '
4949
		SELECT value
4950
		FROM {db_prefix}settings
4951
		WHERE variable = {string:variable}',
4952
		array(
4953
			'variable' => $hook,
4954
		)
4955
	);
4956
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
4957
	$smcFunc['db_free_result']($request);
4958
4959
	if (!empty($current_functions))
4960
	{
4961
		$current_functions = explode(',', $current_functions);
4962
4963
		if (in_array($integration_call, $current_functions))
4964
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
4965
	}
4966
4967
	// Turn the function list into something usable.
4968
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
4969
4970
	// You can only remove it if it's available.
4971
	if (!in_array($integration_call, $functions))
4972
		return;
4973
4974
	$functions = array_diff($functions, array($integration_call));
4975
	$modSettings[$hook] = implode(',', $functions);
4976
}
4977
4978
/**
4979
 * Receives a string and tries to figure it out if its a method or a function.
4980
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
4981
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
4982
 * Prepare and returns a callable depending on the type of method/function found.
4983
 *
4984
 * @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)
4985
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
4986
 * @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.
4987
 */
4988
function call_helper($string, $return = false)
4989
{
4990
	global $context, $smcFunc, $txt, $db_show_debug;
4991
4992
	// Really?
4993
	if (empty($string))
4994
		return false;
4995
4996
	// An array? should be a "callable" array IE array(object/class, valid_callable).
4997
	// A closure? should be a callable one.
4998
	if (is_array($string) || $string instanceof Closure)
4999
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5000
5001
	// No full objects, sorry! pass a method or a property instead!
5002
	if (is_object($string))
5003
		return false;
5004
5005
	// Stay vitaminized my friends...
5006
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5007
5008
	// Is there a file to load?
5009
	$string = load_file($string);
5010
5011
	// Loaded file failed
5012
	if (empty($string))
5013
		return false;
5014
5015
	// Found a method.
5016
	if (strpos($string, '::') !== false)
5017
	{
5018
		list ($class, $method) = explode('::', $string);
5019
5020
		// Check if a new object will be created.
5021
		if (strpos($method, '#') !== false)
5022
		{
5023
			// Need to remove the # thing.
5024
			$method = str_replace('#', '', $method);
5025
5026
			// Don't need to create a new instance for every method.
5027
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
5028
			{
5029
				$context['instances'][$class] = new $class;
5030
5031
				// Add another one to the list.
5032
				if ($db_show_debug === true)
5033
				{
5034
					if (!isset($context['debug']['instances']))
5035
						$context['debug']['instances'] = array();
5036
5037
					$context['debug']['instances'][$class] = $class;
5038
				}
5039
			}
5040
5041
			$func = array($context['instances'][$class], $method);
5042
		}
5043
5044
		// Right then. This is a call to a static method.
5045
		else
5046
			$func = array($class, $method);
5047
	}
5048
5049
	// Nope! just a plain regular function.
5050
	else
5051
		$func = $string;
5052
5053
	// We can't call this helper, but we want to silently ignore this.
5054
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
5055
		return false;
5056
	// Right, we got what we need, time to do some checks.
5057
	elseif (!is_callable($func, false, $callable_name))
5058
	{
5059
		loadLanguage('Errors');
5060
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
5061
5062
		// Gotta tell everybody.
5063
		return false;
5064
	}
5065
5066
	// Everything went better than expected.
5067
	else
5068
	{
5069
		// What are we gonna do about it?
5070
		if ($return)
5071
			return $func;
5072
5073
		// If this is a plain function, avoid the heat of calling call_user_func().
5074
		else
5075
		{
5076
			if (is_array($func))
5077
				call_user_func($func);
5078
5079
			else
5080
				$func();
5081
		}
5082
	}
5083
}
5084
5085
/**
5086
 * Receives a string and tries to figure it out if it contains info to load a file.
5087
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
5088
 * 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.
5089
 *
5090
 * @param string $string The string containing a valid format.
5091
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
5092
 */
5093
function load_file($string)
5094
{
5095
	global $sourcedir, $txt, $boarddir, $settings;
5096
5097
	if (empty($string))
5098
		return false;
5099
5100
	if (strpos($string, '|') !== false)
5101
	{
5102
		list ($file, $string) = explode('|', $string);
5103
5104
		// Match the wildcards to their regular vars.
5105
		if (empty($settings['theme_dir']))
5106
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
5107
5108
		else
5109
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
5110
5111
		// Load the file if it can be loaded.
5112
		if (file_exists($absPath))
5113
			require_once($absPath);
5114
5115
		// No? try a fallback to $sourcedir
5116
		else
5117
		{
5118
			$absPath = $sourcedir . '/' . $file;
5119
5120
			if (file_exists($absPath))
5121
				require_once($absPath);
5122
5123
			// Sorry, can't do much for you at this point.
5124
			else
5125
			{
5126
				loadLanguage('Errors');
5127
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
5128
5129
				// File couldn't be loaded.
5130
				return false;
5131
			}
5132
		}
5133
	}
5134
5135
	return $string;
5136
}
5137
5138
/**
5139
 * Get the contents of a URL, irrespective of allow_url_fopen.
5140
 *
5141
 * - reads the contents of an http or ftp address and returns the page in a string
5142
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
5143
 * - if post_data is supplied, the value and length is posted to the given url as form data
5144
 * - URL must be supplied in lowercase
5145
 *
5146
 * @param string $url The URL
5147
 * @param string $post_data The data to post to the given URL
5148
 * @param bool $keep_alive Whether to send keepalive info
5149
 * @param int $redirection_level How many levels of redirection
5150
 * @return string|false The fetched data or false on failure
5151
 */
5152
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
5153
{
5154
	global $webmaster_email, $sourcedir;
5155
	static $keep_alive_dom = null, $keep_alive_fp = null;
5156
5157
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
5158
5159
	// No scheme? No data for you!
5160
	if (empty($match[1]))
5161
		return false;
5162
5163
	// An FTP url. We should try connecting and RETRieving it...
5164
	elseif ($match[1] == 'ftp')
5165
	{
5166
		// Include the file containing the ftp_connection class.
5167
		require_once($sourcedir . '/Class-Package.php');
5168
5169
		// Establish a connection and attempt to enable passive mode.
5170
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
5171
		if ($ftp->error !== false || !$ftp->passive())
0 ignored issues
show
introduced by
The condition $ftp->error !== false is always true.
Loading history...
5172
			return false;
5173
5174
		// I want that one *points*!
5175
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
5176
5177
		// Since passive mode worked (or we would have returned already!) open the connection.
5178
		$fp = @fsockopen($ftp->pasv['ip'], $ftp->pasv['port'], $err, $err, 5);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $err seems to be never defined.
Loading history...
5179
		if (!$fp)
5180
			return false;
5181
5182
		// The server should now say something in acknowledgement.
5183
		$ftp->check_response(150);
5184
5185
		$data = '';
5186
		while (!feof($fp))
5187
			$data .= fread($fp, 4096);
5188
		fclose($fp);
5189
5190
		// All done, right?  Good.
5191
		$ftp->check_response(226);
5192
		$ftp->close();
5193
	}
5194
5195
	// This is more likely; a standard HTTP URL.
5196
	elseif (isset($match[1]) && $match[1] == 'http')
5197
	{
5198
		// First try to use fsockopen, because it is fastest.
5199
		if ($keep_alive && $match[3] == $keep_alive_dom)
5200
			$fp = $keep_alive_fp;
5201
		if (empty($fp))
5202
		{
5203
			// Open the socket on the port we want...
5204
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
5205
		}
5206
		if (!empty($fp))
5207
		{
5208
			if ($keep_alive)
5209
			{
5210
				$keep_alive_dom = $match[3];
5211
				$keep_alive_fp = $fp;
5212
			}
5213
5214
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
5215
			if (empty($post_data))
5216
			{
5217
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
5218
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5219
				fwrite($fp, 'user-agent: PHP/SMF' . "\r\n");
5220
				if ($keep_alive)
5221
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
5222
				else
5223
					fwrite($fp, 'connection: close' . "\r\n\r\n");
5224
			}
5225
			else
5226
			{
5227
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
5228
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5229
				fwrite($fp, 'user-agent: PHP/SMF' . "\r\n");
5230
				if ($keep_alive)
5231
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
5232
				else
5233
					fwrite($fp, 'connection: close' . "\r\n");
5234
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
5235
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
5236
				fwrite($fp, $post_data);
5237
			}
5238
5239
			$response = fgets($fp, 768);
5240
5241
			// Redirect in case this location is permanently or temporarily moved.
5242
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
5243
			{
5244
				$header = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $header is dead and can be removed.
Loading history...
5245
				$location = '';
5246
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5247
					if (stripos($header, 'location:') !== false)
5248
						$location = trim(substr($header, strpos($header, ':') + 1));
5249
5250
				if (empty($location))
5251
					return false;
5252
				else
5253
				{
5254
					if (!$keep_alive)
5255
						fclose($fp);
5256
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
5257
				}
5258
			}
5259
5260
			// Make sure we get a 200 OK.
5261
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
5262
				return false;
5263
5264
			// Skip the headers...
5265
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5266
			{
5267
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
5268
					$content_length = $match[1];
5269
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
5270
				{
5271
					$keep_alive_dom = null;
5272
					$keep_alive = false;
5273
				}
5274
5275
				continue;
5276
			}
5277
5278
			$data = '';
5279
			if (isset($content_length))
5280
			{
5281
				while (!feof($fp) && strlen($data) < $content_length)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $content_length does not seem to be defined for all execution paths leading up to this point.
Loading history...
5282
					$data .= fread($fp, $content_length - strlen($data));
5283
			}
5284
			else
5285
			{
5286
				while (!feof($fp))
5287
					$data .= fread($fp, 4096);
5288
			}
5289
5290
			if (!$keep_alive)
5291
				fclose($fp);
5292
		}
5293
5294
		// If using fsockopen didn't work, try to use cURL if available.
5295
		elseif (function_exists('curl_init'))
5296
		{
5297
			// Include the file containing the curl_fetch_web_data class.
5298
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
5299
5300
			$fetch_data = new curl_fetch_web_data();
5301
			$fetch_data->get_url_data($url, $post_data);
0 ignored issues
show
Bug introduced by
$post_data of type string is incompatible with the type array expected by parameter $post_data of curl_fetch_web_data::get_url_data(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5301
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5302
5303
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5304
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5305
				$data = $fetch_data->result('body');
5306
			else
5307
				return false;
5308
		}
5309
5310
		// Neither fsockopen nor curl are available. Well, phooey.
5311
		else
5312
			return false;
5313
	}
5314
	else
5315
	{
5316
		// Umm, this shouldn't happen?
5317
		trigger_error('fetch_web_data(): Bad URL', E_USER_NOTICE);
5318
		$data = false;
5319
	}
5320
5321
	return $data;
5322
}
5323
5324
/**
5325
 * Prepares an array of "likes" info for the topic specified by $topic
5326
 *
5327
 * @param integer $topic The topic ID to fetch the info from.
5328
 * @return array An array of IDs of messages in the specified topic that the current user likes
5329
 */
5330
function prepareLikesContext($topic)
5331
{
5332
	global $user_info, $smcFunc;
5333
5334
	// Make sure we have something to work with.
5335
	if (empty($topic))
5336
		return array();
5337
5338
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
5339
	$user = $user_info['id'];
5340
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
5341
	$ttl = 180;
5342
5343
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
0 ignored issues
show
introduced by
The condition $temp = cache_get_data($cache_key, $ttl) === null is always false.
Loading history...
5344
	{
5345
		$temp = array();
5346
		$request = $smcFunc['db_query']('', '
5347
			SELECT content_id
5348
			FROM {db_prefix}user_likes AS l
5349
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
5350
			WHERE l.id_member = {int:current_user}
5351
				AND l.content_type = {literal:msg}
5352
				AND m.id_topic = {int:topic}',
5353
			array(
5354
				'current_user' => $user,
5355
				'topic' => $topic,
5356
			)
5357
		);
5358
		while ($row = $smcFunc['db_fetch_assoc']($request))
5359
			$temp[] = (int) $row['content_id'];
5360
5361
		cache_put_data($cache_key, $temp, $ttl);
5362
	}
5363
5364
	return $temp;
5365
}
5366
5367
/**
5368
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
5369
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
5370
 * that are not normally displayable.  This converts the popular ones that
5371
 * appear from a cut and paste from windows.
5372
 *
5373
 * @param string $string The string
5374
 * @return string The sanitized string
5375
 */
5376
function sanitizeMSCutPaste($string)
5377
{
5378
	global $context;
5379
5380
	if (empty($string))
5381
		return $string;
5382
5383
	// UTF-8 occurences of MS special characters
5384
	$findchars_utf8 = array(
5385
		"\xe2\x80\x9a",	// single low-9 quotation mark
5386
		"\xe2\x80\x9e",	// double low-9 quotation mark
5387
		"\xe2\x80\xa6",	// horizontal ellipsis
5388
		"\xe2\x80\x98",	// left single curly quote
5389
		"\xe2\x80\x99",	// right single curly quote
5390
		"\xe2\x80\x9c",	// left double curly quote
5391
		"\xe2\x80\x9d",	// right double curly quote
5392
		"\xe2\x80\x93",	// en dash
5393
		"\xe2\x80\x94",	// em dash
5394
	);
5395
5396
	// windows 1252 / iso equivalents
5397
	$findchars_iso = array(
5398
		chr(130),
5399
		chr(132),
5400
		chr(133),
5401
		chr(145),
5402
		chr(146),
5403
		chr(147),
5404
		chr(148),
5405
		chr(150),
5406
		chr(151),
5407
	);
5408
5409
	// safe replacements
5410
	$replacechars = array(
5411
		',',	// &sbquo;
5412
		',,',	// &bdquo;
5413
		'...',	// &hellip;
5414
		"'",	// &lsquo;
5415
		"'",	// &rsquo;
5416
		'"',	// &ldquo;
5417
		'"',	// &rdquo;
5418
		'-',	// &ndash;
5419
		'--',	// &mdash;
5420
	);
5421
5422
	if ($context['utf8'])
5423
		$string = str_replace($findchars_utf8, $replacechars, $string);
5424
	else
5425
		$string = str_replace($findchars_iso, $replacechars, $string);
5426
5427
	return $string;
5428
}
5429
5430
/**
5431
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
5432
 *
5433
 * Callback function for preg_replace_callback in subs-members
5434
 * Uses capture group 2 in the supplied array
5435
 * Does basic scan to ensure characters are inside a valid range
5436
 *
5437
 * @param array $matches An array of matches (relevant info should be the 3rd item)
5438
 * @return string A fixed string
5439
 */
5440
function replaceEntities__callback($matches)
5441
{
5442
	global $context;
5443
5444
	if (!isset($matches[2]))
5445
		return '';
5446
5447
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
0 ignored issues
show
Bug introduced by
$matches[2] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5447
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5448
5449
	// remove left to right / right to left overrides
5450
	if ($num === 0x202D || $num === 0x202E)
5451
		return '';
5452
5453
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5454
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5455
		return '&#' . $num . ';';
5456
5457
	if (empty($context['utf8']))
5458
	{
5459
		// no control characters
5460
		if ($num < 0x20)
5461
			return '';
5462
		// text is text
5463
		elseif ($num < 0x80)
5464
			return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $ascii of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5464
			return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5465
		// all others get html-ised
5466
		else
5467
			return '&#' . $matches[2] . ';';
0 ignored issues
show
Bug introduced by
Are you sure $matches[2] of type array can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5467
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
5468
	}
5469
	else
5470
	{
5471
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
5472
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
5473
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
5474
			return '';
5475
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5476
		elseif ($num < 0x80)
5477
			return chr($num);
5478
		// <0x800 (2048)
5479
		elseif ($num < 0x800)
5480
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5481
		// < 0x10000 (65536)
5482
		elseif ($num < 0x10000)
5483
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5484
		// <= 0x10FFFF (1114111)
5485
		else
5486
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5487
	}
5488
}
5489
5490
/**
5491
 * Converts html entities to utf8 equivalents
5492
 *
5493
 * Callback function for preg_replace_callback
5494
 * Uses capture group 1 in the supplied array
5495
 * Does basic checks to keep characters inside a viewable range.
5496
 *
5497
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
5498
 * @return string The fixed string
5499
 */
5500
function fixchar__callback($matches)
5501
{
5502
	if (!isset($matches[1]))
5503
		return '';
5504
5505
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
0 ignored issues
show
Bug introduced by
$matches[1] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5505
	$num = $matches[1][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[1], 1)) : (int) $matches[1];
Loading history...
5506
5507
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
5508
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
5509
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
5510
		return '';
5511
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5512
	elseif ($num < 0x80)
5513
		return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $ascii of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5513
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
5514
	// <0x800 (2048)
5515
	elseif ($num < 0x800)
5516
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5517
	// < 0x10000 (65536)
5518
	elseif ($num < 0x10000)
5519
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5520
	// <= 0x10FFFF (1114111)
5521
	else
5522
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5523
}
5524
5525
/**
5526
 * Strips out invalid html entities, replaces others with html style &#123; codes
5527
 *
5528
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5529
 * strpos, strlen, substr etc
5530
 *
5531
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5532
 * @return string The fixed string
5533
 */
5534
function entity_fix__callback($matches)
5535
{
5536
	if (!isset($matches[2]))
5537
		return '';
5538
5539
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
0 ignored issues
show
Bug introduced by
$matches[2] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5539
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
5540
5541
	// we don't allow control characters, characters out of range, byte markers, etc
5542
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
5543
		return '';
5544
	else
5545
		return '&#' . $num . ';';
5546
}
5547
5548
/**
5549
 * Return a Gravatar URL based on
5550
 * - the supplied email address,
5551
 * - the global maximum rating,
5552
 * - the global default fallback,
5553
 * - maximum sizes as set in the admin panel.
5554
 *
5555
 * It is SSL aware, and caches most of the parameters.
5556
 *
5557
 * @param string $email_address The user's email address
5558
 * @return string The gravatar URL
5559
 */
5560
function get_gravatar_url($email_address)
5561
{
5562
	global $modSettings, $smcFunc;
5563
	static $url_params = null;
5564
5565
	if ($url_params === null)
5566
	{
5567
		$ratings = array('G', 'PG', 'R', 'X');
5568
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
5569
		$url_params = array();
5570
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
5571
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
5572
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
5573
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
5574
		if (!empty($modSettings['avatar_max_width_external']))
5575
			$size_string = (int) $modSettings['avatar_max_width_external'];
5576
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
5577
			if ((int) $modSettings['avatar_max_height_external'] < $size_string)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $size_string does not seem to be defined for all execution paths leading up to this point.
Loading history...
5578
				$size_string = $modSettings['avatar_max_height_external'];
5579
5580
		if (!empty($size_string))
5581
			$url_params[] = 's=' . $size_string;
5582
	}
5583
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
5584
5585
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
5586
}
5587
5588
/**
5589
 * Get a list of timezones.
5590
 *
5591
 * @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'.
5592
 * @return array An array of timezone info.
5593
 */
5594
function smf_list_timezones($when = 'now')
5595
{
5596
	global $smcFunc, $modSettings, $tztxt, $txt;
5597
	static $timezones = null, $lastwhen = null;
5598
5599
	// No point doing this over if we already did it once
5600
	if (!empty($timezones) && $when == $lastwhen)
5601
		return $timezones;
5602
	else
5603
		$lastwhen = $when;
5604
5605
	// Parseable datetime string?
5606
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
5607
		$when = $timestamp;
5608
5609
	// A Unix timestamp?
5610
	elseif (is_numeric($when))
5611
		$when = intval($when);
5612
5613
	// Invalid value? Just get current Unix timestamp.
5614
	else
5615
		$when = time();
5616
5617
	// We'll need these too
5618
	$date_when = date_create('@' . $when);
5619
	$later = (int) date_format(date_add($date_when, date_interval_create_from_date_string('1 year')), 'U');
0 ignored issues
show
Bug introduced by
It seems like $date_when can also be of type false; however, parameter $object of date_add() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5619
	$later = (int) date_format(date_add(/** @scrutinizer ignore-type */ $date_when, date_interval_create_from_date_string('1 year')), 'U');
Loading history...
5620
5621
	// Load up any custom time zone descriptions we might have
5622
	loadLanguage('Timezones');
5623
5624
	// Should we put time zones from certain countries at the top of the list?
5625
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5626
	$priority_tzids = array();
5627
	foreach ($priority_countries as $country)
5628
	{
5629
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
5630
		if (!empty($country_tzids))
5631
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5632
	}
5633
5634
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5635
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5636
5637
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
5638
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
0 ignored issues
show
Bug introduced by
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_diff() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5638
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), /** @scrutinizer ignore-type */ $low_priority_tzids), $low_priority_tzids);
Loading history...
Bug introduced by
It seems like $low_priority_tzids can also be of type false; however, parameter $_ of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5638
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), /** @scrutinizer ignore-type */ $low_priority_tzids);
Loading history...
Bug introduced by
It seems like timezone_identifiers_list() can also be of type false; however, parameter $array1 of array_diff() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5638
	$tzids = array_merge(array_keys($tztxt), array_diff(/** @scrutinizer ignore-type */ timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
Loading history...
5639
5640
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5641
	foreach ($tzids as $tzid)
5642
	{
5643
		// We don't want UTC right now
5644
		if ($tzid == 'UTC')
5645
			continue;
5646
5647
		$tz = timezone_open($tzid);
5648
5649
		// First, get the set of transition rules for this tzid
5650
		$tzinfo = timezone_transitions_get($tz, $when, $later);
0 ignored issues
show
Bug introduced by
It seems like $tz can also be of type false; however, parameter $object of timezone_transitions_get() does only seem to accept DateTimeZone, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5650
		$tzinfo = timezone_transitions_get(/** @scrutinizer ignore-type */ $tz, $when, $later);
Loading history...
5651
5652
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5653
		$tzkey = serialize($tzinfo);
5654
5655
		// Next, get the geographic info for this tzid
5656
		$tzgeo = timezone_location_get($tz);
0 ignored issues
show
Bug introduced by
It seems like $tz can also be of type false; however, parameter $object of timezone_location_get() does only seem to accept DateTimeZone, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5656
		$tzgeo = timezone_location_get(/** @scrutinizer ignore-type */ $tz);
Loading history...
5657
5658
		// Don't overwrite our preferred tzids
5659
		if (empty($zones[$tzkey]['tzid']))
5660
		{
5661
			$zones[$tzkey]['tzid'] = $tzid;
5662
			$zones[$tzkey]['abbr'] = $tzinfo[0]['abbr'];
5663
		}
5664
5665
		// A time zone from a prioritized country?
5666
		if (in_array($tzid, $priority_tzids))
5667
			$priority_zones[$tzkey] = true;
5668
5669
		// Keep track of the location and offset for this tzid
5670
		if (!empty($txt[$tzid]))
5671
			$zones[$tzkey]['locations'][] = $txt[$tzid];
5672
		else
5673
		{
5674
			$tzid_parts = explode('/', $tzid);
5675
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
5676
		}
5677
		$offsets[$tzkey] = $tzinfo[0]['offset'];
5678
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
5679
	}
5680
5681
	// Sort by offset then longitude
5682
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $longitudes, SORT_ASC, SORT_NUMERIC, $zones);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $longitudes does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
5683
5684
	// Build the final array of formatted values
5685
	$priority_timezones = array();
5686
	$timezones = array();
5687
	foreach ($zones as $tzkey => $tzvalue)
5688
	{
5689
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
0 ignored issues
show
Bug introduced by
It seems like timezone_open($tzvalue['tzid']) can also be of type false; however, parameter $timezone of date_timezone_set() does only seem to accept DateTimeZone, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5689
		date_timezone_set($date_when, /** @scrutinizer ignore-type */ timezone_open($tzvalue['tzid']));
Loading history...
Bug introduced by
It seems like $date_when can also be of type false; however, parameter $object of date_timezone_set() does only seem to accept DateTime, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5689
		date_timezone_set(/** @scrutinizer ignore-type */ $date_when, timezone_open($tzvalue['tzid']));
Loading history...
5690
5691
		// Use the custom description, if there is one
5692
		if (!empty($tztxt[$tzvalue['tzid']]))
5693
			$desc = $tztxt[$tzvalue['tzid']];
5694
		// Otherwise, use the list of locations (max 5, so things don't get silly)
5695
		else
5696
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
5697
5698
		// Show the UTC offset and the abbreviation, if it's something like 'MST' and not '-06'
5699
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . (!strspn($tzvalue['abbr'], '+-') ? $tzvalue['abbr'] . ' - ' : '') . $desc;
5700
5701
		if (isset($priority_zones[$tzkey]))
5702
			$priority_timezones[$tzvalue['tzid']] = $desc;
5703
		else
5704
			$timezones[$tzvalue['tzid']] = $desc;
5705
	}
5706
5707
	if (!empty($priority_timezones))
5708
		$priority_timezones[] = '-----';
5709
5710
	$timezones = array_merge(
5711
		$priority_timezones,
5712
		array('' => '(Forum Default)', 'UTC' => 'UTC - ' . $tztxt['UTC'], '-----'),
5713
		$timezones
5714
	);
5715
5716
	return $timezones;
5717
}
5718
5719
/**
5720
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
5721
 * @return string|false The IP address in binary or false
5722
 */
5723
function inet_ptod($ip_address)
5724
{
5725
	if (!isValidIP($ip_address))
5726
		return $ip_address;
5727
5728
	$bin = inet_pton($ip_address);
5729
	return $bin;
5730
}
5731
5732
/**
5733
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
5734
 * @return string|false The IP address in presentation format or false on error
5735
 */
5736
function inet_dtop($bin)
5737
{
5738
	if (empty($bin))
5739
		return '';
5740
5741
	global $db_type;
5742
5743
	if ($db_type == 'postgresql')
5744
		return $bin;
5745
5746
	$ip_address = inet_ntop($bin);
5747
5748
	return $ip_address;
5749
}
5750
5751
/**
5752
 * Safe serialize() and unserialize() replacements
5753
 *
5754
 * @license Public Domain
5755
 *
5756
 * @author anthon (dot) pang (at) gmail (dot) com
5757
 */
5758
5759
/**
5760
 * Safe serialize() replacement. Recursive
5761
 * - output a strict subset of PHP's native serialized representation
5762
 * - does not serialize objects
5763
 *
5764
 * @param mixed $value
5765
 * @return string
5766
 */
5767
function _safe_serialize($value)
5768
{
5769
	if (is_null($value))
5770
		return 'N;';
5771
5772
	if (is_bool($value))
5773
		return 'b:' . (int) $value . ';';
5774
5775
	if (is_int($value))
5776
		return 'i:' . $value . ';';
5777
5778
	if (is_float($value))
5779
		return 'd:' . str_replace(',', '.', $value) . ';';
5780
5781
	if (is_string($value))
5782
		return 's:' . strlen($value) . ':"' . $value . '";';
5783
5784
	if (is_array($value))
5785
	{
5786
		$out = '';
5787
		foreach ($value as $k => $v)
5788
			$out .= _safe_serialize($k) . _safe_serialize($v);
5789
5790
		return 'a:' . count($value) . ':{' . $out . '}';
5791
	}
5792
5793
	// safe_serialize cannot serialize resources or objects.
5794
	return false;
5795
}
5796
5797
/**
5798
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
5799
 *
5800
 * @param mixed $value
5801
 * @return string
5802
 */
5803
function safe_serialize($value)
5804
{
5805
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5806
	if (function_exists('mb_internal_encoding') &&
5807
		(((int) ini_get('mbstring.func_overload')) & 2))
5808
	{
5809
		$mbIntEnc = mb_internal_encoding();
5810
		mb_internal_encoding('ASCII');
5811
	}
5812
5813
	$out = _safe_serialize($value);
5814
5815
	if (isset($mbIntEnc))
5816
		mb_internal_encoding($mbIntEnc);
5817
5818
	return $out;
5819
}
5820
5821
/**
5822
 * Safe unserialize() replacement
5823
 * - accepts a strict subset of PHP's native serialized representation
5824
 * - does not unserialize objects
5825
 *
5826
 * @param string $str
5827
 * @return mixed
5828
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
5829
 */
5830
function _safe_unserialize($str)
5831
{
5832
	// Input  is not a string.
5833
	if (empty($str) || !is_string($str))
5834
		return false;
5835
5836
	$stack = array();
5837
	$expected = array();
5838
5839
	/*
5840
	 * states:
5841
	 *   0 - initial state, expecting a single value or array
5842
	 *   1 - terminal state
5843
	 *   2 - in array, expecting end of array or a key
5844
	 *   3 - in array, expecting value or another array
5845
	 */
5846
	$state = 0;
5847
	while ($state != 1)
5848
	{
5849
		$type = isset($str[0]) ? $str[0] : '';
5850
		if ($type == '}')
5851
			$str = substr($str, 1);
5852
5853
		elseif ($type == 'N' && $str[1] == ';')
5854
		{
5855
			$value = null;
5856
			$str = substr($str, 2);
5857
		}
5858
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
5859
		{
5860
			$value = $matches[1] == '1' ? true : false;
5861
			$str = substr($str, 4);
5862
		}
5863
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
5864
		{
5865
			$value = (int) $matches[1];
5866
			$str = $matches[2];
5867
		}
5868
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
5869
		{
5870
			$value = (float) $matches[1];
5871
			$str = $matches[3];
5872
		}
5873
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
5874
		{
5875
			$value = substr($matches[2], 0, (int) $matches[1]);
5876
			$str = substr($matches[2], (int) $matches[1] + 2);
5877
		}
5878
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
5879
		{
5880
			$expectedLength = (int) $matches[1];
5881
			$str = $matches[2];
5882
		}
5883
5884
		// Object or unknown/malformed type.
5885
		else
5886
			return false;
5887
5888
		switch ($state)
5889
		{
5890
			case 3: // In array, expecting value or another array.
5891
				if ($type == 'a')
5892
				{
5893
					$stack[] = &$list;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $list does not seem to be defined for all execution paths leading up to this point.
Loading history...
5894
					$list[$key] = array();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $key does not seem to be defined for all execution paths leading up to this point.
Loading history...
5895
					$list = &$list[$key];
5896
					$expected[] = $expectedLength;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $expectedLength does not seem to be defined for all execution paths leading up to this point.
Loading history...
5897
					$state = 2;
5898
					break;
5899
				}
5900
				if ($type != '}')
5901
				{
5902
					$list[$key] = $value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.
Loading history...
5903
					$state = 2;
5904
					break;
5905
				}
5906
5907
				// Missing array value.
5908
				return false;
5909
5910
			case 2: // in array, expecting end of array or a key
5911
				if ($type == '}')
5912
				{
5913
					// Array size is less than expected.
5914
					if (count($list) < end($expected))
5915
						return false;
5916
5917
					unset($list);
5918
					$list = &$stack[count($stack) - 1];
5919
					array_pop($stack);
5920
5921
					// Go to terminal state if we're at the end of the root array.
5922
					array_pop($expected);
5923
5924
					if (count($expected) == 0)
5925
						$state = 1;
5926
5927
					break;
5928
				}
5929
5930
				if ($type == 'i' || $type == 's')
5931
				{
5932
					// Array size exceeds expected length.
5933
					if (count($list) >= end($expected))
5934
						return false;
5935
5936
					$key = $value;
5937
					$state = 3;
5938
					break;
5939
				}
5940
5941
				// Illegal array index type.
5942
				return false;
5943
5944
			// Expecting array or value.
5945
			case 0:
5946
				if ($type == 'a')
5947
				{
5948
					$data = array();
5949
					$list = &$data;
5950
					$expected[] = $expectedLength;
5951
					$state = 2;
5952
					break;
5953
				}
5954
5955
				if ($type != '}')
5956
				{
5957
					$data = $value;
5958
					$state = 1;
5959
					break;
5960
				}
5961
5962
				// Not in array.
5963
				return false;
5964
		}
5965
	}
5966
5967
	// Trailing data in input.
5968
	if (!empty($str))
5969
		return false;
5970
5971
	return $data;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data does not seem to be defined for all execution paths leading up to this point.
Loading history...
5972
}
5973
5974
/**
5975
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
5976
 *
5977
 * @param string $str
5978
 * @return mixed
5979
 */
5980
function safe_unserialize($str)
5981
{
5982
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
5983
	if (function_exists('mb_internal_encoding') &&
5984
		(((int) ini_get('mbstring.func_overload')) & 0x02))
5985
	{
5986
		$mbIntEnc = mb_internal_encoding();
5987
		mb_internal_encoding('ASCII');
5988
	}
5989
5990
	$out = _safe_unserialize($str);
5991
5992
	if (isset($mbIntEnc))
5993
		mb_internal_encoding($mbIntEnc);
5994
5995
	return $out;
5996
}
5997
5998
/**
5999
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
6000
 *
6001
 * @param string $file The file/dir full path.
6002
 * @param int $value Not needed, added for legacy reasons.
6003
 * @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.
6004
 */
6005
function smf_chmod($file, $value = 0)
6006
{
6007
	// No file? no checks!
6008
	if (empty($file))
6009
		return false;
6010
6011
	// Already writable?
6012
	if (is_writable($file))
6013
		return true;
6014
6015
	// Do we have a file or a dir?
6016
	$isDir = is_dir($file);
6017
	$isWritable = false;
6018
6019
	// Set different modes.
6020
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
6021
6022
	foreach ($chmodValues as $val)
6023
	{
6024
		// If it's writable, break out of the loop.
6025
		if (is_writable($file))
6026
		{
6027
			$isWritable = true;
6028
			break;
6029
		}
6030
6031
		else
6032
			@chmod($file, $val);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for chmod(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

6032
			/** @scrutinizer ignore-unhandled */ @chmod($file, $val);

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...
6033
	}
6034
6035
	return $isWritable;
6036
}
6037
6038
/**
6039
 * Wrapper function for json_decode() with error handling.
6040
 *
6041
 * @param string $json The string to decode.
6042
 * @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.
6043
 * @param bool $logIt To specify if the error will be logged if theres any.
6044
 * @return array Either an empty array or the decoded data as an array.
6045
 */
6046
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
6047
{
6048
	global $txt;
6049
6050
	// Come on...
6051
	if (empty($json) || !is_string($json))
6052
		return array();
6053
6054
	$returnArray = @json_decode($json, $returnAsArray);
6055
6056
	// PHP 5.3 so no json_last_error_msg()
6057
	switch (json_last_error())
6058
	{
6059
		case JSON_ERROR_NONE:
6060
			$jsonError = false;
6061
			break;
6062
		case JSON_ERROR_DEPTH:
6063
			$jsonError = 'JSON_ERROR_DEPTH';
6064
			break;
6065
		case JSON_ERROR_STATE_MISMATCH:
6066
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
6067
			break;
6068
		case JSON_ERROR_CTRL_CHAR:
6069
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
6070
			break;
6071
		case JSON_ERROR_SYNTAX:
6072
			$jsonError = 'JSON_ERROR_SYNTAX';
6073
			break;
6074
		case JSON_ERROR_UTF8:
6075
			$jsonError = 'JSON_ERROR_UTF8';
6076
			break;
6077
		default:
6078
			$jsonError = 'unknown';
6079
			break;
6080
	}
6081
6082
	// Something went wrong!
6083
	if (!empty($jsonError) && $logIt)
6084
	{
6085
		// Being a wrapper means we lost our smf_error_handler() privileges :(
6086
		$jsonDebug = debug_backtrace();
6087
		$jsonDebug = $jsonDebug[0];
6088
		loadLanguage('Errors');
6089
6090
		if (!empty($jsonDebug))
6091
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
6092
6093
		else
6094
			log_error($txt['json_' . $jsonError], 'critical');
6095
6096
		// Everyone expects an array.
6097
		return array();
6098
	}
6099
6100
	return $returnArray;
6101
}
6102
6103
/**
6104
 * Check the given String if he is a valid IPv4 or IPv6
6105
 * return true or false
6106
 *
6107
 * @param string $IPString
6108
 *
6109
 * @return bool
6110
 */
6111
function isValidIP($IPString)
6112
{
6113
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6114
}
6115
6116
/**
6117
 * Outputs a response.
6118
 * It assumes the data is already a string.
6119
 *
6120
 * @param string $data The data to print
6121
 * @param string $type The content type. Defaults to Json.
6122
 * @return void
6123
 */
6124
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6125
{
6126
	global $db_show_debug, $modSettings;
6127
6128
	// Defensive programming anyone?
6129
	if (empty($data))
6130
		return false;
6131
6132
	// Don't need extra stuff...
6133
	$db_show_debug = false;
6134
6135
	// Kill anything else.
6136
	ob_end_clean();
6137
6138
	if (!empty($modSettings['CompressedOutput']))
6139
		@ob_start('ob_gzhandler');
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ob_start(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

6139
		/** @scrutinizer ignore-unhandled */ @ob_start('ob_gzhandler');

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...
6140
6141
	else
6142
		ob_start();
6143
6144
	// Set the header.
6145
	header($type);
6146
6147
	// Echo!
6148
	echo $data;
6149
6150
	// Done.
6151
	obExit(false);
6152
}
6153
6154
/**
6155
 * Creates an optimized regex to match all known top level domains.
6156
 *
6157
 * The optimized regex is stored in $modSettings['tld_regex'].
6158
 *
6159
 * To update the stored version of the regex to use the latest list of valid TLDs from iana.org, set
6160
 * the $update parameter to true. Updating can take some time, based on network connectivity, so it
6161
 * should normally only be done by calling this function from a background or scheduled task.
6162
 *
6163
 * If $update is not true, but the regex is missing or invalid, the regex will be regenerated from a
6164
 * hard-coded list of TLDs. This regenerated regex will be overwritten on the next scheduled update.
6165
 *
6166
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
6167
 */
6168
function set_tld_regex($update = false)
6169
{
6170
	global $sourcedir, $smcFunc, $modSettings;
6171
	static $done = false;
6172
6173
	// If we don't need to do anything, don't
6174
	if (!$update && $done)
6175
		return;
6176
6177
	// Should we get a new copy of the official list of TLDs?
6178
	if ($update)
6179
	{
6180
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
6181
6182
		// If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
6183
		// We're probably running on a server hidden in a bunker deep underground to protect it from
6184
		// marauding bandits roaming on the surface. We don't want to waste precious electricity on
6185
		// pointlessly repeating background tasks, so we'll wait until the next regularly scheduled
6186
		// update to see if civilization has been restored.
6187
		if ($tlds === false)
6188
			$postapocalypticNightmare = true;
6189
	}
6190
	// If we aren't updating and the regex is valid, we're done
6191
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
6192
	{
6193
		$done = true;
6194
		return;
6195
	}
6196
6197
	// If we successfully got an update, process the list into an array
6198
	if (!empty($tlds))
6199
	{
6200
		// Clean $tlds and convert it to an array
6201
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line)
6202
		{
6203
			$line = trim($line);
6204
			if (empty($line) || strpos($line, '#') !== false || strpos($line, ' ') !== false)
6205
				return false;
6206
			else
6207
				return true;
6208
		});
6209
6210
		// Convert Punycode to Unicode
6211
		require_once($sourcedir . '/Class-Punycode.php');
6212
		$Punycode = new Punycode();
6213
		$tlds = array_map(function($input) use ($Punycode)
6214
		{
6215
			return $Punycode->decode($input);
6216
		}, $tlds);
6217
	}
6218
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
6219
	else
6220
	{
6221
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz', 'cat',
6222
			'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post', 'pro', 'tel',
6223
			'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al', 'am', 'an', 'ao', 'aq',
6224
			'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd', 'be', 'bf', 'bg', 'bh',
6225
			'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv', 'bw', 'by', 'bz', 'ca', 'cc',
6226
			'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm', 'cn', 'co', 'cr', 'cs', 'cu', 'cv',
6227
			'cx', 'cy', 'cz', 'dd', 'de', 'dj', 'dk', 'dm', 'do', 'dz', 'ec', 'ee', 'eg', 'eh',
6228
			'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo', 'fr', 'ga', 'gb', 'gd', 'ge',
6229
			'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp', 'gq', 'gr', 'gs', 'gt', 'gu', 'gw',
6230
			'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu', 'id', 'ie', 'il', 'im', 'in', 'io', 'iq',
6231
			'ir', 'is', 'it', 'ja', 'je', 'jm', 'jo', 'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn',
6232
			'kp', 'kr', 'kw', 'ky', 'kz', 'la', 'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu',
6233
			'lv', 'ly', 'ma', 'mc', 'md', 'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp',
6234
			'mq', 'mr', 'ms', 'mt', 'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf',
6235
			'ng', 'ni', 'nl', 'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph',
6236
			'pk', 'pl', 'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru',
6237
			'rw', 'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
6238
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg', 'th',
6239
			'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tp', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua', 'ug',
6240
			'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf', 'ws', 'ye',
6241
			'yt', 'yu', 'za', 'zm', 'zw');
6242
6243
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
6244
		if (empty($postapocalypticNightmare))
6245
		{
6246
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
6247
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
6248
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
6249
			);
6250
		}
6251
	}
6252
6253
	// Get an optimized regex to match all the TLDs
6254
	$tld_regex = build_regex($tlds);
6255
6256
	// Remember the new regex in $modSettings
6257
	updateSettings(array('tld_regex' => $tld_regex));
6258
6259
	// Redundant repetition is redundant
6260
	$done = true;
6261
}
6262
6263
/**
6264
 * Creates optimized regular expressions from an array of strings.
6265
 *
6266
 * An optimized regex built using this function will be much faster than a simple regex built using
6267
 * `implode('|', $strings)` --- anywhere from several times to several orders of magnitude faster.
6268
 *
6269
 * However, the time required to build the optimized regex is approximately equal to the time it
6270
 * takes to execute the simple regex. Therefore, it is only worth calling this function if the
6271
 * resulting regex will be used more than once.
6272
 *
6273
 * Because PHP places an upper limit on the allowed length of a regex, very large arrays of $strings
6274
 * may not fit in a single regex. Normally, the excess strings will simply be dropped. However, if
6275
 * the $returnArray parameter is set to true, this function will build as many regexes as necessary
6276
 * to accommodate everything in $strings and return them in an array. You will need to iterate
6277
 * through all elements of the returned array in order to test all possible matches.
6278
 *
6279
 * @param array $strings An array of strings to make a regex for.
6280
 * @param string $delim An optional delimiter character to pass to preg_quote().
6281
 * @param bool $returnArray If true, returns an array of regexes.
6282
 * @return string|array One or more regular expressions to match any of the input strings.
6283
 */
6284
function build_regex($strings, $delim = null, $returnArray = false)
6285
{
6286
	global $smcFunc;
6287
6288
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
6289
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
6290
	{
6291
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
6292
		{
6293
			$current_encoding = mb_internal_encoding();
6294
			mb_internal_encoding($string_encoding);
6295
		}
6296
6297
		$strlen = 'mb_strlen';
6298
		$substr = 'mb_substr';
6299
	}
6300
	else
6301
	{
6302
		$strlen = $smcFunc['strlen'];
6303
		$substr = $smcFunc['substr'];
6304
	}
6305
6306
	// This recursive function creates the index array from the strings
6307
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
6308
	{
6309
		static $depth = 0;
6310
		$depth++;
6311
6312
		$first = $substr($string, 0, 1);
6313
6314
		if (empty($index[$first]))
6315
			$index[$first] = array();
6316
6317
		if ($strlen($string) > 1)
6318
		{
6319
			// Sanity check on recursion
6320
			if ($depth > 99)
6321
				$index[$first][$substr($string, 1)] = '';
6322
6323
			else
6324
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
6325
		}
6326
		else
6327
			$index[$first][''] = '';
6328
6329
		$depth--;
6330
		return $index;
6331
	};
6332
6333
	// This recursive function turns the index array into a regular expression
6334
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
6335
	{
6336
		static $depth = 0;
6337
		$depth++;
6338
6339
		// Absolute max length for a regex is 32768, but we might need wiggle room
6340
		$max_length = 30000;
6341
6342
		$regex = array();
6343
		$length = 0;
6344
6345
		foreach ($index as $key => $value)
6346
		{
6347
			$key_regex = preg_quote($key, $delim);
6348
			$new_key = $key;
6349
6350
			if (empty($value))
6351
				$sub_regex = '';
6352
			else
6353
			{
6354
				$sub_regex = $index_to_regex($value, $delim);
6355
6356
				if (count(array_keys($value)) == 1)
6357
				{
6358
					$new_key_array = explode('(?' . '>', $sub_regex);
6359
					$new_key .= $new_key_array[0];
6360
				}
6361
				else
6362
					$sub_regex = '(?' . '>' . $sub_regex . ')';
6363
			}
6364
6365
			if ($depth > 1)
6366
				$regex[$new_key] = $key_regex . $sub_regex;
6367
			else
6368
			{
6369
				if (($length += strlen($key_regex) + 1) < $max_length || empty($regex))
6370
				{
6371
					$regex[$new_key] = $key_regex . $sub_regex;
6372
					unset($index[$key]);
6373
				}
6374
				else
6375
					break;
6376
			}
6377
		}
6378
6379
		// Sort by key length and then alphabetically
6380
		uksort($regex, function($k1, $k2) use (&$strlen)
6381
		{
6382
			$l1 = $strlen($k1);
6383
			$l2 = $strlen($k2);
6384
6385
			if ($l1 == $l2)
6386
				return strcmp($k1, $k2) > 0 ? 1 : -1;
6387
			else
6388
				return $l1 > $l2 ? -1 : 1;
6389
		});
6390
6391
		$depth--;
6392
		return implode('|', $regex);
6393
	};
6394
6395
	// Now that the functions are defined, let's do this thing
6396
	$index = array();
6397
	$regex = '';
6398
6399
	foreach ($strings as $string)
6400
		$index = $add_string_to_index($string, $index);
6401
6402
	if ($returnArray === true)
6403
	{
6404
		$regex = array();
6405
		while (!empty($index))
6406
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6407
	}
6408
	else
6409
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6410
6411
	// Restore PHP's internal character encoding to whatever it was originally
6412
	if (!empty($current_encoding))
6413
		mb_internal_encoding($current_encoding);
6414
6415
	return $regex;
6416
}
6417
6418
/**
6419
 * Check if the passed url has an SSL certificate.
6420
 *
6421
 * Returns true if a cert was found & false if not.
6422
 *
6423
 * @param string $url to check, in $boardurl format (no trailing slash).
6424
 */
6425
function ssl_cert_found($url)
6426
{
6427
	// This check won't work without OpenSSL
6428
	if (!extension_loaded('openssl'))
6429
		return true;
6430
6431
	// First, strip the subfolder from the passed url, if any
6432
	$parsedurl = parse_url($url);
6433
	$url = 'ssl://' . $parsedurl['host'] . ':443';
6434
6435
	// Next, check the ssl stream context for certificate info
6436
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
6437
		$ssloptions = array("capture_peer_cert" => true);
6438
	else
6439
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
6440
6441
	$result = false;
6442
	$context = stream_context_create(array("ssl" => $ssloptions));
6443
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
6444
	if ($stream !== false)
6445
	{
6446
		$params = stream_context_get_params($stream);
6447
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
6448
	}
6449
	return $result;
6450
}
6451
6452
/**
6453
 * Check if the passed url has a redirect to https:// by querying headers.
6454
 *
6455
 * Returns true if a redirect was found & false if not.
6456
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
6457
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
6458
 *
6459
 * @param string $url to check, in $boardurl format (no trailing slash).
6460
 */
6461
function https_redirect_active($url)
6462
{
6463
	// Ask for the headers for the passed url, but via http...
6464
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
6465
	$url = str_ireplace('https://', 'http://', $url) . '/';
6466
	$headers = @get_headers($url);
6467
	if ($headers === false)
6468
		return false;
6469
6470
	// Now to see if it came back https...
6471
	// First check for a redirect status code in first row (301, 302, 307)
6472
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
6473
		return false;
6474
6475
	// Search for the location entry to confirm https
6476
	$result = false;
6477
	foreach ($headers as $header)
6478
	{
6479
		if (stristr($header, 'Location: https://') !== false)
6480
		{
6481
			$result = true;
6482
			break;
6483
		}
6484
	}
6485
	return $result;
6486
}
6487
6488
/**
6489
 * Build query_wanna_see_board and query_see_board for a userid
6490
 *
6491
 * Returns array with keys query_wanna_see_board and query_see_board
6492
 *
6493
 * @param int $userid of the user
6494
 */
6495
function build_query_board($userid)
6496
{
6497
	global $user_info, $modSettings, $smcFunc, $db_prefix;
6498
6499
	$query_part = array();
6500
	$groups = array();
6501
	$is_admin = false;
6502
	$mod_cache;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $mod_cache seems to be never defined.
Loading history...
6503
	$ignoreboards;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $ignoreboards seems to be never defined.
Loading history...
6504
6505
	// If we come from cron, we can't have a $user_info.
6506
	if (isset($user_info['id']) && $user_info['id'] == $userid)
6507
	{
6508
		$groups = $user_info['groups'];
6509
		$is_admin = $user_info['is_admin'];
6510
		$mod_cache = !empty($user_info['mod_cache']) ? $user_info['mod_cache'] : null;
0 ignored issues
show
Unused Code introduced by
The assignment to $mod_cache is dead and can be removed.
Loading history...
6511
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
6512
	}
6513
	else
6514
	{
6515
		$request = $smcFunc['db_query']('', '
6516
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
6517
			FROM {db_prefix}members AS mem
6518
			WHERE mem.id_member = {int:id_member}
6519
			LIMIT 1',
6520
			array(
6521
				'id_member' => $userid,
6522
			)
6523
		);
6524
6525
		$row = $smcFunc['db_fetch_assoc']($request);
6526
6527
		if (empty($row['additional_groups']))
6528
			$groups = array($row['id_group'], $row['id_post_group']);
6529
		else
6530
			$groups = array_merge(
6531
				array($row['id_group'], $row['id_post_group']),
6532
				explode(',', $row['additional_groups'])
6533
			);
6534
6535
		// Because history has proven that it is possible for groups to go bad - clean up in case.
6536
		foreach ($groups as $k => $v)
6537
			$groups[$k] = (int) $v;
6538
6539
		$is_admin = in_array(1, $groups);
6540
6541
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
6542
6543
		// What boards are they the moderator of?
6544
		$boards_mod = array();
6545
6546
		$request = $smcFunc['db_query']('', '
6547
			SELECT id_board
6548
			FROM {db_prefix}moderators
6549
			WHERE id_member = {int:current_member}',
6550
			array(
6551
				'current_member' => $userid,
6552
			)
6553
		);
6554
		while ($row = $smcFunc['db_fetch_assoc']($request))
6555
			$boards_mod[] = $row['id_board'];
6556
		$smcFunc['db_free_result']($request);
6557
6558
		// Can any of the groups they're in moderate any of the boards?
6559
		$request = $smcFunc['db_query']('', '
6560
			SELECT id_board
6561
			FROM {db_prefix}moderator_groups
6562
			WHERE id_group IN({array_int:groups})',
6563
			array(
6564
				'groups' => $groups,
6565
			)
6566
		);
6567
		while ($row = $smcFunc['db_fetch_assoc']($request))
6568
			$boards_mod[] = $row['id_board'];
6569
		$smcFunc['db_free_result']($request);
6570
6571
		// Just in case we've got duplicates here...
6572
		$boards_mod = array_unique($boards_mod);
6573
6574
		$mod_cache['mq'] = empty($boards_mod) ? '0=1' : 'b.id_board IN (' . implode(',', $boards_mod) . ')';
0 ignored issues
show
Comprehensibility Best Practice introduced by
$mod_cache was never initialized. Although not strictly required by PHP, it is generally a good practice to add $mod_cache = array(); before regardless.
Loading history...
6575
	}
6576
6577
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
6578
	if ($is_admin)
6579
		$query_part['query_see_board'] = '1=1';
6580
	// Otherwise just the groups in $user_info['groups'].
6581
	else
6582
	{
6583
		$query_part['query_see_board'] = '
6584
			EXISTS (
6585
				SELECT bpv.id_board
6586
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
6587
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
6588
					AND bpv.deny = 0
6589
					AND bpv.id_board = b.id_board
6590
			)';
6591
6592
		if (!empty($modSettings['deny_boards_access']))
6593
			$query_part['query_see_board'] .= '
6594
			AND NOT EXISTS (
6595
				SELECT bpv.id_board
6596
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
6597
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
6598
					AND bpv.deny = 1
6599
					AND bpv.id_board = b.id_board
6600
			)';
6601
	}
6602
6603
	// Build the list of boards they WANT to see.
6604
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
6605
6606
	// If they aren't ignoring any boards then they want to see all the boards they can see
6607
	if (empty($ignoreboards))
6608
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
6609
	// Ok I guess they don't want to see all the boards
6610
	else
6611
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
6612
6613
	return $query_part;
6614
}
6615
6616
/**
6617
 * Check if the connection is using https.
6618
 *
6619
 * @return boolean true if connection used https
6620
 */
6621
function httpsOn()
6622
{
6623
	$secure = false;
6624
6625
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
6626
		$secure = true;
6627
	elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on')
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! empty($_SERVER['HTTP_...FORWARDED_SSL'] == 'on', Probably Intended Meaning: ! empty($_SERVER['HTTP_X...ORWARDED_SSL'] == 'on')
Loading history...
6628
		$secure = true;
6629
6630
	return $secure;
6631
}
6632
6633
/**
6634
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
6635
 * with international characters (a.k.a. IRIs)
6636
 *
6637
 * @param string $iri The IRI to test.
6638
 * @param int $flags Optional flags to pass to filter_var()
6639
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
6640
 */
6641
function validate_iri($iri, $flags = null)
6642
{
6643
	$url = iri_to_url($iri);
6644
6645
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
6646
		return $iri;
6647
	else
6648
		return false;
6649
}
6650
6651
/**
6652
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
6653
 * with international characters (a.k.a. IRIs)
6654
 *
6655
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
6656
 * feed the result of this function to iri_to_url()
6657
 *
6658
 * @param string $iri The IRI to sanitize.
6659
 * @return string|bool The sanitized version of the IRI
6660
 */
6661
function sanitize_iri($iri)
6662
{
6663
	// Encode any non-ASCII characters (but not space or control characters of any sort)
6664
	$iri = preg_replace_callback('~[^\x00-\x7F\pZ\pC]~u', function($matches)
6665
	{
6666
		return rawurlencode($matches[0]);
6667
	}, $iri);
6668
6669
	// Perform normal sanitization
6670
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
6671
6672
	// Decode the non-ASCII characters
6673
	$iri = rawurldecode($iri);
6674
6675
	return $iri;
6676
}
6677
6678
/**
6679
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
6680
 *
6681
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
6682
 * standard URL encoding on the rest.
6683
 *
6684
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
6685
 * @return string|bool The URL version of the IRI.
6686
 */
6687
function iri_to_url($iri)
6688
{
6689
	global $sourcedir;
6690
6691
	$host = parse_url((strpos($iri, '://') === false ? 'http://' : '') . ltrim($iri, ':/'), PHP_URL_HOST);
6692
6693
	if (empty($host))
6694
		return $iri;
6695
6696
	// Convert the domain using the Punycode algorithm
6697
	require_once($sourcedir . '/Class-Punycode.php');
6698
	$Punycode = new Punycode();
6699
	$encoded_host = $Punycode->encode($host);
6700
	$pos = strpos($iri, $host);
6701
	$iri = substr_replace($iri, $encoded_host, $pos, strlen($host));
6702
6703
	// Encode any disallowed characters in the rest of the URL
6704
	$unescaped = array(
6705
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
6706
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
6707
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
6708
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
6709
		'%25' => '%',
6710
	);
6711
	$iri = strtr(rawurlencode($iri), $unescaped);
6712
6713
	return $iri;
6714
}
6715
6716
/**
6717
 * Decodes a URL containing encoded international characters to UTF-8
6718
 *
6719
 * Decodes any Punycode encoded characters in the domain name, then uses
6720
 * standard URL decoding on the rest.
6721
 *
6722
 * @param string $url The pure ASCII version of a URL.
6723
 * @return string|bool The UTF-8 version of the URL.
6724
 */
6725
function url_to_iri($url)
6726
{
6727
	global $sourcedir;
6728
6729
	$host = parse_url((strpos($url, '://') === false ? 'http://' : '') . ltrim($url, ':/'), PHP_URL_HOST);
6730
6731
	if (empty($host))
6732
		return $url;
6733
6734
	// Decode the domain from Punycode
6735
	require_once($sourcedir . '/Class-Punycode.php');
6736
	$Punycode = new Punycode();
6737
	$decoded_host = $Punycode->decode($host);
6738
	$pos = strpos($url, $host);
6739
	$url = substr_replace($url, $decoded_host, $pos, strlen($host));
6740
6741
	// Decode the rest of the URL
6742
	$url = rawurldecode($url);
6743
6744
	return $url;
6745
}
6746
6747
/**
6748
 * Ensures SMF's scheduled tasks are being run as intended
6749
 *
6750
 * If the admin activated the cron_is_real_cron setting, but the cron job is
6751
 * not running things at least once per day, we need to go back to SMF's default
6752
 * behaviour using "web cron" JavaScript calls.
6753
 */
6754
function check_cron()
6755
{
6756
	global $user_info, $modSettings, $smcFunc, $txt;
6757
6758
	if (empty($modSettings['cron_last_checked']))
6759
		$modSettings['cron_last_checked'] = 0;
6760
6761
	if (!empty($modSettings['cron_is_real_cron']) && time() - $modSettings['cron_last_checked'] > 84600)
6762
	{
6763
		$request = $smcFunc['db_query']('', '
6764
			SELECT time_run
6765
			FROM {db_prefix}log_scheduled_tasks
6766
			ORDER BY id_log DESC
6767
			LIMIT 1',
6768
			array()
6769
		);
6770
		list($time_run) = $smcFunc['db_fetch_row']($request);
6771
		$smcFunc['db_free_result']($request);
6772
6773
		// If it's been more than 24 hours since the last task ran, cron must not be working
6774
		if (!empty($time_run) && time() - $time_run > 84600)
6775
		{
6776
			loadLanguage('ManageScheduledTasks');
6777
			log_error($txt['cron_not_working']);
6778
			updateSettings(array('cron_is_real_cron' => 0));
6779
		}
6780
		else
6781
			updateSettings(array('cron_last_checked' => time()));
6782
	}
6783
}
6784
6785
/**
6786
 * Sends an appropriate HTTP status header based on a given status code
6787
 *
6788
 * @param int $code The status code
6789
 * @param string $status The string for the status. Set automatically if not provided.
6790
 */
6791
function send_http_status($code, $status = '')
6792
{
6793
	$statuses = array(
6794
		206 => 'Partial Content',
6795
		304 => 'Not Modified',
6796
		400 => 'Bad Request',
6797
		403 => 'Forbidden',
6798
		404 => 'Not Found',
6799
		410 => 'Gone',
6800
		500 => 'Internal Server Error',
6801
		503 => 'Service Unavailable',
6802
	);
6803
6804
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
6805
6806
	if (!isset($statuses[$code]) && empty($status))
6807
		header($protocol . ' 500 Internal Server Error');
6808
	else
6809
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
6810
}
6811
6812
/**
6813
 * Concatenates an array of strings into a grammatically correct sentence list
6814
 *
6815
 * Uses formats defined in the language files to build the list appropropriately
6816
 * for the currently loaded language.
6817
 *
6818
 * @param array $list An array of strings to concatenate.
6819
 * @return string The localized sentence list.
6820
 */
6821
function sentence_list($list)
6822
{
6823
	global $txt;
6824
6825
	// Make sure the bare necessities are defined
6826
	if (empty($txt['sentence_list_format']['n']))
6827
		$txt['sentence_list_format']['n'] = '{series}';
6828
	if (!isset($txt['sentence_list_separator']))
6829
		$txt['sentence_list_separator'] = ', ';
6830
	if (!isset($txt['sentence_list_separator_alt']))
6831
		$txt['sentence_list_separator_alt'] = '; ';
6832
6833
	// Which format should we use?
6834
	if (isset($txt['sentence_list_format'][count($list)]))
6835
		$format = $txt['sentence_list_format'][count($list)];
6836
	else
6837
		$format = $txt['sentence_list_format']['n'];
6838
6839
	// Do we want the normal separator or the alternate?
6840
	$separator = $txt['sentence_list_separator'];
6841
	foreach ($list as $item)
6842
	{
6843
		if (strpos($item, $separator) !== false)
6844
		{
6845
			$separator = $txt['sentence_list_separator_alt'];
6846
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
6847
			break;
6848
		}
6849
	}
6850
6851
	$replacements = array();
6852
6853
	// Special handling for the last items on the list
6854
	$i = 0;
6855
	while (empty($done))
6856
	{
6857
		if (strpos($format, '{'. --$i . '}') !== false)
6858
			$replacements['{'. $i . '}'] = array_pop($list);
6859
		else
6860
			$done = true;
6861
	}
6862
	unset($done);
6863
6864
	// Special handling for the first items on the list
6865
	$i = 0;
6866
	while (empty($done))
6867
	{
6868
		if (strpos($format, '{'. ++$i . '}') !== false)
6869
			$replacements['{'. $i . '}'] = array_shift($list);
6870
		else
6871
			$done = true;
6872
	}
6873
	unset($done);
6874
6875
	// Whatever is left
6876
	$replacements['{series}'] = implode($separator, $list);
6877
6878
	// Do the deed
6879
	return strtr($format, $replacements);
6880
}
6881
6882
?>