Issues (1061)

Sources/Subs.php (2 issues)

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 https://www.simplemachines.org
10
 * @copyright 2020 Simple Machines and individual contributors
11
 * @license https://www.simplemachines.org/about/smf/license.php BSD
12
 *
13
 * @version 2.1 RC2
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
				// Update the amount of members awaiting approval
93
				$result = $smcFunc['db_query']('', '
94
					SELECT COUNT(*)
95
					FROM {db_prefix}members
96
					WHERE is_activated IN ({array_int:activation_status})',
97
					array(
98
						'activation_status' => array(3, 4, 5),
99
					)
100
				);
101
102
				list ($changes['unapprovedMembers']) = $smcFunc['db_fetch_row']($result);
103
				$smcFunc['db_free_result']($result);
104
			}
105
			updateSettings($changes);
106
			break;
107
108
		case 'message':
109
			if ($parameter1 === true && $parameter2 !== null)
110
				updateSettings(array('totalMessages' => true, 'maxMsgID' => $parameter2), true);
111
			else
112
			{
113
				// SUM and MAX on a smaller table is better for InnoDB tables.
114
				$result = $smcFunc['db_query']('', '
115
					SELECT SUM(num_posts + unapproved_posts) AS total_messages, MAX(id_last_msg) AS max_msg_id
116
					FROM {db_prefix}boards
117
					WHERE redirect = {string:blank_redirect}' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
118
						AND id_board != {int:recycle_board}' : ''),
119
					array(
120
						'recycle_board' => isset($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
121
						'blank_redirect' => '',
122
					)
123
				);
124
				$row = $smcFunc['db_fetch_assoc']($result);
125
				$smcFunc['db_free_result']($result);
126
127
				updateSettings(array(
128
					'totalMessages' => $row['total_messages'] === null ? 0 : $row['total_messages'],
129
					'maxMsgID' => $row['max_msg_id'] === null ? 0 : $row['max_msg_id']
130
				));
131
			}
132
			break;
133
134
		case 'subject':
135
			// Remove the previous subject (if any).
136
			$smcFunc['db_query']('', '
137
				DELETE FROM {db_prefix}log_search_subjects
138
				WHERE id_topic = {int:id_topic}',
139
				array(
140
					'id_topic' => (int) $parameter1,
141
				)
142
			);
143
144
			// Insert the new subject.
145
			if ($parameter2 !== null)
146
			{
147
				$parameter1 = (int) $parameter1;
148
				$parameter2 = text2words($parameter2);
149
150
				$inserts = array();
151
				foreach ($parameter2 as $word)
152
					$inserts[] = array($word, $parameter1);
153
154
				if (!empty($inserts))
155
					$smcFunc['db_insert']('ignore',
156
						'{db_prefix}log_search_subjects',
157
						array('word' => 'string', 'id_topic' => 'int'),
158
						$inserts,
159
						array('word', 'id_topic')
160
					);
161
			}
162
			break;
163
164
		case 'topic':
165
			if ($parameter1 === true)
166
				updateSettings(array('totalTopics' => true), true);
167
168
			else
169
			{
170
				// Get the number of topics - a SUM is better for InnoDB tables.
171
				// We also ignore the recycle bin here because there will probably be a bunch of one-post topics there.
172
				$result = $smcFunc['db_query']('', '
173
					SELECT SUM(num_topics + unapproved_topics) AS total_topics
174
					FROM {db_prefix}boards' . (!empty($modSettings['recycle_enable']) && $modSettings['recycle_board'] > 0 ? '
175
					WHERE id_board != {int:recycle_board}' : ''),
176
					array(
177
						'recycle_board' => !empty($modSettings['recycle_board']) ? $modSettings['recycle_board'] : 0,
178
					)
179
				);
180
				$row = $smcFunc['db_fetch_assoc']($result);
181
				$smcFunc['db_free_result']($result);
182
183
				updateSettings(array('totalTopics' => $row['total_topics'] === null ? 0 : $row['total_topics']));
184
			}
185
			break;
186
187
		case 'postgroups':
188
			// Parameter two is the updated columns: we should check to see if we base groups off any of these.
189
			if ($parameter2 !== null && !in_array('posts', $parameter2))
190
				return;
191
192
			$postgroups = cache_get_data('updateStats:postgroups', 360);
193
			if ($postgroups == null || $parameter1 == null)
194
			{
195
				// Fetch the postgroups!
196
				$request = $smcFunc['db_query']('', '
197
					SELECT id_group, min_posts
198
					FROM {db_prefix}membergroups
199
					WHERE min_posts != {int:min_posts}',
200
					array(
201
						'min_posts' => -1,
202
					)
203
				);
204
				$postgroups = array();
205
				while ($row = $smcFunc['db_fetch_assoc']($request))
206
					$postgroups[$row['id_group']] = $row['min_posts'];
207
208
				$smcFunc['db_free_result']($request);
209
210
				// Sort them this way because if it's done with MySQL it causes a filesort :(.
211
				arsort($postgroups);
212
213
				cache_put_data('updateStats:postgroups', $postgroups, 360);
214
			}
215
216
			// Oh great, they've screwed their post groups.
217
			if (empty($postgroups))
218
				return;
219
220
			// Set all membergroups from most posts to least posts.
221
			$conditions = '';
222
			$lastMin = 0;
223
			foreach ($postgroups as $id => $min_posts)
224
			{
225
				$conditions .= '
226
					WHEN posts >= ' . $min_posts . (!empty($lastMin) ? ' AND posts <= ' . $lastMin : '') . ' THEN ' . $id;
227
228
				$lastMin = $min_posts;
229
			}
230
231
			// A big fat CASE WHEN... END is faster than a zillion UPDATE's ;).
232
			$smcFunc['db_query']('', '
233
				UPDATE {db_prefix}members
234
				SET id_post_group = CASE ' . $conditions . '
235
				ELSE 0
236
				END' . ($parameter1 != null ? '
237
				WHERE ' . (is_array($parameter1) ? 'id_member IN ({array_int:members})' : 'id_member = {int:members}') : ''),
238
				array(
239
					'members' => $parameter1,
240
				)
241
			);
242
			break;
243
244
		default:
245
			trigger_error('updateStats(): Invalid statistic type \'' . $type . '\'', E_USER_NOTICE);
246
	}
247
}
248
249
/**
250
 * Updates the columns in the members table.
251
 * Assumes the data has been htmlspecialchar'd.
252
 * this function should be used whenever member data needs to be
253
 * updated in place of an UPDATE query.
254
 *
255
 * id_member is either an int or an array of ints to be updated.
256
 *
257
 * data is an associative array of the columns to be updated and their respective values.
258
 * any string values updated should be quoted and slashed.
259
 *
260
 * the value of any column can be '+' or '-', which mean 'increment'
261
 * and decrement, respectively.
262
 *
263
 * if the member's post number is updated, updates their post groups.
264
 *
265
 * @param mixed $members An array of member IDs, the ID of a single member, or null to update this for all members
266
 * @param array $data The info to update for the members
267
 */
268
function updateMemberData($members, $data)
269
{
270
	global $modSettings, $user_info, $smcFunc, $sourcedir, $cache_enable;
271
272
	// An empty array means there's nobody to update.
273
	if ($members === array())
274
		return;
275
276
	$parameters = array();
277
	if (is_array($members))
278
	{
279
		$condition = 'id_member IN ({array_int:members})';
280
		$parameters['members'] = $members;
281
	}
282
283
	elseif ($members === null)
284
		$condition = '1=1';
285
286
	else
287
	{
288
		$condition = 'id_member = {int:member}';
289
		$parameters['member'] = $members;
290
	}
291
292
	// Everything is assumed to be a string unless it's in the below.
293
	$knownInts = array(
294
		'date_registered', 'posts', 'id_group', 'last_login', 'instant_messages', 'unread_messages',
295
		'new_pm', 'pm_prefs', 'gender', 'show_online', 'pm_receive_from', 'alerts',
296
		'id_theme', 'is_activated', 'id_msg_last_visit', 'id_post_group', 'total_time_logged_in', 'warning',
297
	);
298
	$knownFloats = array(
299
		'time_offset',
300
	);
301
302
	if (!empty($modSettings['integrate_change_member_data']))
303
	{
304
		// Only a few member variables are really interesting for integration.
305
		$integration_vars = array(
306
			'member_name',
307
			'real_name',
308
			'email_address',
309
			'id_group',
310
			'gender',
311
			'birthdate',
312
			'website_title',
313
			'website_url',
314
			'location',
315
			'time_format',
316
			'time_offset',
317
			'avatar',
318
			'lngfile',
319
		);
320
		$vars_to_integrate = array_intersect($integration_vars, array_keys($data));
321
322
		// Only proceed if there are any variables left to call the integration function.
323
		if (count($vars_to_integrate) != 0)
324
		{
325
			// Fetch a list of member_names if necessary
326
			if ((!is_array($members) && $members === $user_info['id']) || (is_array($members) && count($members) == 1 && in_array($user_info['id'], $members)))
327
				$member_names = array($user_info['username']);
328
			else
329
			{
330
				$member_names = array();
331
				$request = $smcFunc['db_query']('', '
332
					SELECT member_name
333
					FROM {db_prefix}members
334
					WHERE ' . $condition,
335
					$parameters
336
				);
337
				while ($row = $smcFunc['db_fetch_assoc']($request))
338
					$member_names[] = $row['member_name'];
339
				$smcFunc['db_free_result']($request);
340
			}
341
342
			if (!empty($member_names))
343
				foreach ($vars_to_integrate as $var)
344
					call_integration_hook('integrate_change_member_data', array($member_names, $var, &$data[$var], &$knownInts, &$knownFloats));
345
		}
346
	}
347
348
	$setString = '';
349
	foreach ($data as $var => $val)
350
	{
351
		switch ($var)
352
		{
353
			case  'birthdate':
354
				$type = 'date';
355
				break;
356
357
			case 'member_ip':
358
			case 'member_ip2':
359
				$type = 'inet';
360
				break;
361
362
			default:
363
				$type = 'string';
364
		}
365
366
		if (in_array($var, $knownInts))
367
			$type = 'int';
368
369
		elseif (in_array($var, $knownFloats))
370
			$type = 'float';
371
372
		// Doing an increment?
373
		if ($var == 'alerts' && ($val === '+' || $val === '-'))
374
		{
375
			include_once($sourcedir . '/Profile-Modify.php');
376
			if (is_array($members))
377
			{
378
				$val = 'CASE ';
379
				foreach ($members as $k => $v)
380
					$val .= 'WHEN id_member = ' . $v . ' THEN '. alert_count($v, true) . ' ';
381
382
				$val = $val . ' END';
383
				$type = 'raw';
384
			}
385
386
			else
387
				$val = alert_count($members, true);
388
		}
389
390
		elseif ($type == 'int' && ($val === '+' || $val === '-'))
391
		{
392
			$val = $var . ' ' . $val . ' 1';
393
			$type = 'raw';
394
		}
395
396
		// Ensure posts, instant_messages, and unread_messages don't overflow or underflow.
397
		if (in_array($var, array('posts', 'instant_messages', 'unread_messages')))
398
		{
399
			if (preg_match('~^' . $var . ' (\+ |- |\+ -)([\d]+)~', $val, $match))
400
			{
401
				if ($match[1] != '+ ')
402
					$val = 'CASE WHEN ' . $var . ' <= ' . abs($match[2]) . ' THEN 0 ELSE ' . $val . ' END';
403
404
				$type = 'raw';
405
			}
406
		}
407
408
		$setString .= ' ' . $var . ' = {' . $type . ':p_' . $var . '},';
409
		$parameters['p_' . $var] = $val;
410
	}
411
412
	$smcFunc['db_query']('', '
413
		UPDATE {db_prefix}members
414
		SET' . substr($setString, 0, -1) . '
415
		WHERE ' . $condition,
416
		$parameters
417
	);
418
419
	updateStats('postgroups', $members, array_keys($data));
420
421
	// Clear any caching?
422
	if (!empty($cache_enable) && $cache_enable >= 2 && !empty($members))
423
	{
424
		if (!is_array($members))
425
			$members = array($members);
426
427
		foreach ($members as $member)
428
		{
429
			if ($cache_enable >= 3)
430
			{
431
				cache_put_data('member_data-profile-' . $member, null, 120);
432
				cache_put_data('member_data-normal-' . $member, null, 120);
433
				cache_put_data('member_data-minimal-' . $member, null, 120);
434
			}
435
			cache_put_data('user_settings-' . $member, null, 60);
436
		}
437
	}
438
}
439
440
/**
441
 * Updates the settings table as well as $modSettings... only does one at a time if $update is true.
442
 *
443
 * - updates both the settings table and $modSettings array.
444
 * - all of changeArray's indexes and values are assumed to have escaped apostrophes (')!
445
 * - if a variable is already set to what you want to change it to, that
446
 *   variable will be skipped over; it would be unnecessary to reset.
447
 * - When use_update is true, UPDATEs will be used instead of REPLACE.
448
 * - when use_update is true, the value can be true or false to increment
449
 *  or decrement it, respectively.
450
 *
451
 * @param array $changeArray An array of info about what we're changing in 'setting' => 'value' format
452
 * @param bool $update Whether to use an UPDATE query instead of a REPLACE query
453
 */
454
function updateSettings($changeArray, $update = false)
455
{
456
	global $modSettings, $smcFunc;
457
458
	if (empty($changeArray) || !is_array($changeArray))
459
		return;
460
461
	$toRemove = array();
462
463
	// Go check if there is any setting to be removed.
464
	foreach ($changeArray as $k => $v)
465
		if ($v === null)
466
		{
467
			// Found some, remove them from the original array and add them to ours.
468
			unset($changeArray[$k]);
469
			$toRemove[] = $k;
470
		}
471
472
	// Proceed with the deletion.
473
	if (!empty($toRemove))
474
		$smcFunc['db_query']('', '
475
			DELETE FROM {db_prefix}settings
476
			WHERE variable IN ({array_string:remove})',
477
			array(
478
				'remove' => $toRemove,
479
			)
480
		);
481
482
	// In some cases, this may be better and faster, but for large sets we don't want so many UPDATEs.
483
	if ($update)
484
	{
485
		foreach ($changeArray as $variable => $value)
486
		{
487
			$smcFunc['db_query']('', '
488
				UPDATE {db_prefix}settings
489
				SET value = {' . ($value === false || $value === true ? 'raw' : 'string') . ':value}
490
				WHERE variable = {string:variable}',
491
				array(
492
					'value' => $value === true ? 'value + 1' : ($value === false ? 'value - 1' : $value),
493
					'variable' => $variable,
494
				)
495
			);
496
			$modSettings[$variable] = $value === true ? $modSettings[$variable] + 1 : ($value === false ? $modSettings[$variable] - 1 : $value);
497
		}
498
499
		// Clean out the cache and make sure the cobwebs are gone too.
500
		cache_put_data('modSettings', null, 90);
501
502
		return;
503
	}
504
505
	$replaceArray = array();
506
	foreach ($changeArray as $variable => $value)
507
	{
508
		// Don't bother if it's already like that ;).
509
		if (isset($modSettings[$variable]) && $modSettings[$variable] == $value)
510
			continue;
511
		// If the variable isn't set, but would only be set to nothing'ness, then don't bother setting it.
512
		elseif (!isset($modSettings[$variable]) && empty($value))
513
			continue;
514
515
		$replaceArray[] = array($variable, $value);
516
517
		$modSettings[$variable] = $value;
518
	}
519
520
	if (empty($replaceArray))
521
		return;
522
523
	$smcFunc['db_insert']('replace',
524
		'{db_prefix}settings',
525
		array('variable' => 'string-255', 'value' => 'string-65534'),
526
		$replaceArray,
527
		array('variable')
528
	);
529
530
	// Kill the cache - it needs redoing now, but we won't bother ourselves with that here.
531
	cache_put_data('modSettings', null, 90);
532
}
533
534
/**
535
 * Constructs a page list.
536
 *
537
 * - builds the page list, e.g. 1 ... 6 7 [8] 9 10 ... 15.
538
 * - flexible_start causes it to use "url.page" instead of "url;start=page".
539
 * - very importantly, cleans up the start value passed, and forces it to
540
 *   be a multiple of num_per_page.
541
 * - checks that start is not more than max_value.
542
 * - base_url should be the URL without any start parameter on it.
543
 * - uses the compactTopicPagesEnable and compactTopicPagesContiguous
544
 *   settings to decide how to display the menu.
545
 *
546
 * an example is available near the function definition.
547
 * $pageindex = constructPageIndex($scripturl . '?board=' . $board, $_REQUEST['start'], $num_messages, $maxindex, true);
548
 *
549
 * @param string $base_url The basic URL to be used for each link.
550
 * @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.
551
 * @param int $max_value The total number of items you are paginating for.
552
 * @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.
553
 * @param bool $flexible_start Whether a ;start=x component should be introduced into the URL automatically (see above)
554
 * @param bool $show_prevnext Whether the Previous and Next links should be shown (should be on only when navigating the list)
555
 *
556
 * @return string The complete HTML of the page index that was requested, formatted by the template.
557
 */
558
function constructPageIndex($base_url, &$start, $max_value, $num_per_page, $flexible_start = false, $show_prevnext = true)
559
{
560
	global $modSettings, $context, $smcFunc, $settings, $txt;
561
562
	// Save whether $start was less than 0 or not.
563
	$start = (int) $start;
564
	$start_invalid = $start < 0;
565
566
	// Make sure $start is a proper variable - not less than 0.
567
	if ($start_invalid)
568
		$start = 0;
569
	// Not greater than the upper bound.
570
	elseif ($start >= $max_value)
571
		$start = max(0, (int) $max_value - (((int) $max_value % (int) $num_per_page) == 0 ? $num_per_page : ((int) $max_value % (int) $num_per_page)));
572
	// And it has to be a multiple of $num_per_page!
573
	else
574
		$start = max(0, (int) $start - ((int) $start % (int) $num_per_page));
575
576
	$context['current_page'] = $start / $num_per_page;
577
578
	// Define some default page index settings if we don't already have it...
579
	if (!isset($settings['page_index']))
580
	{
581
		// This defines the formatting for the page indexes used throughout the forum.
582
		$settings['page_index'] = array(
583
			'extra_before' => '<span class="pages">' . $txt['pages'] . '</span>',
584
			'previous_page' => '<span class="main_icons previous_page"></span>',
585
			'current_page' => '<span class="current_page">%1$d</span> ',
586
			'page' => '<a class="nav_page" href="{URL}">%2$s</a> ',
587
			'expand_pages' => '<span class="expand_pages" onclick="expandPages(this, {LINK}, {FIRST_PAGE}, {LAST_PAGE}, {PER_PAGE});"> ... </span>',
588
			'next_page' => '<span class="main_icons next_page"></span>',
589
			'extra_after' => '',
590
		);
591
	}
592
593
	$base_link = strtr($settings['page_index']['page'], array('{URL}' => $flexible_start ? $base_url : strtr($base_url, array('%' => '%%')) . ';start=%1$d'));
594
	$pageindex = $settings['page_index']['extra_before'];
595
596
	// Compact pages is off or on?
597
	if (empty($modSettings['compactTopicPagesEnable']))
598
	{
599
		// Show the left arrow.
600
		$pageindex .= $start == 0 ? ' ' : sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
601
602
		// Show all the pages.
603
		$display_page = 1;
604
		for ($counter = 0; $counter < $max_value; $counter += $num_per_page)
605
			$pageindex .= $start == $counter && !$start_invalid ? sprintf($settings['page_index']['current_page'], $display_page++) : sprintf($base_link, $counter, $display_page++);
606
607
		// Show the right arrow.
608
		$display_page = ($start + $num_per_page) > $max_value ? $max_value : ($start + $num_per_page);
609
		if ($start != $counter - $max_value && !$start_invalid)
610
			$pageindex .= $display_page > $counter - $num_per_page ? ' ' : sprintf($base_link, $display_page, $settings['page_index']['next_page']);
611
	}
612
	else
613
	{
614
		// If they didn't enter an odd value, pretend they did.
615
		$PageContiguous = (int) ($modSettings['compactTopicPagesContiguous'] - ($modSettings['compactTopicPagesContiguous'] % 2)) / 2;
616
617
		// Show the "prev page" link. (>prev page< 1 ... 6 7 [8] 9 10 ... 15 next page)
618
		if (!empty($start) && $show_prevnext)
619
			$pageindex .= sprintf($base_link, $start - $num_per_page, $settings['page_index']['previous_page']);
620
		else
621
			$pageindex .= '';
622
623
		// Show the first page. (prev page >1< ... 6 7 [8] 9 10 ... 15)
624
		if ($start > $num_per_page * $PageContiguous)
625
			$pageindex .= sprintf($base_link, 0, '1');
626
627
		// Show the ... after the first page.  (prev page 1 >...< 6 7 [8] 9 10 ... 15 next page)
628
		if ($start > $num_per_page * ($PageContiguous + 1))
629
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
630
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
631
				'{FIRST_PAGE}' => $num_per_page,
632
				'{LAST_PAGE}' => $start - $num_per_page * $PageContiguous,
633
				'{PER_PAGE}' => $num_per_page,
634
			));
635
636
		// Show the pages before the current one. (prev page 1 ... >6 7< [8] 9 10 ... 15 next page)
637
		for ($nCont = $PageContiguous; $nCont >= 1; $nCont--)
638
			if ($start >= $num_per_page * $nCont)
639
			{
640
				$tmpStart = $start - $num_per_page * $nCont;
641
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
642
			}
643
644
		// Show the current page. (prev page 1 ... 6 7 >[8]< 9 10 ... 15 next page)
645
		if (!$start_invalid)
646
			$pageindex .= sprintf($settings['page_index']['current_page'], $start / $num_per_page + 1);
647
		else
648
			$pageindex .= sprintf($base_link, $start, $start / $num_per_page + 1);
649
650
		// Show the pages after the current one... (prev page 1 ... 6 7 [8] >9 10< ... 15 next page)
651
		$tmpMaxPages = (int) (($max_value - 1) / $num_per_page) * $num_per_page;
652
		for ($nCont = 1; $nCont <= $PageContiguous; $nCont++)
653
			if ($start + $num_per_page * $nCont <= $tmpMaxPages)
654
			{
655
				$tmpStart = $start + $num_per_page * $nCont;
656
				$pageindex .= sprintf($base_link, $tmpStart, $tmpStart / $num_per_page + 1);
657
			}
658
659
		// Show the '...' part near the end. (prev page 1 ... 6 7 [8] 9 10 >...< 15 next page)
660
		if ($start + $num_per_page * ($PageContiguous + 1) < $tmpMaxPages)
661
			$pageindex .= strtr($settings['page_index']['expand_pages'], array(
662
				'{LINK}' => JavaScriptEscape($smcFunc['htmlspecialchars']($base_link)),
663
				'{FIRST_PAGE}' => $start + $num_per_page * ($PageContiguous + 1),
664
				'{LAST_PAGE}' => $tmpMaxPages,
665
				'{PER_PAGE}' => $num_per_page,
666
			));
667
668
		// Show the last number in the list. (prev page 1 ... 6 7 [8] 9 10 ... >15<  next page)
669
		if ($start + $num_per_page * $PageContiguous < $tmpMaxPages)
670
			$pageindex .= sprintf($base_link, $tmpMaxPages, $tmpMaxPages / $num_per_page + 1);
671
672
		// Show the "next page" link. (prev page 1 ... 6 7 [8] 9 10 ... 15 >next page<)
673
		if ($start != $tmpMaxPages && $show_prevnext)
674
			$pageindex .= sprintf($base_link, $start + $num_per_page, $settings['page_index']['next_page']);
675
	}
676
	$pageindex .= $settings['page_index']['extra_after'];
677
678
	return $pageindex;
679
}
680
681
/**
682
 * - Formats a number.
683
 * - uses the format of number_format to decide how to format the number.
684
 *   for example, it might display "1 234,50".
685
 * - caches the formatting data from the setting for optimization.
686
 *
687
 * @param float $number A number
688
 * @param bool|int $override_decimal_count If set, will use the specified number of decimal places. Otherwise it's automatically determined
689
 * @return string A formatted number
690
 */
691
function comma_format($number, $override_decimal_count = false)
692
{
693
	global $txt;
694
	static $thousands_separator = null, $decimal_separator = null, $decimal_count = null;
695
696
	// Cache these values...
697
	if ($decimal_separator === null)
698
	{
699
		// Not set for whatever reason?
700
		if (empty($txt['number_format']) || preg_match('~^1([^\d]*)?234([^\d]*)(0*?)$~', $txt['number_format'], $matches) != 1)
701
			return $number;
702
703
		// Cache these each load...
704
		$thousands_separator = $matches[1];
705
		$decimal_separator = $matches[2];
706
		$decimal_count = strlen($matches[3]);
707
	}
708
709
	// Format the string with our friend, number_format.
710
	return number_format($number, (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
711
}
712
713
/**
714
 * Format a time to make it look purdy.
715
 *
716
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
717
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
718
 * - if todayMod is set and show_today was not not specified or true, an
719
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
720
 * - performs localization (more than just strftime would do alone.)
721
 *
722
 * @param int $log_time A timestamp
723
 * @param bool $show_today Whether to show "Today"/"Yesterday" or just a date
724
 * @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.
725
 * @param bool $process_safe Activate setlocale check for changes at runtime. Slower, but safer.
726
 * @return string A formatted timestamp
727
 */
728
function timeformat($log_time, $show_today = true, $offset_type = false, $process_safe = false)
729
{
730
	global $context, $user_info, $txt, $modSettings;
731
	static $non_twelve_hour, $locale, $now;
732
	static $unsupportedFormats, $finalizedFormats;
733
734
	$unsupportedFormatsWindows = array('z', 'Z');
735
736
	// Ensure required values are set
737
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
738
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
739
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
740
741
	// Offset the time.
742
	if (!$offset_type)
743
		$log_time = $log_time + ($user_info['time_offset'] + $modSettings['time_offset']) * 3600;
744
	// Just the forum offset?
745
	elseif ($offset_type == 'forum')
746
		$log_time = $log_time + $modSettings['time_offset'] * 3600;
747
748
	// We can't have a negative date (on Windows, at least.)
749
	if ($log_time < 0)
750
		$log_time = 0;
751
752
	// Today and Yesterday?
753
	$prefix = '';
754
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
755
	{
756
		$now_time = forum_time();
757
758
		if ($now_time - $log_time < (86400 * $modSettings['todayMod']))
759
		{
760
			$then = @getdate($log_time);
761
			$now = (!empty($now) ? $now : @getdate($now_time));
762
763
			// Same day of the year, same year.... Today!
764
			if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
765
			{
766
				$prefix = $txt['today'];
767
			}
768
			// 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...
769
			elseif ($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))
770
			{
771
				$prefix = $txt['yesterday'];
772
			}
773
		}
774
	}
775
776
	$str = !is_bool($show_today) ? $show_today : $user_info['time_format'];
777
778
	// Use the cached formats if available
779
	if (is_null($finalizedFormats))
780
		$finalizedFormats = (array) cache_get_data('timeformatstrings', 86400);
781
782
	if (!isset($finalizedFormats[$str]) || !is_array($finalizedFormats[$str]))
783
		$finalizedFormats[$str] = array();
784
785
	// Make a supported version for this format if we don't already have one
786
	$format_type = !empty($prefix) ? 'time_only' : 'normal';
787
	if (empty($finalizedFormats[$str][$format_type]))
788
	{
789
		$timeformat = $format_type == 'time_only' ? get_date_or_time_format('time', $str) : $str;
790
791
		// Not all systems support all formats, and Windows fails altogether if unsupported ones are
792
		// used, so let's prevent that. Some substitutions go to the nearest reasonable fallback, some
793
		// turn into static strings, some (i.e. %a, %A, %b, %B, %p) have special handling below.
794
		$strftimeFormatSubstitutions = array(
795
			// Day
796
			'a' => '#txt_days_short_%w#', 'A' => '#txt_days_%w#', 'e' => '%d', 'd' => '&#37;d', 'j' => '&#37;j', 'u' => '%w', 'w' => '&#37;w',
797
			// Week
798
			'U' => '&#37;U', 'V' => '%U', 'W' => '%U',
799
			// Month
800
			'b' => '#txt_months_short_%m#', 'B' => '#txt_months_%m#', 'h' => '%b', 'm' => '&#37;m',
801
			// Year
802
			'C' => '&#37;C', 'g' => '%y', 'G' => '%Y', 'y' => '&#37;y', 'Y' => '&#37;Y',
803
			// Time
804
			'H' => '&#37;H', 'k' => '%H', 'I' => '%H', 'l' => '%I', 'M' => '&#37;M', 'p' => '&#37;p', 'P' => '%p',
805
			'r' => '%I:%M:%S %p', 'R' => '%H:%M', 'S' => '&#37;S', 'T' => '%H:%M:%S', 'X' => '%T', 'z' => '&#37;z', 'Z' => '&#37;Z',
806
			// Time and Date Stamps
807
			'c' => '%F %T', 'D' => '%m/%d/%y', 'F' => '%Y-%m-%d', 's' => '&#37;s', 'x' => '%F',
808
			// Miscellaneous
809
			'n' => "\n", 't' => "\t", '%' => '&#37;',
810
		);
811
812
		// No need to do this part again if we already did it once
813
		if (is_null($unsupportedFormats))
814
			$unsupportedFormats = (array) cache_get_data('unsupportedtimeformats', 86400);
815
		if (empty($unsupportedFormats))
816
		{
817
			foreach ($strftimeFormatSubstitutions as $format => $substitution)
818
			{
819
				// Avoid a crashing bug with PHP 7 on certain versions of Windows
820
				if ($context['server']['is_windows'] && in_array($format, $unsupportedFormatsWindows))
821
				{
822
					$unsupportedFormats[] = $format;
823
					continue;
824
				}
825
826
				$value = @strftime('%' . $format);
827
828
				// Windows will return false for unsupported formats
829
				// Other operating systems return the format string as a literal
830
				if ($value === false || $value === $format)
831
					$unsupportedFormats[] = $format;
832
			}
833
			cache_put_data('unsupportedtimeformats', $unsupportedFormats, 86400);
834
		}
835
836
		// Windows needs extra help if $timeformat contains something completely invalid, e.g. '%Q'
837
		if (DIRECTORY_SEPARATOR === '\\')
838
			$timeformat = preg_replace('~%(?!' . implode('|', array_keys($strftimeFormatSubstitutions)) . ')~', '&#37;', $timeformat);
839
840
		// Substitute unsupported formats with supported ones
841
		if (!empty($unsupportedFormats))
842
			while (preg_match('~%(' . implode('|', $unsupportedFormats) . ')~', $timeformat, $matches))
843
				$timeformat = str_replace($matches[0], $strftimeFormatSubstitutions[$matches[1]], $timeformat);
844
845
		// Remember this so we don't need to do it again
846
		$finalizedFormats[$str][$format_type] = $timeformat;
847
		cache_put_data('timeformatstrings', $finalizedFormats, 86400);
848
	}
849
850
	$timeformat = $finalizedFormats[$str][$format_type];
851
852
	// Make sure we are using the correct locale.
853
	if (!isset($locale) || ($process_safe === true && setlocale(LC_TIME, '0') != $locale))
854
		$locale = setlocale(LC_TIME, array($txt['lang_locale'] . '.' . $modSettings['global_character_set'], $txt['lang_locale'] . '.' . $txt['lang_character_set'], $txt['lang_locale']));
855
856
	// If the current locale is unsupported, we'll have to localize the hard way.
857
	if ($locale === false)
858
	{
859
		$timeformat = strtr($timeformat, array(
860
			'%a' => '#txt_days_short_%w#',
861
			'%A' => '#txt_days_%w#',
862
			'%b' => '#txt_months_short_%m#',
863
			'%B' => '#txt_months_%m#',
864
			'%p' => '&#37;p',
865
			'%P' => '&#37;p'
866
		));
867
	}
868
	// Just in case the locale doesn't support '%p' properly.
869
	// @todo Is this even necessary?
870
	else
871
	{
872
		if (!isset($non_twelve_hour) && strpos($timeformat, '%p') !== false)
873
			$non_twelve_hour = trim(strftime('%p')) === '';
874
875
		if (!empty($non_twelve_hour))
876
			$timeformat = strtr($timeformat, array(
877
				'%p' => '&#37;p',
878
				'%P' => '&#37;p'
879
			));
880
	}
881
882
	// And now, the moment we've all be waiting for...
883
	$timestring = strftime($timeformat, $log_time);
884
885
	// Do-it-yourself time localization.  Fun.
886
	if (strpos($timestring, '&#37;p') !== false)
887
		$timestring = str_replace('&#37;p', (strftime('%H', $log_time) < 12 ? $txt['time_am'] : $txt['time_pm']), $timestring);
888
	if (strpos($timestring, '#txt_') !== false)
889
	{
890
		if (strpos($timestring, '#txt_days_short_') !== false)
891
			$timestring = strtr($timestring, array(
892
				'#txt_days_short_0#' => $txt['days_short'][0],
893
				'#txt_days_short_1#' => $txt['days_short'][1],
894
				'#txt_days_short_2#' => $txt['days_short'][2],
895
				'#txt_days_short_3#' => $txt['days_short'][3],
896
				'#txt_days_short_4#' => $txt['days_short'][4],
897
				'#txt_days_short_5#' => $txt['days_short'][5],
898
				'#txt_days_short_6#' => $txt['days_short'][6],
899
			));
900
901
		if (strpos($timestring, '#txt_days_') !== false)
902
			$timestring = strtr($timestring, array(
903
				'#txt_days_0#' => $txt['days'][0],
904
				'#txt_days_1#' => $txt['days'][1],
905
				'#txt_days_2#' => $txt['days'][2],
906
				'#txt_days_3#' => $txt['days'][3],
907
				'#txt_days_4#' => $txt['days'][4],
908
				'#txt_days_5#' => $txt['days'][5],
909
				'#txt_days_6#' => $txt['days'][6],
910
			));
911
912
		if (strpos($timestring, '#txt_months_short_') !== false)
913
			$timestring = strtr($timestring, array(
914
				'#txt_months_short_01#' => $txt['months_short'][1],
915
				'#txt_months_short_02#' => $txt['months_short'][2],
916
				'#txt_months_short_03#' => $txt['months_short'][3],
917
				'#txt_months_short_04#' => $txt['months_short'][4],
918
				'#txt_months_short_05#' => $txt['months_short'][5],
919
				'#txt_months_short_06#' => $txt['months_short'][6],
920
				'#txt_months_short_07#' => $txt['months_short'][7],
921
				'#txt_months_short_08#' => $txt['months_short'][8],
922
				'#txt_months_short_09#' => $txt['months_short'][9],
923
				'#txt_months_short_10#' => $txt['months_short'][10],
924
				'#txt_months_short_11#' => $txt['months_short'][11],
925
				'#txt_months_short_12#' => $txt['months_short'][12],
926
			));
927
928
		if (strpos($timestring, '#txt_months_') !== false)
929
			$timestring = strtr($timestring, array(
930
				'#txt_months_01#' => $txt['months'][1],
931
				'#txt_months_02#' => $txt['months'][2],
932
				'#txt_months_03#' => $txt['months'][3],
933
				'#txt_months_04#' => $txt['months'][4],
934
				'#txt_months_05#' => $txt['months'][5],
935
				'#txt_months_06#' => $txt['months'][6],
936
				'#txt_months_07#' => $txt['months'][7],
937
				'#txt_months_08#' => $txt['months'][8],
938
				'#txt_months_09#' => $txt['months'][9],
939
				'#txt_months_10#' => $txt['months'][10],
940
				'#txt_months_11#' => $txt['months'][11],
941
				'#txt_months_12#' => $txt['months'][12],
942
			));
943
	}
944
945
	// Restore any literal percent characters, add the prefix, and we're done.
946
	return $prefix . str_replace('&#37;', '%', $timestring);
947
}
948
949
/**
950
 * Gets a version of a strftime() format that only shows the date or time components
951
 *
952
 * @param string $type Either 'date' or 'time'.
953
 * @param string $format A strftime() format to process. Defaults to $user_info['time_format'].
954
 * @return string A strftime() format string
955
 */
956
function get_date_or_time_format($type = '', $format = '')
957
{
958
	global $user_info, $modSettings;
959
	static $formats;
960
961
	// If the format is invalid, fall back to defaults.
962
	if (strpos($format, '%') === false)
963
		$format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M');
964
965
	$orig_format = $format;
966
967
	// Have we already done this?
968
	if (isset($formats[$orig_format][$type]))
969
		return $formats[$orig_format][$type];
970
971
	if ($type === 'date')
972
	{
973
		$specifications = array(
974
			// Day
975
			'%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w',
976
			// Week
977
			'%U' => '%U', '%V' => '%V', '%W' => '%W',
978
			// Month
979
			'%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m',
980
			// Year
981
			'%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y',
982
			// Time
983
			'%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '',
984
			'%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '',
985
			// Time and Date Stamps
986
			'%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x',
987
			// Miscellaneous
988
			'%n' => '', '%t' => '', '%%' => '%%',
989
		);
990
991
		$default_format = '%F';
992
	}
993
	elseif ($type === 'time')
994
	{
995
		$specifications = array(
996
			// Day
997
			'%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '',
998
			// Week
999
			'%U' => '', '%V' => '', '%W' => '',
1000
			// Month
1001
			'%b' => '', '%B' => '', '%h' => '', '%m' => '',
1002
			// Year
1003
			'%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '',
1004
			// Time
1005
			'%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P',
1006
			'%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z',
1007
			// Time and Date Stamps
1008
			'%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '',
1009
			// Miscellaneous
1010
			'%n' => '', '%t' => '', '%%' => '%%',
1011
		);
1012
1013
		$default_format = '%k:%M';
1014
	}
1015
	// Invalid type requests just get the full format string.
1016
	else
1017
		return $format;
1018
1019
	// Separate the specifications we want from the ones we don't.
1020
	$wanted = array_filter($specifications);
1021
	$unwanted = array_diff(array_keys($specifications), $wanted);
1022
1023
	// First, make any necessary substitutions in the format.
1024
	$format = strtr($format, $wanted);
1025
1026
	// Next, strip out any specifications and literal text that we don't want.
1027
	$format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format);
1028
1029
	foreach ($format_parts as $p => $f)
1030
	{
1031
		if (strpos($f, '%') === false)
1032
			unset($format_parts[$p]);
1033
	}
1034
1035
	$format = implode('', $format_parts);
1036
1037
	// Finally, strip out any unwanted leftovers.
1038
	// For info on the charcter classes used here, see https://www.php.net/manual/en/regexp.reference.unicode.php and https://www.regular-expressions.info/unicode.html
1039
	$format = preg_replace(
1040
		array(
1041
			// Anything that isn't a specification, punctuation mark, or whitespace.
1042
			'~(?<!%)\p{L}|[^\p{L}\p{P}\s]~u',
1043
			// A series of punctuation marks (except %), possibly separated by whitespace.
1044
			'~([^%\P{P}])(\s*)(?'.'>(\1|[^%\P{Po}])\s*(?!$))*~u',
1045
			// Unwanted trailing punctuation and whitespace.
1046
			'~(?'.'>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u',
1047
			// Unwanted opening punctuation and whitespace.
1048
			'~^\s*(?'.'>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u',
1049
		),
1050
		array(
1051
			'',
1052
			'$1$2',
1053
			'',
1054
			'',
1055
		),
1056
		$format
1057
	);
1058
1059
	// Gotta have something...
1060
	if (empty($format))
1061
		$format = $default_format;
1062
1063
	// Remember what we've done.
1064
	$formats[$orig_format][$type] = trim($format);
1065
1066
	return $formats[$orig_format][$type];
1067
}
1068
1069
/**
1070
 * Replaces special entities in strings with the real characters.
1071
 *
1072
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1073
 * replaces '&nbsp;' with a simple space character.
1074
 *
1075
 * @param string $string A string
1076
 * @return string The string without entities
1077
 */
1078
function un_htmlspecialchars($string)
1079
{
1080
	global $context;
1081
	static $translation = array();
1082
1083
	// Determine the character set... Default to UTF-8
1084
	if (empty($context['character_set']))
1085
		$charset = 'UTF-8';
1086
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
1087
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1088
		$charset = 'ISO-8859-1';
1089
	else
1090
		$charset = $context['character_set'];
1091
1092
	if (empty($translation))
1093
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1094
1095
	return strtr($string, $translation);
1096
}
1097
1098
/**
1099
 * Shorten a subject + internationalization concerns.
1100
 *
1101
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
1102
 * - respects internationalization characters and entities as one character.
1103
 * - avoids trailing entities.
1104
 * - returns the shortened string.
1105
 *
1106
 * @param string $subject The subject
1107
 * @param int $len How many characters to limit it to
1108
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
1109
 */
1110
function shorten_subject($subject, $len)
1111
{
1112
	global $smcFunc;
1113
1114
	// It was already short enough!
1115
	if ($smcFunc['strlen']($subject) <= $len)
1116
		return $subject;
1117
1118
	// Shorten it by the length it was too long, and strip off junk from the end.
1119
	return $smcFunc['substr']($subject, 0, $len) . '...';
1120
}
1121
1122
/**
1123
 * Gets the current time with offset.
1124
 *
1125
 * - always applies the offset in the time_offset setting.
1126
 *
1127
 * @param bool $use_user_offset Whether to apply the user's offset as well
1128
 * @param int $timestamp A timestamp (null to use current time)
1129
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
1130
 */
1131
function forum_time($use_user_offset = true, $timestamp = null)
1132
{
1133
	global $user_info, $modSettings;
1134
1135
	if ($timestamp === null)
1136
		$timestamp = time();
1137
	elseif ($timestamp == 0)
1138
		return 0;
1139
1140
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
1141
}
1142
1143
/**
1144
 * Calculates all the possible permutations (orders) of array.
1145
 * should not be called on huge arrays (bigger than like 10 elements.)
1146
 * returns an array containing each permutation.
1147
 *
1148
 * @deprecated since 2.1
1149
 * @param array $array An array
1150
 * @return array An array containing each permutation
1151
 */
1152
function permute($array)
1153
{
1154
	$orders = array($array);
1155
1156
	$n = count($array);
1157
	$p = range(0, $n);
1158
	for ($i = 1; $i < $n; null)
1159
	{
1160
		$p[$i]--;
1161
		$j = $i % 2 != 0 ? $p[$i] : 0;
1162
1163
		$temp = $array[$i];
1164
		$array[$i] = $array[$j];
1165
		$array[$j] = $temp;
1166
1167
		for ($i = 1; $p[$i] == 0; $i++)
1168
			$p[$i] = 1;
1169
1170
		$orders[] = $array;
1171
	}
1172
1173
	return $orders;
1174
}
1175
1176
/**
1177
 * Parse bulletin board code in a string, as well as smileys optionally.
1178
 *
1179
 * - only parses bbc tags which are not disabled in disabledBBC.
1180
 * - handles basic HTML, if enablePostHTML is on.
1181
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1182
 * - only parses smileys if smileys is true.
1183
 * - does nothing if the enableBBC setting is off.
1184
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1185
 * - returns the modified message.
1186
 *
1187
 * @param string|bool $message The message.
1188
 *		When a empty string, nothing is done.
1189
 *		When false we provide a list of BBC codes available.
1190
 *		When a string, the message is parsed and bbc handled.
1191
 * @param bool $smileys Whether to parse smileys as well
1192
 * @param string $cache_id The cache ID
1193
 * @param array $parse_tags If set, only parses these tags rather than all of them
1194
 * @return string The parsed message
1195
 */
1196
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1197
{
1198
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable;
1199
	static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array();
1200
	static $disabled, $alltags_regex = '', $param_regexes = array();
1201
1202
	// Don't waste cycles
1203
	if ($message === '')
1204
		return '';
1205
1206
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1207
	if (!isset($context['utf8']))
1208
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1209
1210
	// Clean up any cut/paste issues we may have
1211
	$message = sanitizeMSCutPaste($message);
1212
1213
	// If the load average is too high, don't parse the BBC.
1214
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1215
	{
1216
		$context['disabled_parse_bbc'] = true;
1217
		return $message;
1218
	}
1219
1220
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1221
		$smileys = (bool) $smileys;
1222
1223
	if (empty($modSettings['enableBBC']) && $message !== false)
1224
	{
1225
		if ($smileys === true)
1226
			parsesmileys($message);
1227
1228
		return $message;
1229
	}
1230
1231
	// If we already have a version of the BBCodes for the current language, use that. Otherwise, make one.
1232
	if (!empty($bbc_lang_locales[$txt['lang_locale']]))
1233
		$bbc_codes = $bbc_lang_locales[$txt['lang_locale']];
1234
	else
1235
		$bbc_codes = array();
1236
1237
	// If we are not doing every tag then we don't cache this run.
1238
	if (!empty($parse_tags))
1239
		$bbc_codes = array();
1240
1241
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1242
	if (!empty($modSettings['autoLinkUrls']))
1243
		set_tld_regex();
1244
1245
	// Allow mods access before entering the main parse_bbc loop
1246
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1247
1248
	// Sift out the bbc for a performance improvement.
1249
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1250
	{
1251
		if (!empty($modSettings['disabledBBC']))
1252
		{
1253
			$disabled = array();
1254
1255
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1256
1257
			foreach ($temp as $tag)
1258
				$disabled[trim($tag)] = true;
1259
1260
			if (in_array('color', $disabled))
1261
				$disabled = array_merge($disabled, array(
1262
					'black' => true,
1263
					'white' => true,
1264
					'red' => true,
1265
					'green' => true,
1266
					'blue' => true,
1267
					)
1268
				);
1269
		}
1270
1271
		// The YouTube bbc needs this for its origin parameter
1272
		$scripturl_parts = parse_url($scripturl);
1273
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1274
1275
		/* The following bbc are formatted as an array, with keys as follows:
1276
1277
			tag: the tag's name - should be lowercase!
1278
1279
			type: one of...
1280
				- (missing): [tag]parsed content[/tag]
1281
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1282
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1283
				- unparsed_content: [tag]unparsed content[/tag]
1284
				- closed: [tag], [tag/], [tag /]
1285
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1286
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1287
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1288
1289
			parameters: an optional array of parameters, for the form
1290
			  [tag abc=123]content[/tag].  The array is an associative array
1291
			  where the keys are the parameter names, and the values are an
1292
			  array which may contain the following:
1293
				- match: a regular expression to validate and match the value.
1294
				- quoted: true if the value should be quoted.
1295
				- validate: callback to evaluate on the data, which is $data.
1296
				- value: a string in which to replace $1 with the data.
1297
					Either value or validate may be used, not both.
1298
				- optional: true if the parameter is optional.
1299
				- default: a default value for missing optional parameters.
1300
1301
			test: a regular expression to test immediately after the tag's
1302
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1303
			  Optional.
1304
1305
			content: only available for unparsed_content, closed,
1306
			  unparsed_commas_content, and unparsed_equals_content.
1307
			  $1 is replaced with the content of the tag.  Parameters
1308
			  are replaced in the form {param}.  For unparsed_commas_content,
1309
			  $2, $3, ..., $n are replaced.
1310
1311
			before: only when content is not used, to go before any
1312
			  content.  For unparsed_equals, $1 is replaced with the value.
1313
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1314
1315
			after: similar to before in every way, except that it is used
1316
			  when the tag is closed.
1317
1318
			disabled_content: used in place of content when the tag is
1319
			  disabled.  For closed, default is '', otherwise it is '$1' if
1320
			  block_level is false, '<div>$1</div>' elsewise.
1321
1322
			disabled_before: used in place of before when disabled.  Defaults
1323
			  to '<div>' if block_level, '' if not.
1324
1325
			disabled_after: used in place of after when disabled.  Defaults
1326
			  to '</div>' if block_level, '' if not.
1327
1328
			block_level: set to true the tag is a "block level" tag, similar
1329
			  to HTML.  Block level tags cannot be nested inside tags that are
1330
			  not block level, and will not be implicitly closed as easily.
1331
			  One break following a block level tag may also be removed.
1332
1333
			trim: if set, and 'inside' whitespace after the begin tag will be
1334
			  removed.  If set to 'outside', whitespace after the end tag will
1335
			  meet the same fate.
1336
1337
			validate: except when type is missing or 'closed', a callback to
1338
			  validate the data as $data.  Depending on the tag's type, $data
1339
			  may be a string or an array of strings (corresponding to the
1340
			  replacement.)
1341
1342
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1343
			  may be not set, 'optional', or 'required' corresponding to if
1344
			  the content may be quoted.  This allows the parser to read
1345
			  [tag="abc]def[esdf]"] properly.
1346
1347
			require_parents: an array of tag names, or not set.  If set, the
1348
			  enclosing tag *must* be one of the listed tags, or parsing won't
1349
			  occur.
1350
1351
			require_children: similar to require_parents, if set children
1352
			  won't be parsed if they are not in the list.
1353
1354
			disallow_children: similar to, but very different from,
1355
			  require_children, if it is set the listed tags will not be
1356
			  parsed inside the tag.
1357
1358
			parsed_tags_allowed: an array restricting what BBC can be in the
1359
			  parsed_equals parameter, if desired.
1360
		*/
1361
1362
		$codes = array(
1363
			array(
1364
				'tag' => 'abbr',
1365
				'type' => 'unparsed_equals',
1366
				'before' => '<abbr title="$1">',
1367
				'after' => '</abbr>',
1368
				'quoted' => 'optional',
1369
				'disabled_after' => ' ($1)',
1370
			),
1371
			// Legacy (and just an alias for [abbr] even when enabled)
1372
			array(
1373
				'tag' => 'acronym',
1374
				'type' => 'unparsed_equals',
1375
				'before' => '<abbr title="$1">',
1376
				'after' => '</abbr>',
1377
				'quoted' => 'optional',
1378
				'disabled_after' => ' ($1)',
1379
			),
1380
			array(
1381
				'tag' => 'anchor',
1382
				'type' => 'unparsed_equals',
1383
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1384
				'before' => '<span id="post_$1">',
1385
				'after' => '</span>',
1386
			),
1387
			array(
1388
				'tag' => 'attach',
1389
				'type' => 'unparsed_content',
1390
				'parameters' => array(
1391
					'id' => array('match' => '(\d+)'),
1392
					'alt' => array('optional' => true),
1393
					'width' => array('optional' => true, 'match' => '(\d+)'),
1394
					'height' => array('optional' => true, 'match' => '(\d+)'),
1395
					'display' => array('optional' => true, 'match' => '(link|embed)'),
1396
				),
1397
				'content' => '$1',
1398
				'validate' => function(&$tag, &$data, $disabled, $params) use ($modSettings, $context, $sourcedir, $txt, $smcFunc)
1399
				{
1400
					$returnContext = '';
1401
1402
					// BBC or the entire attachments feature is disabled
1403
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1404
						return $data;
1405
1406
					// Save the attach ID.
1407
					$attachID = $params['{id}'];
1408
1409
					// Kinda need this.
1410
					require_once($sourcedir . '/Subs-Attachments.php');
1411
1412
					$currentAttachment = parseAttachBBC($attachID);
1413
1414
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1415
					if (is_string($currentAttachment))
1416
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1417
1418
					// We need a display mode.
1419
					if (empty($params['{display}']))
1420
					{
1421
						// Images, video, and audio are embedded by default.
1422
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1423
							$params['{display}'] = 'embed';
1424
						// Anything else shows a link by default.
1425
						else
1426
							$params['{display}'] = 'link';
1427
					}
1428
1429
					// Embedded file.
1430
					if ($params['{display}'] == 'embed')
1431
					{
1432
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1433
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1434
1435
						if (empty($params['{width}']) && empty($params['{height}']))
1436
						{
1437
							$width = !empty($currentAttachment['width']) ? $currentAttachment['width'] : '';
1438
							$height = !empty($currentAttachment['height']) ? $currentAttachment['height'] : '';
1439
						}
1440
						else
1441
						{
1442
							$width = !empty($params['{width}']) ? $params['{width}'] : '';
1443
							$height = !empty($params['{height}']) ? $params['{height}'] : '';
1444
						}
1445
1446
						// Image.
1447
						if (!empty($currentAttachment['is_image']))
1448
						{
1449
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1450
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1451
1452
							if ($currentAttachment['thumbnail']['has_thumb'] && empty($params['{width}']) && empty($params['{height}']))
1453
								$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>';
1454
							else
1455
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img"/>';
1456
						}
1457
						// Video.
1458
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1459
						{
1460
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1461
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1462
1463
							$returnContext .= '<div class="videocontainer"><video controls preload="metadata" src="'. $currentAttachment['href'] . '" playsinline' . $width . $height . '><a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a></video></div>' . (!empty($data) && $data != $currentAttachment['name'] ? '<div class="smalltext">' . $data . '</div>' : '');
1464
						}
1465
						// Audio.
1466
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1467
						{
1468
							$width = 'max-width:100%; width: ' . (!empty($width) ? $width : '400') . 'px;';
1469
							$height = !empty($height) ? 'height: ' . $height . 'px;' : '';
1470
1471
							$returnContext .= (!empty($data) && $data != $currentAttachment['name'] ? $data . ' ' : '') . '<audio controls preload="none" src="'. $currentAttachment['href'] . '" class="bbc_audio" style="vertical-align:middle;' . $width . $height . '"><a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a></audio>';
1472
						}
1473
						// Anything else.
1474
						else
1475
						{
1476
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1477
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1478
1479
							$returnContext .= '<object type="' . $currentAttachment['mime_type'] . '" data="' . $currentAttachment['href'] . '"' . $width . $height . ' typemustmatch><a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a></object>';
1480
						}
1481
					}
1482
1483
					// No image. Show a link.
1484
					else
1485
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1486
1487
					// Gotta append what we just did.
1488
					$data = $returnContext;
1489
				},
1490
			),
1491
			array(
1492
				'tag' => 'b',
1493
				'before' => '<b>',
1494
				'after' => '</b>',
1495
			),
1496
			// Legacy (equivalent to [ltr] or [rtl])
1497
			array(
1498
				'tag' => 'bdo',
1499
				'type' => 'unparsed_equals',
1500
				'before' => '<bdo dir="$1">',
1501
				'after' => '</bdo>',
1502
				'test' => '(rtl|ltr)\]',
1503
				'block_level' => true,
1504
			),
1505
			// Legacy (alias of [color=black])
1506
			array(
1507
				'tag' => 'black',
1508
				'before' => '<span style="color: black;" class="bbc_color">',
1509
				'after' => '</span>',
1510
			),
1511
			// Legacy (alias of [color=blue])
1512
			array(
1513
				'tag' => 'blue',
1514
				'before' => '<span style="color: blue;" class="bbc_color">',
1515
				'after' => '</span>',
1516
			),
1517
			array(
1518
				'tag' => 'br',
1519
				'type' => 'closed',
1520
				'content' => '<br>',
1521
			),
1522
			array(
1523
				'tag' => 'center',
1524
				'before' => '<div class="centertext">',
1525
				'after' => '</div>',
1526
				'block_level' => true,
1527
			),
1528
			array(
1529
				'tag' => 'code',
1530
				'type' => 'unparsed_content',
1531
				'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>',
1532
				// @todo Maybe this can be simplified?
1533
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1534
				{
1535
					if (!isset($disabled['code']))
1536
					{
1537
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1538
1539
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1540
						{
1541
							// Do PHP code coloring?
1542
							if ($php_parts[$php_i] != '&lt;?php')
1543
								continue;
1544
1545
							$php_string = '';
1546
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1547
							{
1548
								$php_string .= $php_parts[$php_i];
1549
								$php_parts[$php_i++] = '';
1550
							}
1551
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1552
						}
1553
1554
						// Fix the PHP code stuff...
1555
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
0 ignored issues
show
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

1555
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1556
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1557
1558
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1559
						if (!empty($context['browser']['is_opera']))
1560
							$data .= '&nbsp;';
1561
					}
1562
				},
1563
				'block_level' => true,
1564
			),
1565
			array(
1566
				'tag' => 'code',
1567
				'type' => 'unparsed_equals_content',
1568
				'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>',
1569
				// @todo Maybe this can be simplified?
1570
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1571
				{
1572
					if (!isset($disabled['code']))
1573
					{
1574
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1575
1576
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1577
						{
1578
							// Do PHP code coloring?
1579
							if ($php_parts[$php_i] != '&lt;?php')
1580
								continue;
1581
1582
							$php_string = '';
1583
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1584
							{
1585
								$php_string .= $php_parts[$php_i];
1586
								$php_parts[$php_i++] = '';
1587
							}
1588
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1589
						}
1590
1591
						// Fix the PHP code stuff...
1592
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
0 ignored issues
show
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

1592
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', /** @scrutinizer ignore-type */ $php_parts));
Loading history...
1593
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1594
1595
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1596
						if (!empty($context['browser']['is_opera']))
1597
							$data[0] .= '&nbsp;';
1598
					}
1599
				},
1600
				'block_level' => true,
1601
			),
1602
			array(
1603
				'tag' => 'color',
1604
				'type' => 'unparsed_equals',
1605
				'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]?)\))\]',
1606
				'before' => '<span style="color: $1;" class="bbc_color">',
1607
				'after' => '</span>',
1608
			),
1609
			array(
1610
				'tag' => 'email',
1611
				'type' => 'unparsed_content',
1612
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1613
				// @todo Should this respect guest_hideContacts?
1614
				'validate' => function(&$tag, &$data, $disabled)
1615
				{
1616
					$data = strtr($data, array('<br>' => ''));
1617
				},
1618
			),
1619
			array(
1620
				'tag' => 'email',
1621
				'type' => 'unparsed_equals',
1622
				'before' => '<a href="mailto:$1" class="bbc_email">',
1623
				'after' => '</a>',
1624
				// @todo Should this respect guest_hideContacts?
1625
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1626
				'disabled_after' => ' ($1)',
1627
			),
1628
			// Legacy (and just a link even when not disabled)
1629
			array(
1630
				'tag' => 'flash',
1631
				'type' => 'unparsed_commas_content',
1632
				'test' => '\d+,\d+\]',
1633
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1634
				'validate' => function (&$tag, &$data, $disabled)
1635
				{
1636
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1637
					if (empty($scheme))
1638
						$data[0] = '//' . ltrim($data[0], ':/');
1639
				},
1640
			),
1641
			array(
1642
				'tag' => 'float',
1643
				'type' => 'unparsed_equals',
1644
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1645
				'before' => '<div $1>',
1646
				'after' => '</div>',
1647
				'validate' => function(&$tag, &$data, $disabled)
1648
				{
1649
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1650
1651
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1652
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1653
					else
1654
						$css = '';
1655
1656
					$data = $class . $css;
1657
				},
1658
				'trim' => 'outside',
1659
				'block_level' => true,
1660
			),
1661
			// Legacy (alias of [url] with an FTP URL)
1662
			array(
1663
				'tag' => 'ftp',
1664
				'type' => 'unparsed_content',
1665
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1666
				'validate' => function(&$tag, &$data, $disabled)
1667
				{
1668
					$data = strtr($data, array('<br>' => ''));
1669
					$scheme = parse_url($data, PHP_URL_SCHEME);
1670
					if (empty($scheme))
1671
						$data = 'ftp://' . ltrim($data, ':/');
1672
				},
1673
			),
1674
			// Legacy (alias of [url] with an FTP URL)
1675
			array(
1676
				'tag' => 'ftp',
1677
				'type' => 'unparsed_equals',
1678
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1679
				'after' => '</a>',
1680
				'validate' => function(&$tag, &$data, $disabled)
1681
				{
1682
					$scheme = parse_url($data, PHP_URL_SCHEME);
1683
					if (empty($scheme))
1684
						$data = 'ftp://' . ltrim($data, ':/');
1685
				},
1686
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1687
				'disabled_after' => ' ($1)',
1688
			),
1689
			array(
1690
				'tag' => 'font',
1691
				'type' => 'unparsed_equals',
1692
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1693
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1694
				'after' => '</span>',
1695
			),
1696
			// Legacy (one of those things that should not be done)
1697
			array(
1698
				'tag' => 'glow',
1699
				'type' => 'unparsed_commas',
1700
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1701
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1702
				'after' => '</span>',
1703
			),
1704
			// Legacy (alias of [color=green])
1705
			array(
1706
				'tag' => 'green',
1707
				'before' => '<span style="color: green;" class="bbc_color">',
1708
				'after' => '</span>',
1709
			),
1710
			array(
1711
				'tag' => 'html',
1712
				'type' => 'unparsed_content',
1713
				'content' => '<div>$1</div>',
1714
				'block_level' => true,
1715
				'disabled_content' => '$1',
1716
			),
1717
			array(
1718
				'tag' => 'hr',
1719
				'type' => 'closed',
1720
				'content' => '<hr>',
1721
				'block_level' => true,
1722
			),
1723
			array(
1724
				'tag' => 'i',
1725
				'before' => '<i>',
1726
				'after' => '</i>',
1727
			),
1728
			array(
1729
				'tag' => 'img',
1730
				'type' => 'unparsed_content',
1731
				'parameters' => array(
1732
					'alt' => array('optional' => true),
1733
					'title' => array('optional' => true),
1734
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1735
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1736
				),
1737
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized">',
1738
				'validate' => function(&$tag, &$data, $disabled)
1739
				{
1740
					$data = strtr($data, array('<br>' => ''));
1741
1742
					if (parse_url($data, PHP_URL_SCHEME) === null)
1743
						$data = '//' . ltrim($data, ':/');
1744
					else
1745
						$data = get_proxied_url($data);
1746
				},
1747
				'disabled_content' => '($1)',
1748
			),
1749
			array(
1750
				'tag' => 'img',
1751
				'type' => 'unparsed_content',
1752
				'content' => '<img src="$1" alt="" class="bbc_img">',
1753
				'validate' => function(&$tag, &$data, $disabled)
1754
				{
1755
					$data = strtr($data, array('<br>' => ''));
1756
1757
					if (parse_url($data, PHP_URL_SCHEME) === null)
1758
						$data = '//' . ltrim($data, ':/');
1759
					else
1760
						$data = get_proxied_url($data);
1761
				},
1762
				'disabled_content' => '($1)',
1763
			),
1764
			array(
1765
				'tag' => 'iurl',
1766
				'type' => 'unparsed_content',
1767
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1768
				'validate' => function(&$tag, &$data, $disabled)
1769
				{
1770
					$data = strtr($data, array('<br>' => ''));
1771
					$scheme = parse_url($data, PHP_URL_SCHEME);
1772
					if (empty($scheme))
1773
						$data = '//' . ltrim($data, ':/');
1774
				},
1775
			),
1776
			array(
1777
				'tag' => 'iurl',
1778
				'type' => 'unparsed_equals',
1779
				'quoted' => 'optional',
1780
				'before' => '<a href="$1" class="bbc_link">',
1781
				'after' => '</a>',
1782
				'validate' => function(&$tag, &$data, $disabled)
1783
				{
1784
					if (substr($data, 0, 1) == '#')
1785
						$data = '#post_' . substr($data, 1);
1786
					else
1787
					{
1788
						$scheme = parse_url($data, PHP_URL_SCHEME);
1789
						if (empty($scheme))
1790
							$data = '//' . ltrim($data, ':/');
1791
					}
1792
				},
1793
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1794
				'disabled_after' => ' ($1)',
1795
			),
1796
			array(
1797
				'tag' => 'justify',
1798
				'before' => '<div class="justifytext">',
1799
				'after' => '</div>',
1800
				'block_level' => true,
1801
			),
1802
			array(
1803
				'tag' => 'left',
1804
				'before' => '<div class="lefttext">',
1805
				'after' => '</div>',
1806
				'block_level' => true,
1807
			),
1808
			array(
1809
				'tag' => 'li',
1810
				'before' => '<li>',
1811
				'after' => '</li>',
1812
				'trim' => 'outside',
1813
				'require_parents' => array('list'),
1814
				'block_level' => true,
1815
				'disabled_before' => '',
1816
				'disabled_after' => '<br>',
1817
			),
1818
			array(
1819
				'tag' => 'list',
1820
				'before' => '<ul class="bbc_list">',
1821
				'after' => '</ul>',
1822
				'trim' => 'inside',
1823
				'require_children' => array('li', 'list'),
1824
				'block_level' => true,
1825
			),
1826
			array(
1827
				'tag' => 'list',
1828
				'parameters' => array(
1829
					'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)'),
1830
				),
1831
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
1832
				'after' => '</ul>',
1833
				'trim' => 'inside',
1834
				'require_children' => array('li'),
1835
				'block_level' => true,
1836
			),
1837
			array(
1838
				'tag' => 'ltr',
1839
				'before' => '<bdo dir="ltr">',
1840
				'after' => '</bdo>',
1841
				'block_level' => true,
1842
			),
1843
			array(
1844
				'tag' => 'me',
1845
				'type' => 'unparsed_equals',
1846
				'before' => '<div class="meaction">* $1 ',
1847
				'after' => '</div>',
1848
				'quoted' => 'optional',
1849
				'block_level' => true,
1850
				'disabled_before' => '/me ',
1851
				'disabled_after' => '<br>',
1852
			),
1853
			array(
1854
				'tag' => 'member',
1855
				'type' => 'unparsed_equals',
1856
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
1857
				'after' => '</a>',
1858
			),
1859
			// Legacy (horrible memories of the 1990s)
1860
			array(
1861
				'tag' => 'move',
1862
				'before' => '<marquee>',
1863
				'after' => '</marquee>',
1864
				'block_level' => true,
1865
				'disallow_children' => array('move'),
1866
			),
1867
			array(
1868
				'tag' => 'nobbc',
1869
				'type' => 'unparsed_content',
1870
				'content' => '$1',
1871
			),
1872
			array(
1873
				'tag' => 'php',
1874
				'type' => 'unparsed_content',
1875
				'content' => '<span class="phpcode">$1</span>',
1876
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
1877
				{
1878
					if (!isset($disabled['php']))
1879
					{
1880
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
1881
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
1882
						if ($add_begin)
1883
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
1884
					}
1885
				},
1886
				'block_level' => false,
1887
				'disabled_content' => '$1',
1888
			),
1889
			array(
1890
				'tag' => 'pre',
1891
				'before' => '<pre>',
1892
				'after' => '</pre>',
1893
			),
1894
			array(
1895
				'tag' => 'quote',
1896
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
1897
				'after' => '</blockquote>',
1898
				'trim' => 'both',
1899
				'block_level' => true,
1900
			),
1901
			array(
1902
				'tag' => 'quote',
1903
				'parameters' => array(
1904
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
1905
				),
1906
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1907
				'after' => '</blockquote>',
1908
				'trim' => 'both',
1909
				'block_level' => true,
1910
			),
1911
			array(
1912
				'tag' => 'quote',
1913
				'type' => 'parsed_equals',
1914
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
1915
				'after' => '</blockquote>',
1916
				'trim' => 'both',
1917
				'quoted' => 'optional',
1918
				// Don't allow everything to be embedded with the author name.
1919
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
1920
				'block_level' => true,
1921
			),
1922
			array(
1923
				'tag' => 'quote',
1924
				'parameters' => array(
1925
					'author' => array('match' => '([^<>]{1,192}?)'),
1926
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
1927
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
1928
				),
1929
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
1930
				'after' => '</blockquote>',
1931
				'trim' => 'both',
1932
				'block_level' => true,
1933
			),
1934
			array(
1935
				'tag' => 'quote',
1936
				'parameters' => array(
1937
					'author' => array('match' => '(.{1,192}?)'),
1938
				),
1939
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
1940
				'after' => '</blockquote>',
1941
				'trim' => 'both',
1942
				'block_level' => true,
1943
			),
1944
			// Legacy (alias of [color=red])
1945
			array(
1946
				'tag' => 'red',
1947
				'before' => '<span style="color: red;" class="bbc_color">',
1948
				'after' => '</span>',
1949
			),
1950
			array(
1951
				'tag' => 'right',
1952
				'before' => '<div class="righttext">',
1953
				'after' => '</div>',
1954
				'block_level' => true,
1955
			),
1956
			array(
1957
				'tag' => 'rtl',
1958
				'before' => '<bdo dir="rtl">',
1959
				'after' => '</bdo>',
1960
				'block_level' => true,
1961
			),
1962
			array(
1963
				'tag' => 's',
1964
				'before' => '<s>',
1965
				'after' => '</s>',
1966
			),
1967
			// Legacy (never a good idea)
1968
			array(
1969
				'tag' => 'shadow',
1970
				'type' => 'unparsed_commas',
1971
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
1972
				'before' => '<span style="text-shadow: $1 $2">',
1973
				'after' => '</span>',
1974
				'validate' => function(&$tag, &$data, $disabled)
1975
				{
1976
1977
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
1978
						$data[1] = '0 -2px 1px';
1979
1980
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
1981
						$data[1] = '2px 0 1px';
1982
1983
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
1984
						$data[1] = '0 2px 1px';
1985
1986
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
1987
						$data[1] = '-2px 0 1px';
1988
1989
					else
1990
						$data[1] = '1px 1px 1px';
1991
				},
1992
			),
1993
			array(
1994
				'tag' => 'size',
1995
				'type' => 'unparsed_equals',
1996
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
1997
				'before' => '<span style="font-size: $1;" class="bbc_size">',
1998
				'after' => '</span>',
1999
			),
2000
			array(
2001
				'tag' => 'size',
2002
				'type' => 'unparsed_equals',
2003
				'test' => '[1-7]\]',
2004
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2005
				'after' => '</span>',
2006
				'validate' => function(&$tag, &$data, $disabled)
2007
				{
2008
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
2009
					$data = $sizes[$data] . 'em';
2010
				},
2011
			),
2012
			array(
2013
				'tag' => 'sub',
2014
				'before' => '<sub>',
2015
				'after' => '</sub>',
2016
			),
2017
			array(
2018
				'tag' => 'sup',
2019
				'before' => '<sup>',
2020
				'after' => '</sup>',
2021
			),
2022
			array(
2023
				'tag' => 'table',
2024
				'before' => '<table class="bbc_table">',
2025
				'after' => '</table>',
2026
				'trim' => 'inside',
2027
				'require_children' => array('tr'),
2028
				'block_level' => true,
2029
			),
2030
			array(
2031
				'tag' => 'td',
2032
				'before' => '<td>',
2033
				'after' => '</td>',
2034
				'require_parents' => array('tr'),
2035
				'trim' => 'outside',
2036
				'block_level' => true,
2037
				'disabled_before' => '',
2038
				'disabled_after' => '',
2039
			),
2040
			array(
2041
				'tag' => 'time',
2042
				'type' => 'unparsed_content',
2043
				'content' => '$1',
2044
				'validate' => function(&$tag, &$data, $disabled)
2045
				{
2046
					if (is_numeric($data))
2047
						$data = timeformat($data);
2048
					else
2049
						$tag['content'] = '[time]$1[/time]';
2050
				},
2051
			),
2052
			array(
2053
				'tag' => 'tr',
2054
				'before' => '<tr>',
2055
				'after' => '</tr>',
2056
				'require_parents' => array('table'),
2057
				'require_children' => array('td'),
2058
				'trim' => 'both',
2059
				'block_level' => true,
2060
				'disabled_before' => '',
2061
				'disabled_after' => '',
2062
			),
2063
			// Legacy (the <tt> element is dead)
2064
			array(
2065
				'tag' => 'tt',
2066
				'before' => '<span class="monospace">',
2067
				'after' => '</span>',
2068
			),
2069
			array(
2070
				'tag' => 'u',
2071
				'before' => '<u>',
2072
				'after' => '</u>',
2073
			),
2074
			array(
2075
				'tag' => 'url',
2076
				'type' => 'unparsed_content',
2077
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2078
				'validate' => function(&$tag, &$data, $disabled)
2079
				{
2080
					$data = strtr($data, array('<br>' => ''));
2081
					$scheme = parse_url($data, PHP_URL_SCHEME);
2082
					if (empty($scheme))
2083
						$data = '//' . ltrim($data, ':/');
2084
				},
2085
			),
2086
			array(
2087
				'tag' => 'url',
2088
				'type' => 'unparsed_equals',
2089
				'quoted' => 'optional',
2090
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2091
				'after' => '</a>',
2092
				'validate' => function(&$tag, &$data, $disabled)
2093
				{
2094
					$scheme = parse_url($data, PHP_URL_SCHEME);
2095
					if (empty($scheme))
2096
						$data = '//' . ltrim($data, ':/');
2097
				},
2098
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2099
				'disabled_after' => ' ($1)',
2100
			),
2101
			// Legacy (alias of [color=white])
2102
			array(
2103
				'tag' => 'white',
2104
				'before' => '<span style="color: white;" class="bbc_color">',
2105
				'after' => '</span>',
2106
			),
2107
			array(
2108
				'tag' => 'youtube',
2109
				'type' => 'unparsed_content',
2110
				'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>',
2111
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2112
				'block_level' => true,
2113
			),
2114
		);
2115
2116
		// Inside these tags autolink is not recommendable.
2117
		$no_autolink_tags = array(
2118
			'url',
2119
			'iurl',
2120
			'email',
2121
			'img',
2122
			'html',
2123
		);
2124
2125
		// Let mods add new BBC without hassle.
2126
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2127
2128
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2129
		if ($message === false)
2130
		{
2131
			usort($codes, function($a, $b)
2132
			{
2133
				return strcmp($a['tag'], $b['tag']);
2134
			});
2135
			return $codes;
2136
		}
2137
2138
		// So the parser won't skip them.
2139
		$itemcodes = array(
2140
			'*' => 'disc',
2141
			'@' => 'disc',
2142
			'+' => 'square',
2143
			'x' => 'square',
2144
			'#' => 'square',
2145
			'o' => 'circle',
2146
			'O' => 'circle',
2147
			'0' => 'circle',
2148
		);
2149
		if (!isset($disabled['li']) && !isset($disabled['list']))
2150
		{
2151
			foreach ($itemcodes as $c => $dummy)
2152
				$bbc_codes[$c] = array();
2153
		}
2154
2155
		// Shhhh!
2156
		if (!isset($disabled['color']))
2157
		{
2158
			$codes[] = array(
2159
				'tag' => 'chrissy',
2160
				'before' => '<span style="color: #cc0099;">',
2161
				'after' => ' :-*</span>',
2162
			);
2163
			$codes[] = array(
2164
				'tag' => 'kissy',
2165
				'before' => '<span style="color: #cc0099;">',
2166
				'after' => ' :-*</span>',
2167
			);
2168
		}
2169
		$codes[] = array(
2170
			'tag' => 'cowsay',
2171
			'parameters' => array(
2172
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2173
					{
2174
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
2175
					},
2176
				),
2177
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
2178
					{
2179
						return $smcFunc['substr']($tongue . '  ', 0, 2);
2180
					},
2181
				),
2182
			),
2183
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
2184
			'after' => '</div><script>' . '$("head").append("<style>" + ' . JavaScriptEscape(base64_decode('cHJlW2RhdGEtZV1bZGF0YS10XXt3aGl0ZS1zcGFjZTpwcmUtd3JhcDtsaW5lLWhlaWdodDppbml0aWFsO31wcmVbZGF0YS1lXVtkYXRhLXRdID4gZGl2e2Rpc3BsYXk6dGFibGU7Ym9yZGVyOjFweCBzb2xpZDtib3JkZXItcmFkaXVzOjAuNWVtO3BhZGRpbmc6MWNoO21heC13aWR0aDo4MGNoO21pbi13aWR0aDoxMmNoO31wcmVbZGF0YS1lXVtkYXRhLXRdOjphZnRlcntkaXNwbGF5OmlubGluZS1ibG9jazttYXJnaW4tbGVmdDo4Y2g7bWluLXdpZHRoOjIwY2g7ZGlyZWN0aW9uOmx0cjtjb250ZW50OidcNUMgJycgJycgXl9fXlxBICcnIFw1QyAnJyAoJyBhdHRyKGRhdGEtZSkgJylcNUNfX19fX19fXEEgJycgJycgJycgKF9fKVw1QyAnJyAnJyAnJyAnJyAnJyAnJyAnJyApXDVDL1w1Q1xBICcnICcnICcnICcnICcgYXR0cihkYXRhLXQpICcgfHwtLS0tdyB8XEEgJycgJycgJycgJycgJycgJycgJycgfHwgJycgJycgJycgJycgfHwnO30=')) . ' + "</style>");' . '</script></pre>',
2185
			'block_level' => true,
2186
		);
2187
2188
		foreach ($codes as $code)
2189
		{
2190
			// Make it easier to process parameters later
2191
			if (!empty($code['parameters']))
2192
				ksort($code['parameters'], SORT_STRING);
2193
2194
			// If we are not doing every tag only do ones we are interested in.
2195
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2196
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2197
		}
2198
		$codes = null;
2199
	}
2200
2201
	// Shall we take the time to cache this?
2202
	if ($cache_id != '' && !empty($cache_enable) && (($cache_enable >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2203
	{
2204
		// It's likely this will change if the message is modified.
2205
		$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']);
2206
2207
		if (($temp = cache_get_data($cache_key, 240)) != null)
2208
			return $temp;
2209
2210
		$cache_t = microtime(true);
2211
	}
2212
2213
	if ($smileys === 'print')
2214
	{
2215
		// [glow], [shadow], and [move] can't really be printed.
2216
		$disabled['glow'] = true;
2217
		$disabled['shadow'] = true;
2218
		$disabled['move'] = true;
2219
2220
		// Colors can't well be displayed... supposed to be black and white.
2221
		$disabled['color'] = true;
2222
		$disabled['black'] = true;
2223
		$disabled['blue'] = true;
2224
		$disabled['white'] = true;
2225
		$disabled['red'] = true;
2226
		$disabled['green'] = true;
2227
		$disabled['me'] = true;
2228
2229
		// Color coding doesn't make sense.
2230
		$disabled['php'] = true;
2231
2232
		// Links are useless on paper... just show the link.
2233
		$disabled['ftp'] = true;
2234
		$disabled['url'] = true;
2235
		$disabled['iurl'] = true;
2236
		$disabled['email'] = true;
2237
		$disabled['flash'] = true;
2238
2239
		// @todo Change maybe?
2240
		if (!isset($_GET['images']))
2241
			$disabled['img'] = true;
2242
2243
		// Maybe some custom BBC need to be disabled for printing.
2244
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2245
	}
2246
2247
	$open_tags = array();
2248
	$message = strtr($message, array("\n" => '<br>'));
2249
2250
	if (!empty($parse_tags))
2251
	{
2252
		$real_alltags_regex = $alltags_regex;
2253
		$alltags_regex = '';
2254
	}
2255
	if (empty($alltags_regex))
2256
	{
2257
		$alltags = array();
2258
		foreach ($bbc_codes as $section)
2259
		{
2260
			foreach ($section as $code)
2261
				$alltags[] = $code['tag'];
2262
		}
2263
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
2264
	}
2265
2266
	$pos = -1;
2267
	while ($pos !== false)
2268
	{
2269
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2270
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2271
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2272
2273
		// Failsafe.
2274
		if ($pos === false || $last_pos > $pos)
2275
			$pos = strlen($message) + 1;
2276
2277
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2278
		if ($last_pos < $pos - 1)
2279
		{
2280
			// Make sure the $last_pos is not negative.
2281
			$last_pos = max($last_pos, 0);
2282
2283
			// Pick a block of data to do some raw fixing on.
2284
			$data = substr($message, $last_pos, $pos - $last_pos);
2285
2286
			$placeholders = array();
2287
			$placeholders_counter = 0;
2288
2289
			// Take care of some HTML!
2290
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2291
			{
2292
				$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);
2293
2294
				// <br> should be empty.
2295
				$empty_tags = array('br', 'hr');
2296
				foreach ($empty_tags as $tag)
2297
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2298
2299
				// b, u, i, s, pre... basic tags.
2300
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2301
				foreach ($closable_tags as $tag)
2302
				{
2303
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2304
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2305
2306
					if ($diff > 0)
2307
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2308
				}
2309
2310
				// Do <img ...> - with security... action= -> action-.
2311
				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);
2312
				if (!empty($matches[0]))
2313
				{
2314
					$replaces = array();
2315
					foreach ($matches[2] as $match => $imgtag)
2316
					{
2317
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2318
2319
						// Remove action= from the URL - no funny business, now.
2320
						// @todo Testing this preg_match seems pointless
2321
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2322
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2323
2324
						$placeholder = '<placeholder ' . ++$placeholders_counter . '>';
2325
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2326
2327
						$replaces[$matches[0][$match]] = $placeholder;
2328
					}
2329
2330
					$data = strtr($data, $replaces);
2331
				}
2332
			}
2333
2334
			if (!empty($modSettings['autoLinkUrls']))
2335
			{
2336
				// Are we inside tags that should be auto linked?
2337
				$no_autolink_area = false;
2338
				if (!empty($open_tags))
2339
				{
2340
					foreach ($open_tags as $open_tag)
2341
						if (in_array($open_tag['tag'], $no_autolink_tags))
2342
							$no_autolink_area = true;
2343
				}
2344
2345
				// Don't go backwards.
2346
				// @todo Don't think is the real solution....
2347
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2348
				if ($pos < $lastAutoPos)
2349
					$no_autolink_area = true;
2350
				$lastAutoPos = $pos;
2351
2352
				if (!$no_autolink_area)
2353
				{
2354
					// An &nbsp; right after a URL can break the autolinker
2355
					if (strpos($data, '&nbsp;') !== false)
2356
					{
2357
						$placeholders['<placeholder non-breaking-space>'] = '&nbsp;';
2358
						$data = strtr($data, array('&nbsp;' => '<placeholder non-breaking-space>'));
2359
					}
2360
2361
					// Parse any URLs
2362
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2363
					{
2364
						// For efficiency, first define the TLD regex in a PCRE subroutine
2365
						$url_regex = '(?(DEFINE)(?<tlds>' . $modSettings['tld_regex'] . '))';
2366
2367
						// Now build the rest of the regex
2368
						$url_regex .=
2369
						// 1. IRI scheme and domain components
2370
						'(?:' .
2371
							// 1a. IRIs with a scheme, or at least an opening "//"
2372
							'(?:' .
2373
2374
								// URI scheme (or lack thereof for schemeless URLs)
2375
								'(?:' .
2376
									// URL scheme and colon
2377
									'\b[a-z][\w\-]+:' .
2378
									// or
2379
									'|' .
2380
									// A boundary followed by two slashes for schemeless URLs
2381
									'(?<=^|\W)(?=//)' .
2382
								')' .
2383
2384
								// IRI "authority" chunk
2385
								'(?:' .
2386
									// 2 slashes for IRIs with an "authority"
2387
									'//' .
2388
									// then a domain name
2389
									'(?:' .
2390
										// Either the reserved "localhost" domain name
2391
										'localhost' .
2392
										// or
2393
										'|' .
2394
										// a run of IRI characters, a dot, and a TLD
2395
										'[\p{L}\p{M}\p{N}\-.:@]+\.(?P>tlds)' .
2396
									')' .
2397
									// followed by a non-domain character or end of line
2398
									'(?=[^\p{L}\p{N}\-.]|$)' .
2399
2400
									// or, if no "authority" per se (e.g. "mailto:" URLs)...
2401
									'|' .
2402
2403
									// a run of IRI characters
2404
									'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]' .
2405
									// and then a dot and a closing IRI label
2406
									'\.[\p{L}\p{M}\p{N}\-]+' .
2407
								')' .
2408
							')' .
2409
2410
							// Or
2411
							'|' .
2412
2413
							// 1b. Naked domains (e.g. "example.com" in "Go to example.com for an example.")
2414
							'(?:' .
2415
								// Preceded by start of line or a non-domain character
2416
								'(?<=^|[^\p{L}\p{M}\p{N}\-:@])' .
2417
								// A run of Unicode domain name characters (excluding [:@])
2418
								'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]' .
2419
								// and then a dot and a valid TLD
2420
								'\.(?P>tlds)' .
2421
								// Followed by either:
2422
								'(?=' .
2423
									// end of line or a non-domain character (excluding [.:@])
2424
									'$|[^\p{L}\p{N}\-]' .
2425
									// or
2426
									'|' .
2427
									// a dot followed by end of line or a non-domain character (excluding [.:@])
2428
									'\.(?=$|[^\p{L}\p{N}\-])' .
2429
								')' .
2430
							')' .
2431
						')' .
2432
2433
						// 2. IRI path, query, and fragment components (if present)
2434
						'(?:' .
2435
2436
							// If any of these parts exist, must start with a single "/"
2437
							'/' .
2438
2439
							// And then optionally:
2440
							'(?:' .
2441
								// One or more of:
2442
								'(?:' .
2443
									// a run of non-space, non-()<>
2444
									'[^\s()<>]+' .
2445
									// or
2446
									'|' .
2447
									// balanced parentheses, up to 2 levels
2448
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2449
								')+' .
2450
								// Ending with:
2451
								'(?:' .
2452
									// balanced parentheses, up to 2 levels
2453
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2454
									// or
2455
									'|' .
2456
									// not a space or one of these punctuation characters
2457
									'[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]' .
2458
									// or
2459
									'|' .
2460
									// a trailing slash (but not two in a row)
2461
									'(?<!/)/' .
2462
								')' .
2463
							')?' .
2464
						')?';
2465
2466
						$data = preg_replace_callback('~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''), function($matches)
2467
						{
2468
							$url = array_shift($matches);
2469
2470
							// If this isn't a clean URL, bail out
2471
							if ($url != sanitize_iri($url))
2472
								return $url;
2473
2474
							$scheme = parse_url($url, PHP_URL_SCHEME);
2475
2476
							if ($scheme == 'mailto')
2477
							{
2478
								$email_address = str_replace('mailto:', '', $url);
2479
								if (!isset($disabled['email']) && filter_var($email_address, FILTER_VALIDATE_EMAIL) !== false)
2480
									return '[email=' . $email_address . ']' . $url . '[/email]';
2481
								else
2482
									return $url;
2483
							}
2484
2485
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2486
							if (empty($scheme))
2487
								$fullUrl = '//' . ltrim($url, ':/');
2488
							else
2489
								$fullUrl = $url;
2490
2491
							// Make sure that $fullUrl really is valid
2492
							if (validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false)
2493
								return $url;
2494
2495
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2496
						}, $data);
2497
					}
2498
2499
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
2500
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
2501
					{
2502
						$email_regex = '
2503
						# Preceded by a non-domain character or start of line
2504
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
2505
2506
						# An email address
2507
						[\p{L}\p{M}\p{N}_\-.]{1,80}
2508
						@
2509
						[\p{L}\p{M}\p{N}\-.]+
2510
						\.
2511
						' . $modSettings['tld_regex'] . '
2512
2513
						# Followed by either:
2514
						(?=
2515
							# end of line or a non-domain character (excluding the dot)
2516
							$|[^\p{L}\p{M}\p{N}\-]
2517
							| # or
2518
							# a dot followed by end of line or a non-domain character
2519
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2520
						)';
2521
2522
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2523
					}
2524
				}
2525
			}
2526
2527
			// Restore any placeholders
2528
			$data = strtr($data, $placeholders);
2529
2530
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2531
2532
			// If it wasn't changed, no copying or other boring stuff has to happen!
2533
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2534
			{
2535
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2536
2537
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2538
				$old_pos = strlen($data) + $last_pos;
2539
				$pos = strpos($message, '[', $last_pos);
2540
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2541
			}
2542
		}
2543
2544
		// Are we there yet?  Are we there yet?
2545
		if ($pos >= strlen($message) - 1)
2546
			break;
2547
2548
		$tag_character = strtolower($message[$pos + 1]);
2549
2550
		if ($tag_character == '/' && !empty($open_tags))
2551
		{
2552
			$pos2 = strpos($message, ']', $pos + 1);
2553
			if ($pos2 == $pos + 2)
2554
				continue;
2555
2556
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2557
2558
			// A closing tag that doesn't match any open tags? Skip it.
2559
			if (!in_array($look_for, array_map(function($code)
2560
			{
2561
				return $code['tag'];
2562
			}, $open_tags)))
2563
				continue;
2564
2565
			$to_close = array();
2566
			$block_level = null;
2567
2568
			do
2569
			{
2570
				$tag = array_pop($open_tags);
2571
				if (!$tag)
2572
					break;
2573
2574
				if (!empty($tag['block_level']))
2575
				{
2576
					// Only find out if we need to.
2577
					if ($block_level === false)
2578
					{
2579
						array_push($open_tags, $tag);
2580
						break;
2581
					}
2582
2583
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2584
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2585
					{
2586
						foreach ($bbc_codes[$look_for[0]] as $temp)
2587
							if ($temp['tag'] == $look_for)
2588
							{
2589
								$block_level = !empty($temp['block_level']);
2590
								break;
2591
							}
2592
					}
2593
2594
					if ($block_level !== true)
2595
					{
2596
						$block_level = false;
2597
						array_push($open_tags, $tag);
2598
						break;
2599
					}
2600
				}
2601
2602
				$to_close[] = $tag;
2603
			}
2604
			while ($tag['tag'] != $look_for);
2605
2606
			// Did we just eat through everything and not find it?
2607
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2608
			{
2609
				$open_tags = $to_close;
2610
				continue;
2611
			}
2612
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2613
			{
2614
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2615
				{
2616
					foreach ($bbc_codes[$look_for[0]] as $temp)
2617
						if ($temp['tag'] == $look_for)
2618
						{
2619
							$block_level = !empty($temp['block_level']);
2620
							break;
2621
						}
2622
				}
2623
2624
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2625
				if (!$block_level)
2626
				{
2627
					foreach ($to_close as $tag)
2628
						array_push($open_tags, $tag);
2629
					continue;
2630
				}
2631
			}
2632
2633
			foreach ($to_close as $tag)
2634
			{
2635
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2636
				$pos += strlen($tag['after']) + 2;
2637
				$pos2 = $pos - 1;
2638
2639
				// See the comment at the end of the big loop - just eating whitespace ;).
2640
				$whitespace_regex = '';
2641
				if (!empty($tag['block_level']))
2642
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
2643
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2644
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2645
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2646
2647
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2648
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2649
			}
2650
2651
			if (!empty($to_close))
2652
			{
2653
				$to_close = array();
2654
				$pos--;
2655
			}
2656
2657
			continue;
2658
		}
2659
2660
		// No tags for this character, so just keep going (fastest possible course.)
2661
		if (!isset($bbc_codes[$tag_character]))
2662
			continue;
2663
2664
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2665
		$tag = null;
2666
		foreach ($bbc_codes[$tag_character] as $possible)
2667
		{
2668
			$pt_strlen = strlen($possible['tag']);
2669
2670
			// Not a match?
2671
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2672
				continue;
2673
2674
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
2675
2676
			// A tag is the last char maybe
2677
			if ($next_c == '')
2678
				break;
2679
2680
			// A test validation?
2681
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2682
				continue;
2683
			// Do we want parameters?
2684
			elseif (!empty($possible['parameters']))
2685
			{
2686
				// Are all the parameters optional?
2687
				$param_required = false;
2688
				foreach ($possible['parameters'] as $param)
2689
				{
2690
					if (empty($param['optional']))
2691
					{
2692
						$param_required = true;
2693
						break;
2694
					}
2695
				}
2696
2697
				if ($param_required && $next_c != ' ')
2698
					continue;
2699
			}
2700
			elseif (isset($possible['type']))
2701
			{
2702
				// Do we need an equal sign?
2703
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
2704
					continue;
2705
				// Maybe we just want a /...
2706
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
2707
					continue;
2708
				// An immediate ]?
2709
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
2710
					continue;
2711
			}
2712
			// No type means 'parsed_content', which demands an immediate ] without parameters!
2713
			elseif ($next_c != ']')
2714
				continue;
2715
2716
			// Check allowed tree?
2717
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
2718
				continue;
2719
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
2720
				continue;
2721
			// If this is in the list of disallowed child tags, don't parse it.
2722
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
2723
				continue;
2724
2725
			$pos1 = $pos + 1 + $pt_strlen + 1;
2726
2727
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
2728
			if ($possible['tag'] == 'quote')
2729
			{
2730
				// Start with standard
2731
				$quote_alt = false;
2732
				foreach ($open_tags as $open_quote)
2733
				{
2734
					// Every parent quote this quote has flips the styling
2735
					if ($open_quote['tag'] == 'quote')
2736
						$quote_alt = !$quote_alt;
2737
				}
2738
				// Add a class to the quote to style alternating blockquotes
2739
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
2740
			}
2741
2742
			// This is long, but it makes things much easier and cleaner.
2743
			if (!empty($possible['parameters']))
2744
			{
2745
				// Build a regular expression for each parameter for the current tag.
2746
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
2747
				if (!isset($params_regexes[$regex_key]))
2748
				{
2749
					$params_regexes[$regex_key] = '';
2750
2751
					foreach ($possible['parameters'] as $p => $info)
2752
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
2753
				}
2754
2755
				// Extract the string that potentially holds our parameters.
2756
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
2757
				$blobs = preg_split('~\]~i', $blob[1]);
2758
2759
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
2760
2761
				// Progressively append more blobs until we find our parameters or run out of blobs
2762
				$blob_counter = 1;
2763
				while ($blob_counter <= count($blobs))
2764
				{
2765
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
2766
2767
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
2768
					sort($given_params, SORT_STRING);
2769
2770
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
2771
2772
					if ($match)
2773
						break;
2774
				}
2775
2776
				// Didn't match our parameter list, try the next possible.
2777
				if (!$match)
2778
					continue;
2779
2780
				$params = array();
2781
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
2782
				{
2783
					$key = strtok(ltrim($matches[$i]), '=');
2784
					if ($key === false)
2785
						continue;
2786
					elseif (isset($possible['parameters'][$key]['value']))
2787
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
2788
					elseif (isset($possible['parameters'][$key]['validate']))
2789
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
2790
					else
2791
						$params['{' . $key . '}'] = $matches[$i + 1];
2792
2793
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
2794
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
2795
				}
2796
2797
				foreach ($possible['parameters'] as $p => $info)
2798
				{
2799
					if (!isset($params['{' . $p . '}']))
2800
					{
2801
						if (!isset($info['default']))
2802
							$params['{' . $p . '}'] = '';
2803
						elseif (isset($possible['parameters'][$p]['value']))
2804
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
2805
						elseif (isset($possible['parameters'][$p]['validate']))
2806
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
2807
						else
2808
							$params['{' . $p . '}'] = $info['default'];
2809
					}
2810
				}
2811
2812
				$tag = $possible;
2813
2814
				// Put the parameters into the string.
2815
				if (isset($tag['before']))
2816
					$tag['before'] = strtr($tag['before'], $params);
2817
				if (isset($tag['after']))
2818
					$tag['after'] = strtr($tag['after'], $params);
2819
				if (isset($tag['content']))
2820
					$tag['content'] = strtr($tag['content'], $params);
2821
2822
				$pos1 += strlen($given_param_string);
2823
			}
2824
			else
2825
			{
2826
				$tag = $possible;
2827
				$params = array();
2828
			}
2829
			break;
2830
		}
2831
2832
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
2833
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
2834
		{
2835
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
2836
				continue;
2837
2838
			$tag = $itemcodes[$message[$pos + 1]];
2839
2840
			// First let's set up the tree: it needs to be in a list, or after an li.
2841
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
2842
			{
2843
				$open_tags[] = array(
2844
					'tag' => 'list',
2845
					'after' => '</ul>',
2846
					'block_level' => true,
2847
					'require_children' => array('li'),
2848
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2849
				);
2850
				$code = '<ul class="bbc_list">';
2851
			}
2852
			// We're in a list item already: another itemcode?  Close it first.
2853
			elseif ($inside['tag'] == 'li')
2854
			{
2855
				array_pop($open_tags);
2856
				$code = '</li>';
2857
			}
2858
			else
2859
				$code = '';
2860
2861
			// Now we open a new tag.
2862
			$open_tags[] = array(
2863
				'tag' => 'li',
2864
				'after' => '</li>',
2865
				'trim' => 'outside',
2866
				'block_level' => true,
2867
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
2868
			);
2869
2870
			// First, open the tag...
2871
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
2872
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
2873
			$pos += strlen($code) - 1 + 2;
2874
2875
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
2876
			$pos2 = strpos($message, '<br>', $pos);
2877
			$pos3 = strpos($message, '[/', $pos);
2878
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
2879
			{
2880
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
2881
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
2882
2883
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
2884
			}
2885
			// Tell the [list] that it needs to close specially.
2886
			else
2887
			{
2888
				// Move the li over, because we're not sure what we'll hit.
2889
				$open_tags[count($open_tags) - 1]['after'] = '';
2890
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
2891
			}
2892
2893
			continue;
2894
		}
2895
2896
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
2897
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
2898
		{
2899
			array_pop($open_tags);
2900
2901
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
2902
			$pos += strlen($inside['after']) - 1 + 2;
2903
		}
2904
2905
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
2906
		if ($tag === null)
2907
			continue;
2908
2909
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
2910
		if (isset($inside['disallow_children']))
2911
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
2912
2913
		// Is this tag disabled?
2914
		if (isset($disabled[$tag['tag']]))
2915
		{
2916
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
2917
			{
2918
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
2919
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
2920
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
2921
			}
2922
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
2923
			{
2924
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
2925
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
2926
			}
2927
			else
2928
				$tag['content'] = $tag['disabled_content'];
2929
		}
2930
2931
		// we use this a lot
2932
		$tag_strlen = strlen($tag['tag']);
2933
2934
		// The only special case is 'html', which doesn't need to close things.
2935
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
2936
		{
2937
			$n = count($open_tags) - 1;
2938
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
2939
				$n--;
2940
2941
			// Close all the non block level tags so this tag isn't surrounded by them.
2942
			for ($i = count($open_tags) - 1; $i > $n; $i--)
2943
			{
2944
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
2945
				$ot_strlen = strlen($open_tags[$i]['after']);
2946
				$pos += $ot_strlen + 2;
2947
				$pos1 += $ot_strlen + 2;
2948
2949
				// Trim or eat trailing stuff... see comment at the end of the big loop.
2950
				$whitespace_regex = '';
2951
				if (!empty($tag['block_level']))
2952
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
2953
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2954
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2955
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2956
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2957
2958
				array_pop($open_tags);
2959
			}
2960
		}
2961
2962
		// Can't read past the end of the message
2963
		$pos1 = min(strlen($message), $pos1);
2964
2965
		// No type means 'parsed_content'.
2966
		if (!isset($tag['type']))
2967
		{
2968
			$open_tags[] = $tag;
2969
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
2970
			$pos += strlen($tag['before']) - 1 + 2;
2971
		}
2972
		// Don't parse the content, just skip it.
2973
		elseif ($tag['type'] == 'unparsed_content')
2974
		{
2975
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
2976
			if ($pos2 === false)
2977
				continue;
2978
2979
			$data = substr($message, $pos1, $pos2 - $pos1);
2980
2981
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
2982
				$data = substr($data, 4);
2983
2984
			if (isset($tag['validate']))
2985
				$tag['validate']($tag, $data, $disabled, $params);
2986
2987
			$code = strtr($tag['content'], array('$1' => $data));
2988
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
2989
2990
			$pos += strlen($code) - 1 + 2;
2991
			$last_pos = $pos + 1;
2992
		}
2993
		// Don't parse the content, just skip it.
2994
		elseif ($tag['type'] == 'unparsed_equals_content')
2995
		{
2996
			// The value may be quoted for some tags - check.
2997
			if (isset($tag['quoted']))
2998
			{
2999
				$quoted = substr($message, $pos1, 6) == '&quot;';
3000
				if ($tag['quoted'] != 'optional' && !$quoted)
3001
					continue;
3002
3003
				if ($quoted)
3004
					$pos1 += 6;
3005
			}
3006
			else
3007
				$quoted = false;
3008
3009
			$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
3010
			if ($pos2 === false)
3011
				continue;
3012
3013
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3014
			if ($pos3 === false)
3015
				continue;
3016
3017
			$data = array(
3018
				substr($message, $pos2 + ($quoted == false ? 1 : 7), $pos3 - ($pos2 + ($quoted == false ? 1 : 7))),
3019
				substr($message, $pos1, $pos2 - $pos1)
3020
			);
3021
3022
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3023
				$data[0] = substr($data[0], 4);
3024
3025
			// Validation for my parking, please!
3026
			if (isset($tag['validate']))
3027
				$tag['validate']($tag, $data, $disabled, $params);
3028
3029
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3030
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3031
			$pos += strlen($code) - 1 + 2;
3032
		}
3033
		// A closed tag, with no content or value.
3034
		elseif ($tag['type'] == 'closed')
3035
		{
3036
			$pos2 = strpos($message, ']', $pos);
3037
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3038
			$pos += strlen($tag['content']) - 1 + 2;
3039
		}
3040
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3041
		elseif ($tag['type'] == 'unparsed_commas_content')
3042
		{
3043
			$pos2 = strpos($message, ']', $pos1);
3044
			if ($pos2 === false)
3045
				continue;
3046
3047
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3048
			if ($pos3 === false)
3049
				continue;
3050
3051
			// We want $1 to be the content, and the rest to be csv.
3052
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3053
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3054
3055
			if (isset($tag['validate']))
3056
				$tag['validate']($tag, $data, $disabled, $params);
3057
3058
			$code = $tag['content'];
3059
			foreach ($data as $k => $d)
3060
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3061
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3062
			$pos += strlen($code) - 1 + 2;
3063
		}
3064
		// This has parsed content, and a csv value which is unparsed.
3065
		elseif ($tag['type'] == 'unparsed_commas')
3066
		{
3067
			$pos2 = strpos($message, ']', $pos1);
3068
			if ($pos2 === false)
3069
				continue;
3070
3071
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3072
3073
			if (isset($tag['validate']))
3074
				$tag['validate']($tag, $data, $disabled, $params);
3075
3076
			// Fix after, for disabled code mainly.
3077
			foreach ($data as $k => $d)
3078
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3079
3080
			$open_tags[] = $tag;
3081
3082
			// Replace them out, $1, $2, $3, $4, etc.
3083
			$code = $tag['before'];
3084
			foreach ($data as $k => $d)
3085
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3086
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3087
			$pos += strlen($code) - 1 + 2;
3088
		}
3089
		// A tag set to a value, parsed or not.
3090
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3091
		{
3092
			// The value may be quoted for some tags - check.
3093
			if (isset($tag['quoted']))
3094
			{
3095
				$quoted = substr($message, $pos1, 6) == '&quot;';
3096
				if ($tag['quoted'] != 'optional' && !$quoted)
3097
					continue;
3098
3099
				if ($quoted)
3100
					$pos1 += 6;
3101
			}
3102
			else
3103
				$quoted = false;
3104
3105
			$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
3106
			if ($pos2 === false)
3107
				continue;
3108
3109
			$data = substr($message, $pos1, $pos2 - $pos1);
3110
3111
			// Validation for my parking, please!
3112
			if (isset($tag['validate']))
3113
				$tag['validate']($tag, $data, $disabled, $params);
3114
3115
			// For parsed content, we must recurse to avoid security problems.
3116
			if ($tag['type'] != 'unparsed_equals')
3117
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3118
3119
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3120
3121
			$open_tags[] = $tag;
3122
3123
			$code = strtr($tag['before'], array('$1' => $data));
3124
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 7));
3125
			$pos += strlen($code) - 1 + 2;
3126
		}
3127
3128
		// If this is block level, eat any breaks after it.
3129
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3130
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3131
3132
		// Are we trimming outside this tag?
3133
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3134
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3135
	}
3136
3137
	// Close any remaining tags.
3138
	while ($tag = array_pop($open_tags))
3139
		$message .= "\n" . $tag['after'] . "\n";
3140
3141
	// Parse the smileys within the parts where it can be done safely.
3142
	if ($smileys === true)
3143
	{
3144
		$message_parts = explode("\n", $message);
3145
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3146
			parsesmileys($message_parts[$i]);
3147
3148
		$message = implode('', $message_parts);
3149
	}
3150
3151
	// No smileys, just get rid of the markers.
3152
	else
3153
		$message = strtr($message, array("\n" => ''));
3154
3155
	if ($message !== '' && $message[0] === ' ')
3156
		$message = '&nbsp;' . substr($message, 1);
3157
3158
	// Cleanup whitespace.
3159
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3160
3161
	// Allow mods access to what parse_bbc created
3162
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3163
3164
	// Cache the output if it took some time...
3165
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3166
		cache_put_data($cache_key, $message, 240);
3167
3168
	// If this was a force parse revert if needed.
3169
	if (!empty($parse_tags))
3170
	{
3171
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3172
		unset($real_alltags_regex);
3173
	}
3174
	elseif (!empty($bbc_codes))
3175
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3176
3177
	return $message;
3178
}
3179
3180
/**
3181
 * Parse smileys in the passed message.
3182
 *
3183
 * The smiley parsing function which makes pretty faces appear :).
3184
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3185
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3186
 * Caches the smileys from the database or array in memory.
3187
 * Doesn't return anything, but rather modifies message directly.
3188
 *
3189
 * @param string &$message The message to parse smileys in
3190
 */
3191
function parsesmileys(&$message)
3192
{
3193
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3194
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3195
3196
	// No smiley set at all?!
3197
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3198
		return;
3199
3200
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3201
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3202
3203
	// If smileyPregSearch hasn't been set, do it now.
3204
	if (empty($smileyPregSearch))
3205
	{
3206
		// Cache for longer when customized smiley codes aren't enabled
3207
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3208
3209
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3210
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3211
		{
3212
			$result = $smcFunc['db_query']('', '
3213
				SELECT s.code, f.filename, s.description
3214
				FROM {db_prefix}smileys AS s
3215
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3216
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3217
					AND s.code IN ({array_string:default_codes})' : '') . '
3218
				ORDER BY LENGTH(s.code) DESC',
3219
				array(
3220
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3221
					'smiley_set' => $user_info['smiley_set'],
3222
				)
3223
			);
3224
			$smileysfrom = array();
3225
			$smileysto = array();
3226
			$smileysdescs = array();
3227
			while ($row = $smcFunc['db_fetch_assoc']($result))
3228
			{
3229
				$smileysfrom[] = $row['code'];
3230
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3231
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3232
			}
3233
			$smcFunc['db_free_result']($result);
3234
3235
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3236
		}
3237
		else
3238
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3239
3240
		// The non-breaking-space is a complex thing...
3241
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3242
3243
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3244
		$smileyPregReplacements = array();
3245
		$searchParts = array();
3246
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3247
3248
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3249
		{
3250
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3251
			$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">';
3252
3253
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3254
3255
			$searchParts[] = $smileysfrom[$i];
3256
			if ($smileysfrom[$i] != $specialChars)
3257
			{
3258
				$smileyPregReplacements[$specialChars] = $smileyCode;
3259
				$searchParts[] = $specialChars;
3260
3261
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3262
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3263
				if ($specialChars2 != $specialChars)
3264
				{
3265
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3266
					$searchParts[] = $specialChars2;
3267
				}
3268
			}
3269
		}
3270
3271
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
3272
	}
3273
3274
	// Replace away!
3275
	$message = preg_replace_callback($smileyPregSearch, function($matches) use ($smileyPregReplacements)
3276
		{
3277
			return $smileyPregReplacements[$matches[1]];
3278
		}, $message);
3279
}
3280
3281
/**
3282
 * Highlight any code.
3283
 *
3284
 * Uses PHP's highlight_string() to highlight PHP syntax
3285
 * does special handling to keep the tabs in the code available.
3286
 * used to parse PHP code from inside [code] and [php] tags.
3287
 *
3288
 * @param string $code The code
3289
 * @return string The code with highlighted HTML.
3290
 */
3291
function highlight_php_code($code)
3292
{
3293
	// Remove special characters.
3294
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3295
3296
	$oldlevel = error_reporting(0);
3297
3298
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3299
3300
	error_reporting($oldlevel);
3301
3302
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3303
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3304
3305
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3306
}
3307
3308
/**
3309
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3310
 *
3311
 * The returned URL may or may not be a proxied URL, depending on the situation.
3312
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3313
 *
3314
 * @param string $url The original URL of the requested resource
3315
 * @return string The URL to use
3316
 */
3317
function get_proxied_url($url)
3318
{
3319
	global $boardurl, $image_proxy_enabled, $image_proxy_secret, $user_info;
3320
3321
	// Only use the proxy if enabled, and never for robots
3322
	if (empty($image_proxy_enabled) || !empty($user_info['possibly_robot']))
3323
		return $url;
3324
3325
	$parsedurl = parse_url($url);
3326
3327
	// Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs
3328
	if (empty($parsedurl['scheme']) || empty($parsedurl['host']) || empty($parsedurl['path']) || $parsedurl['scheme'] === 'https')
3329
		return $url;
3330
3331
	// We don't need to proxy our own resources
3332
	if ($parsedurl['host'] === parse_url($boardurl, PHP_URL_HOST))
3333
		return strtr($url, array('http://' => 'https://'));
3334
3335
	// By default, use SMF's own image proxy script
3336
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . hash_hmac('sha1', $url, $image_proxy_secret);
3337
3338
	// Allow mods to easily implement an alternative proxy
3339
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
3340
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
3341
3342
	return $proxied_url;
3343
}
3344
3345
/**
3346
 * Make sure the browser doesn't come back and repost the form data.
3347
 * Should be used whenever anything is posted.
3348
 *
3349
 * @param string $setLocation The URL to redirect them to
3350
 * @param bool $refresh Whether to use a meta refresh instead
3351
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
3352
 */
3353
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
3354
{
3355
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
3356
3357
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
3358
	if (!empty($context['flush_mail']))
3359
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3360
		AddMailQueue(true);
3361
3362
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
3363
3364
	if ($add)
3365
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
3366
3367
	// Put the session ID in.
3368
	if (defined('SID') && SID != '')
3369
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
3370
	// Keep that debug in their for template debugging!
3371
	elseif (isset($_GET['debug']))
3372
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
3373
3374
	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'])))
3375
	{
3376
		if (defined('SID') && SID != '')
3377
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
3378
				function($m) use ($scripturl)
3379
				{
3380
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
3381
				}, $setLocation);
3382
		else
3383
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
3384
				function($m) use ($scripturl)
3385
				{
3386
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
3387
				}, $setLocation);
3388
	}
3389
3390
	// Maybe integrations want to change where we are heading?
3391
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
3392
3393
	// Set the header.
3394
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
3395
3396
	// Debugging.
3397
	if (isset($db_show_debug) && $db_show_debug === true)
3398
		$_SESSION['debug_redirect'] = $db_cache;
3399
3400
	obExit(false);
3401
}
3402
3403
/**
3404
 * Ends execution.  Takes care of template loading and remembering the previous URL.
3405
 *
3406
 * @param bool $header Whether to do the header
3407
 * @param bool $do_footer Whether to do the footer
3408
 * @param bool $from_index Whether we're coming from the board index
3409
 * @param bool $from_fatal_error Whether we're coming from a fatal error
3410
 */
3411
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
3412
{
3413
	global $context, $settings, $modSettings, $txt, $smcFunc, $should_log;
3414
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
3415
3416
	// Attempt to prevent a recursive loop.
3417
	++$level;
3418
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
3419
		exit;
3420
	if ($from_fatal_error)
3421
		$has_fatal_error = true;
3422
3423
	// Clear out the stat cache.
3424
	if (function_exists('trackStats'))
3425
		trackStats();
3426
3427
	// If we have mail to send, send it.
3428
	if (function_exists('AddMailQueue') && !empty($context['flush_mail']))
3429
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3430
		AddMailQueue(true);
3431
3432
	$do_header = $header === null ? !$header_done : $header;
3433
	if ($do_footer === null)
3434
		$do_footer = $do_header;
3435
3436
	// Has the template/header been done yet?
3437
	if ($do_header)
3438
	{
3439
		// Was the page title set last minute? Also update the HTML safe one.
3440
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
3441
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3442
3443
		// Start up the session URL fixer.
3444
		ob_start('ob_sessrewrite');
3445
3446
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
3447
			$buffers = explode(',', $settings['output_buffers']);
3448
		elseif (!empty($settings['output_buffers']))
3449
			$buffers = $settings['output_buffers'];
3450
		else
3451
			$buffers = array();
3452
3453
		if (isset($modSettings['integrate_buffer']))
3454
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
3455
3456
		if (!empty($buffers))
3457
			foreach ($buffers as $function)
3458
			{
3459
				$call = call_helper($function, true);
3460
3461
				// Is it valid?
3462
				if (!empty($call))
3463
					ob_start($call);
3464
			}
3465
3466
		// Display the screen in the logical order.
3467
		template_header();
3468
		$header_done = true;
3469
	}
3470
	if ($do_footer)
3471
	{
3472
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
3473
3474
		// Anything special to put out?
3475
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
3476
			echo $context['insert_after_template'];
3477
3478
		// Just so we don't get caught in an endless loop of errors from the footer...
3479
		if (!$footer_done)
3480
		{
3481
			$footer_done = true;
3482
			template_footer();
3483
3484
			// (since this is just debugging... it's okay that it's after </html>.)
3485
			if (!isset($_REQUEST['xml']))
3486
				displayDebug();
3487
		}
3488
	}
3489
3490
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
3491
	if ($should_log)
3492
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
3493
3494
	// For session check verification.... don't switch browsers...
3495
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
3496
3497
	// Hand off the output to the portal, etc. we're integrated with.
3498
	call_integration_hook('integrate_exit', array($do_footer));
3499
3500
	// Don't exit if we're coming from index.php; that will pass through normally.
3501
	if (!$from_index)
3502
		exit;
3503
}
3504
3505
/**
3506
 * Get the size of a specified image with better error handling.
3507
 *
3508
 * @todo see if it's better in Subs-Graphics, but one step at the time.
3509
 * Uses getimagesize() to determine the size of a file.
3510
 * Attempts to connect to the server first so it won't time out.
3511
 *
3512
 * @param string $url The URL of the image
3513
 * @return array|false The image size as array (width, height), or false on failure
3514
 */
3515
function url_image_size($url)
3516
{
3517
	global $sourcedir;
3518
3519
	// Make sure it is a proper URL.
3520
	$url = str_replace(' ', '%20', $url);
3521
3522
	// Can we pull this from the cache... please please?
3523
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
3524
		return $temp;
3525
	$t = microtime(true);
3526
3527
	// Get the host to pester...
3528
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3529
3530
	// Can't figure it out, just try the image size.
3531
	if ($url == '' || $url == 'http://' || $url == 'https://')
3532
	{
3533
		return false;
3534
	}
3535
	elseif (!isset($match[1]))
3536
	{
3537
		$size = @getimagesize($url);
3538
	}
3539
	else
3540
	{
3541
		// Try to connect to the server... give it half a second.
3542
		$temp = 0;
3543
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
3544
3545
		// Successful?  Continue...
3546
		if ($fp != false)
3547
		{
3548
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3549
			fwrite($fp, 'HEAD /' . $match[2] . ' HTTP/1.1' . "\r\n" . 'Host: ' . $match[1] . "\r\n" . 'user-agent: '. SMF_USER_AGENT . "\r\n" . 'Connection: close' . "\r\n\r\n");
3550
3551
			// Read in the HTTP/1.1 or whatever.
3552
			$test = substr(fgets($fp, 11), -1);
3553
			fclose($fp);
3554
3555
			// See if it returned a 404/403 or something.
3556
			if ($test < 4)
3557
			{
3558
				$size = @getimagesize($url);
3559
3560
				// This probably means allow_url_fopen is off, let's try GD.
3561
				if ($size === false && function_exists('imagecreatefromstring'))
3562
				{
3563
					// It's going to hate us for doing this, but another request...
3564
					$image = @imagecreatefromstring(fetch_web_data($url));
3565
					if ($image !== false)
3566
					{
3567
						$size = array(imagesx($image), imagesy($image));
3568
						imagedestroy($image);
3569
					}
3570
				}
3571
			}
3572
		}
3573
	}
3574
3575
	// If we didn't get it, we failed.
3576
	if (!isset($size))
3577
		$size = false;
3578
3579
	// If this took a long time, we may never have to do it again, but then again we might...
3580
	if (microtime(true) - $t > 0.8)
3581
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3582
3583
	// Didn't work.
3584
	return $size;
3585
}
3586
3587
/**
3588
 * Sets up the basic theme context stuff.
3589
 *
3590
 * @param bool $forceload Whether to load the theme even if it's already loaded
3591
 */
3592
function setupThemeContext($forceload = false)
3593
{
3594
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3595
	global $smcFunc;
3596
	static $loaded = false;
3597
3598
	// Under SSI this function can be called more then once.  That can cause some problems.
3599
	//   So only run the function once unless we are forced to run it again.
3600
	if ($loaded && !$forceload)
3601
		return;
3602
3603
	$loaded = true;
3604
3605
	$context['in_maintenance'] = !empty($maintenance);
3606
	$context['current_time'] = timeformat(time(), false);
3607
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3608
	$context['random_news_line'] = array();
3609
3610
	// Get some news...
3611
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3612
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3613
	{
3614
		if (trim($context['news_lines'][$i]) == '')
3615
			continue;
3616
3617
		// Clean it up for presentation ;).
3618
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3619
	}
3620
3621
	if (!empty($context['news_lines']) && (!empty($modSettings['allow_guestAccess']) || $context['user']['is_logged']))
3622
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3623
3624
	if (!$user_info['is_guest'])
3625
	{
3626
		$context['user']['messages'] = &$user_info['messages'];
3627
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3628
		$context['user']['alerts'] = &$user_info['alerts'];
3629
3630
		// Personal message popup...
3631
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3632
			$context['user']['popup_messages'] = true;
3633
		else
3634
			$context['user']['popup_messages'] = false;
3635
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3636
3637
		if (allowedTo('moderate_forum'))
3638
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
3639
3640
		$context['user']['avatar'] = array();
3641
3642
		// Check for gravatar first since we might be forcing them...
3643
		if (($modSettings['gravatarEnabled'] && substr($user_info['avatar']['url'], 0, 11) == 'gravatar://') || !empty($modSettings['gravatarOverride']))
3644
		{
3645
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3646
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3647
			else
3648
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3649
		}
3650
		// Uploaded?
3651
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3652
			$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';
3653
		// Full URL?
3654
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3655
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3656
		// Otherwise we assume it's server stored.
3657
		elseif ($user_info['avatar']['url'] != '')
3658
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3659
		// No avatar at all? Fine, we have a big fat default avatar ;)
3660
		else
3661
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3662
3663
		if (!empty($context['user']['avatar']))
3664
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3665
3666
		// Figure out how long they've been logged in.
3667
		$context['user']['total_time_logged_in'] = array(
3668
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3669
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3670
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3671
		);
3672
	}
3673
	else
3674
	{
3675
		$context['user']['messages'] = 0;
3676
		$context['user']['unread_messages'] = 0;
3677
		$context['user']['avatar'] = array();
3678
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3679
		$context['user']['popup_messages'] = false;
3680
3681
		if (!empty($modSettings['registration_method']) && $modSettings['registration_method'] == 1)
3682
			$txt['welcome_guest'] .= $txt['welcome_guest_activate'];
3683
3684
		// If we've upgraded recently, go easy on the passwords.
3685
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3686
			$context['disable_login_hashing'] = true;
3687
	}
3688
3689
	// Setup the main menu items.
3690
	setupMenuContext();
3691
3692
	// This is here because old index templates might still use it.
3693
	$context['show_news'] = !empty($settings['enable_news']);
3694
3695
	// This is done to allow theme authors to customize it as they want.
3696
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
3697
3698
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
3699
	if ($context['show_pm_popup'])
3700
		addInlineJavaScript('
3701
		jQuery(document).ready(function($) {
3702
			new smc_Popup({
3703
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
3704
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
3705
				icon_class: \'main_icons mail_new\'
3706
			});
3707
		});');
3708
3709
	// Add a generic "Are you sure?" confirmation message.
3710
	addInlineJavaScript('
3711
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
3712
3713
	// Now add the capping code for avatars.
3714
	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')
3715
		addInlineCss('
3716
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
3717
3718
	// Add max image limits
3719
	if (!empty($modSettings['max_image_width']))
3720
		addInlineCss('
3721
	.postarea .bbc_img { max-width: ' . $modSettings['max_image_width'] . 'px; }');
3722
3723
	if (!empty($modSettings['max_image_height']))
3724
		addInlineCss('
3725
	.postarea .bbc_img { max-height: ' . $modSettings['max_image_height'] . 'px; }');
3726
3727
	// This looks weird, but it's because BoardIndex.php references the variable.
3728
	$context['common_stats']['latest_member'] = array(
3729
		'id' => $modSettings['latestMember'],
3730
		'name' => $modSettings['latestRealName'],
3731
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
3732
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
3733
	);
3734
	$context['common_stats'] = array(
3735
		'total_posts' => comma_format($modSettings['totalMessages']),
3736
		'total_topics' => comma_format($modSettings['totalTopics']),
3737
		'total_members' => comma_format($modSettings['totalMembers']),
3738
		'latest_member' => $context['common_stats']['latest_member'],
3739
	);
3740
	$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']);
3741
3742
	if (empty($settings['theme_version']))
3743
		addJavaScriptVar('smf_scripturl', $scripturl);
3744
3745
	if (!isset($context['page_title']))
3746
		$context['page_title'] = '';
3747
3748
	// Set some specific vars.
3749
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3750
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
3751
3752
	// Content related meta tags, including Open Graph
3753
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
3754
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
3755
3756
	if (!empty($context['meta_keywords']))
3757
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
3758
3759
	if (!empty($context['canonical_url']))
3760
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
3761
3762
	if (!empty($settings['og_image']))
3763
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
3764
3765
	if (!empty($context['meta_description']))
3766
	{
3767
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
3768
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
3769
	}
3770
	else
3771
	{
3772
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
3773
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
3774
	}
3775
3776
	call_integration_hook('integrate_theme_context');
3777
}
3778
3779
/**
3780
 * Helper function to set the system memory to a needed value
3781
 * - If the needed memory is greater than current, will attempt to get more
3782
 * - if in_use is set to true, will also try to take the current memory usage in to account
3783
 *
3784
 * @param string $needed The amount of memory to request, if needed, like 256M
3785
 * @param bool $in_use Set to true to account for current memory usage of the script
3786
 * @return boolean True if we have at least the needed memory
3787
 */
3788
function setMemoryLimit($needed, $in_use = false)
3789
{
3790
	// everything in bytes
3791
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3792
	$memory_needed = memoryReturnBytes($needed);
3793
3794
	// should we account for how much is currently being used?
3795
	if ($in_use)
3796
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
3797
3798
	// if more is needed, request it
3799
	if ($memory_current < $memory_needed)
3800
	{
3801
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
3802
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
3803
	}
3804
3805
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
3806
3807
	// return success or not
3808
	return (bool) ($memory_current >= $memory_needed);
3809
}
3810
3811
/**
3812
 * Helper function to convert memory string settings to bytes
3813
 *
3814
 * @param string $val The byte string, like 256M or 1G
3815
 * @return integer The string converted to a proper integer in bytes
3816
 */
3817
function memoryReturnBytes($val)
3818
{
3819
	if (is_integer($val))
3820
		return $val;
3821
3822
	// Separate the number from the designator
3823
	$val = trim($val);
3824
	$num = intval(substr($val, 0, strlen($val) - 1));
3825
	$last = strtolower(substr($val, -1));
3826
3827
	// convert to bytes
3828
	switch ($last)
3829
	{
3830
		case 'g':
3831
			$num *= 1024;
3832
		case 'm':
3833
			$num *= 1024;
3834
		case 'k':
3835
			$num *= 1024;
3836
	}
3837
	return $num;
3838
}
3839
3840
/**
3841
 * The header template
3842
 */
3843
function template_header()
3844
{
3845
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir, $cache_enable;
3846
3847
	setupThemeContext();
3848
3849
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
3850
	if (empty($context['no_last_modified']))
3851
	{
3852
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
3853
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
3854
3855
		// Are we debugging the template/html content?
3856
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
3857
			header('content-type: application/xhtml+xml');
3858
		elseif (!isset($_REQUEST['xml']))
3859
			header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3860
	}
3861
3862
	header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
3863
3864
	// We need to splice this in after the body layer, or after the main layer for older stuff.
3865
	if ($context['in_maintenance'] && $context['user']['is_admin'])
3866
	{
3867
		$position = array_search('body', $context['template_layers']);
3868
		if ($position === false)
3869
			$position = array_search('main', $context['template_layers']);
3870
3871
		if ($position !== false)
3872
		{
3873
			$before = array_slice($context['template_layers'], 0, $position + 1);
3874
			$after = array_slice($context['template_layers'], $position + 1);
3875
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
3876
		}
3877
	}
3878
3879
	$checked_securityFiles = false;
3880
	$showed_banned = false;
3881
	foreach ($context['template_layers'] as $layer)
3882
	{
3883
		loadSubTemplate($layer . '_above', true);
3884
3885
		// May seem contrived, but this is done in case the body and main layer aren't there...
3886
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
3887
		{
3888
			$checked_securityFiles = true;
3889
3890
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
3891
3892
			// Add your own files.
3893
			call_integration_hook('integrate_security_files', array(&$securityFiles));
3894
3895
			foreach ($securityFiles as $i => $securityFile)
3896
			{
3897
				if (!file_exists($boarddir . '/' . $securityFile))
3898
					unset($securityFiles[$i]);
3899
			}
3900
3901
			// We are already checking so many files...just few more doesn't make any difference! :P
3902
			if (!empty($modSettings['currentAttachmentUploadDir']))
3903
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
3904
3905
			else
3906
				$path = $modSettings['attachmentUploadDir'];
3907
3908
			secureDirectory($path, true);
3909
			secureDirectory($cachedir);
3910
3911
			// If agreement is enabled, at least the english version shall exists
3912
			if ($modSettings['requireAgreement'])
3913
				$agreement = !file_exists($boarddir . '/agreement.txt');
3914
3915
			if (!empty($securityFiles) || (!empty($cache_enable) && !is_writable($cachedir)) || !empty($agreement))
3916
			{
3917
				echo '
3918
		<div class="errorbox">
3919
			<p class="alert">!!</p>
3920
			<h3>', empty($securityFiles) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
3921
			<p>';
3922
3923
				foreach ($securityFiles as $securityFile)
3924
				{
3925
					echo '
3926
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
3927
3928
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
3929
						echo '
3930
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
3931
				}
3932
3933
				if (!empty($cache_enable) && !is_writable($cachedir))
3934
					echo '
3935
				<strong>', $txt['cache_writable'], '</strong><br>';
3936
3937
				if (!empty($agreement))
3938
					echo '
3939
				<strong>', $txt['agreement_missing'], '</strong><br>';
3940
3941
				echo '
3942
			</p>
3943
		</div>';
3944
			}
3945
		}
3946
		// If the user is banned from posting inform them of it.
3947
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
3948
		{
3949
			$showed_banned = true;
3950
			echo '
3951
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
3952
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
3953
3954
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
3955
				echo '
3956
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
3957
3958
			if (!empty($_SESSION['ban']['expire_time']))
3959
				echo '
3960
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
3961
			else
3962
				echo '
3963
					<div>', $txt['your_ban_expires_never'], '</div>';
3964
3965
			echo '
3966
				</div>';
3967
		}
3968
	}
3969
}
3970
3971
/**
3972
 * Show the copyright.
3973
 */
3974
function theme_copyright()
3975
{
3976
	global $forum_copyright;
3977
3978
	// Don't display copyright for things like SSI.
3979
	if (SMF !== 1)
3980
		return;
3981
3982
	// Put in the version...
3983
	printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR);
3984
}
3985
3986
/**
3987
 * The template footer
3988
 */
3989
function template_footer()
3990
{
3991
	global $context, $modSettings, $db_count;
3992
3993
	// Show the load time?  (only makes sense for the footer.)
3994
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
3995
	$context['load_time'] = round(microtime(true) - TIME_START, 3);
3996
	$context['load_queries'] = $db_count;
3997
3998
	if (!empty($context['template_layers']) && is_array($context['template_layers']))
3999
		foreach (array_reverse($context['template_layers']) as $layer)
4000
			loadSubTemplate($layer . '_below', true);
4001
}
4002
4003
/**
4004
 * Output the Javascript files
4005
 * 	- tabbing in this function is to make the HTML source look good and proper
4006
 *  - if deferred is set function will output all JS set to load at page end
4007
 *
4008
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
4009
 */
4010
function template_javascript($do_deferred = false)
4011
{
4012
	global $context, $modSettings, $settings;
4013
4014
	// Use this hook to minify/optimize Javascript files and vars
4015
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
4016
4017
	$toMinify = array(
4018
		'standard' => array(),
4019
		'defer' => array(),
4020
		'async' => array(),
4021
	);
4022
4023
	// Ouput the declared Javascript variables.
4024
	if (!empty($context['javascript_vars']) && !$do_deferred)
4025
	{
4026
		echo '
4027
	<script>';
4028
4029
		foreach ($context['javascript_vars'] as $key => $value)
4030
		{
4031
			if (empty($value))
4032
			{
4033
				echo '
4034
		var ', $key, ';';
4035
			}
4036
			else
4037
			{
4038
				echo '
4039
		var ', $key, ' = ', $value, ';';
4040
			}
4041
		}
4042
4043
		echo '
4044
	</script>';
4045
	}
4046
4047
	// In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body.
4048
	// Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance.
4049
	if (!$do_deferred)
4050
	{
4051
		// While we have JavaScript files to place in the template.
4052
		foreach ($context['javascript_files'] as $id => $js_file)
4053
		{
4054
			// Last minute call! allow theme authors to disable single files.
4055
			if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4056
				continue;
4057
4058
			// By default files don't get minimized unless the file explicitly says so!
4059
			if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
4060
			{
4061
				if (!empty($js_file['options']['async']))
4062
					$toMinify['async'][] = $js_file;
4063
4064
				elseif (!empty($js_file['options']['defer']))
4065
					$toMinify['defer'][] = $js_file;
4066
4067
				else
4068
					$toMinify['standard'][] = $js_file;
4069
4070
				// Grab a random seed.
4071
				if (!isset($minSeed) && isset($js_file['options']['seed']))
4072
					$minSeed = $js_file['options']['seed'];
4073
			}
4074
4075
			else
4076
			{
4077
				echo '
4078
	<script src="', $js_file['fileUrl'], isset($js_file['options']['seed']) ? $js_file['options']['seed'] : '', '"', !empty($js_file['options']['async']) ? ' async' : '', !empty($js_file['options']['defer']) ? ' defer' : '';
4079
4080
				if (!empty($js_file['options']['attributes']))
4081
					foreach ($js_file['options']['attributes'] as $key => $value)
4082
					{
4083
						if (is_bool($value))
4084
							echo !empty($value) ? ' ' . $key : '';
4085
4086
						else
4087
							echo ' ', $key, '="', $value, '"';
4088
					}
4089
4090
				echo '></script>';
4091
			}
4092
		}
4093
4094
		foreach ($toMinify as $js_files)
4095
		{
4096
			if (!empty($js_files))
4097
			{
4098
				$result = custMinify($js_files, 'js');
4099
4100
				$minSuccessful = array_keys($result) === array('smf_minified');
4101
4102
				foreach ($result as $minFile)
4103
					echo '
4104
	<script src="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '"', !empty($minFile['options']['async']) ? ' async' : '', !empty($minFile['options']['defer']) ? ' defer' : '', '></script>';
4105
			}
4106
		}
4107
	}
4108
4109
	// Inline JavaScript - Actually useful some times!
4110
	if (!empty($context['javascript_inline']))
4111
	{
4112
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
4113
		{
4114
			echo '
4115
<script>
4116
window.addEventListener("DOMContentLoaded", function() {';
4117
4118
			foreach ($context['javascript_inline']['defer'] as $js_code)
4119
				echo $js_code;
4120
4121
			echo '
4122
});
4123
</script>';
4124
		}
4125
4126
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
4127
		{
4128
			echo '
4129
	<script>';
4130
4131
			foreach ($context['javascript_inline']['standard'] as $js_code)
4132
				echo $js_code;
4133
4134
			echo '
4135
	</script>';
4136
		}
4137
	}
4138
}
4139
4140
/**
4141
 * Output the CSS files
4142
 */
4143
function template_css()
4144
{
4145
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
4146
4147
	// Use this hook to minify/optimize CSS files
4148
	call_integration_hook('integrate_pre_css_output');
4149
4150
	$toMinify = array();
4151
	$normal = array();
4152
4153
	usort($context['css_files'], function ($a, $b)
4154
	{
4155
		return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0);
4156
	});
4157
	foreach ($context['css_files'] as $id => $file)
4158
	{
4159
		// Last minute call! allow theme authors to disable single files.
4160
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4161
			continue;
4162
4163
		// Files are minimized unless they explicitly opt out.
4164
		if (!isset($file['options']['minimize']))
4165
			$file['options']['minimize'] = true;
4166
4167
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss']))
4168
		{
4169
			$toMinify[] = $file;
4170
4171
			// Grab a random seed.
4172
			if (!isset($minSeed) && isset($file['options']['seed']))
4173
				$minSeed = $file['options']['seed'];
4174
		}
4175
		else
4176
			$normal[] = array(
4177
				'url' => $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : ''),
4178
				'attributes' => !empty($file['options']['attributes']) ? $file['options']['attributes'] : array()
4179
			);
4180
	}
4181
4182
	if (!empty($toMinify))
4183
	{
4184
		$result = custMinify($toMinify, 'css');
4185
4186
		$minSuccessful = array_keys($result) === array('smf_minified');
4187
4188
		foreach ($result as $minFile)
4189
			echo '
4190
	<link rel="stylesheet" href="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '">';
4191
	}
4192
4193
	// Print the rest after the minified files.
4194
	if (!empty($normal))
4195
		foreach ($normal as $nf)
4196
		{
4197
			echo '
4198
	<link rel="stylesheet" href="', $nf['url'], '"';
4199
4200
			if (!empty($nf['attributes']))
4201
				foreach ($nf['attributes'] as $key => $value)
4202
				{
4203
					if (is_bool($value))
4204
						echo !empty($value) ? ' ' . $key : '';
4205
					else
4206
						echo ' ', $key, '="', $value, '"';
4207
				}
4208
4209
			echo '>';
4210
		}
4211
4212
	if ($db_show_debug === true)
4213
	{
4214
		// Try to keep only what's useful.
4215
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
4216
		foreach ($context['css_files'] as $file)
4217
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
4218
	}
4219
4220
	if (!empty($context['css_header']))
4221
	{
4222
		echo '
4223
	<style>';
4224
4225
		foreach ($context['css_header'] as $css)
4226
			echo $css . '
4227
	';
4228
4229
		echo '
4230
	</style>';
4231
	}
4232
}
4233
4234
/**
4235
 * Get an array of previously defined files and adds them to our main minified files.
4236
 * Sets a one day cache to avoid re-creating a file on every request.
4237
 *
4238
 * @param array $data The files to minify.
4239
 * @param string $type either css or js.
4240
 * @return array Info about the minified file, or about the original files if the minify process failed.
4241
 */
4242
function custMinify($data, $type)
4243
{
4244
	global $settings, $txt;
4245
4246
	$types = array('css', 'js');
4247
	$type = !empty($type) && in_array($type, $types) ? $type : false;
4248
	$data = is_array($data) ? $data : array();
4249
4250
	if (empty($type) || empty($data))
4251
		return $data;
4252
4253
	// Different pages include different files, so we use a hash to label the different combinations
4254
	$hash = md5(implode(' ', array_map(function($file)
4255
	{
4256
		return $file['filePath'] . '-' . $file['mtime'];
4257
	}, $data)));
4258
4259
	// Is this a deferred or asynchronous JavaScript file?
4260
	$async = $type === 'js';
4261
	$defer = $type === 'js';
4262
	if ($type === 'js')
4263
	{
4264
		foreach ($data as $id => $file)
4265
		{
4266
			// A minified script should only be loaded asynchronously if all its components wanted to be.
4267
			if (empty($file['options']['async']))
4268
				$async = false;
4269
4270
			// A minified script should only be deferred if all its components wanted to be.
4271
			if (empty($file['options']['defer']))
4272
				$defer = false;
4273
		}
4274
	}
4275
4276
	// Did we already do this?
4277
	$minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type;
4278
	$already_exists = file_exists($minified_file);
4279
4280
	// Already done?
4281
	if ($already_exists)
4282
	{
4283
		return array('smf_minified' => array(
4284
			'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4285
			'filePath' => $minified_file,
4286
			'fileName' => basename($minified_file),
4287
			'options' => array('async' => !empty($async), 'defer' => !empty($defer)),
4288
		));
4289
	}
4290
	// File has to exist. If it doesn't, try to create it.
4291
	elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file))
4292
	{
4293
		loadLanguage('Errors');
4294
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4295
4296
		// The process failed, so roll back to print each individual file.
4297
		return $data;
4298
	}
4299
4300
	// No namespaces, sorry!
4301
	$classType = 'MatthiasMullie\\Minify\\' . strtoupper($type);
4302
4303
	$minifier = new $classType();
4304
4305
	foreach ($data as $id => $file)
4306
	{
4307
		$toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false;
4308
4309
		// The file couldn't be located so it won't be added. Log this error.
4310
		if (empty($toAdd))
4311
		{
4312
			loadLanguage('Errors');
4313
			log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general');
4314
			continue;
4315
		}
4316
4317
		// Add this file to the list.
4318
		$minifier->add($toAdd);
4319
	}
4320
4321
	// Create the file.
4322
	$minifier->minify($minified_file);
4323
	unset($minifier);
4324
	clearstatcache();
4325
4326
	// Minify process failed.
4327
	if (!filesize($minified_file))
4328
	{
4329
		loadLanguage('Errors');
4330
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4331
4332
		// The process failed so roll back to print each individual file.
4333
		return $data;
4334
	}
4335
4336
	return array('smf_minified' => array(
4337
		'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4338
		'filePath' => $minified_file,
4339
		'fileName' => basename($minified_file),
4340
		'options' => array('async' => $async, 'defer' => $defer),
4341
	));
4342
}
4343
4344
/**
4345
 * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date
4346
 */
4347
function deleteAllMinified()
4348
{
4349
	global $smcFunc, $txt, $modSettings;
4350
4351
	$not_deleted = array();
4352
	$most_recent = 0;
4353
4354
	// Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie.
4355
	$request = $smcFunc['db_query']('', '
4356
		SELECT id_theme AS id, value AS dir
4357
		FROM {db_prefix}themes
4358
		WHERE variable = {string:var}',
4359
		array(
4360
			'var' => 'theme_dir',
4361
		)
4362
	);
4363
	while ($theme = $smcFunc['db_fetch_assoc']($request))
4364
	{
4365
		foreach (array('css', 'js') as $type)
4366
		{
4367
			foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename)
4368
			{
4369
				// We want to find the most recent mtime of non-minified files
4370
				if (strpos(pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
4371
					$most_recent = max($modSettings['browser_cache'], (int) @filemtime($filename));
4372
4373
				// Try to delete minified files. Add them to our error list if that fails.
4374
				elseif (!@unlink($filename))
4375
					$not_deleted[] = $filename;
4376
			}
4377
		}
4378
	}
4379
	$smcFunc['db_free_result']($request);
4380
4381
	// This setting tracks the most recent modification time of any of our CSS and JS files
4382
	if ($most_recent > $modSettings['browser_cache'])
4383
		updateSettings(array('browser_cache' => $most_recent));
4384
4385
	// If any of the files could not be deleted, log an error about it.
4386
	if (!empty($not_deleted))
4387
	{
4388
		loadLanguage('Errors');
4389
		log_error(sprintf($txt['unlink_minimized_fail'], implode('<br>', $not_deleted)), 'general');
4390
	}
4391
}
4392
4393
/**
4394
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
4395
 *
4396
 * @todo this currently returns the hash if new, and the full filename otherwise.
4397
 * Something messy like that.
4398
 * @todo and of course everything relies on this behavior and work around it. :P.
4399
 * Converters included.
4400
 *
4401
 * @param string $filename The name of the file
4402
 * @param int $attachment_id The ID of the attachment
4403
 * @param string|null $dir Which directory it should be in (null to use current one)
4404
 * @param bool $new Whether this is a new attachment
4405
 * @param string $file_hash The file hash
4406
 * @return string The path to the file
4407
 */
4408
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
4409
{
4410
	global $modSettings, $smcFunc;
4411
4412
	// Just make up a nice hash...
4413
	if ($new)
4414
		return sha1(md5($filename . time()) . mt_rand());
4415
4416
	// Just make sure that attachment id is only a int
4417
	$attachment_id = (int) $attachment_id;
4418
4419
	// Grab the file hash if it wasn't added.
4420
	// Left this for legacy.
4421
	if ($file_hash === '')
4422
	{
4423
		$request = $smcFunc['db_query']('', '
4424
			SELECT file_hash
4425
			FROM {db_prefix}attachments
4426
			WHERE id_attach = {int:id_attach}',
4427
			array(
4428
				'id_attach' => $attachment_id,
4429
			)
4430
		);
4431
4432
		if ($smcFunc['db_num_rows']($request) === 0)
4433
			return false;
4434
4435
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
4436
		$smcFunc['db_free_result']($request);
4437
	}
4438
4439
	// Still no hash? mmm...
4440
	if (empty($file_hash))
4441
		$file_hash = sha1(md5($filename . time()) . mt_rand());
4442
4443
	// Are we using multiple directories?
4444
	if (is_array($modSettings['attachmentUploadDir']))
4445
		$path = $modSettings['attachmentUploadDir'][$dir];
4446
4447
	else
4448
		$path = $modSettings['attachmentUploadDir'];
4449
4450
	return $path . '/' . $attachment_id . '_' . $file_hash . '.dat';
4451
}
4452
4453
/**
4454
 * Convert a single IP to a ranged IP.
4455
 * internal function used to convert a user-readable format to a format suitable for the database.
4456
 *
4457
 * @param string $fullip The full IP
4458
 * @return array An array of IP parts
4459
 */
4460
function ip2range($fullip)
4461
{
4462
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
4463
	if ($fullip == 'unknown')
4464
		$fullip = '255.255.255.255';
4465
4466
	$ip_parts = explode('-', $fullip);
4467
	$ip_array = array();
4468
4469
	// if ip 22.12.31.21
4470
	if (count($ip_parts) == 1 && isValidIP($fullip))
4471
	{
4472
		$ip_array['low'] = $fullip;
4473
		$ip_array['high'] = $fullip;
4474
		return $ip_array;
4475
	} // if ip 22.12.* -> 22.12.* - 22.12.*
4476
	elseif (count($ip_parts) == 1)
4477
	{
4478
		$ip_parts[0] = $fullip;
4479
		$ip_parts[1] = $fullip;
4480
	}
4481
4482
	// if ip 22.12.31.21-12.21.31.21
4483
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
4484
	{
4485
		$ip_array['low'] = $ip_parts[0];
4486
		$ip_array['high'] = $ip_parts[1];
4487
		return $ip_array;
4488
	}
4489
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
4490
	{
4491
		$valid_low = isValidIP($ip_parts[0]);
4492
		$valid_high = isValidIP($ip_parts[1]);
4493
		$count = 0;
4494
		$mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.');
4495
		$max = ($mode == ':' ? 'ffff' : '255');
4496
		$min = 0;
4497
		if (!$valid_low)
4498
		{
4499
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
4500
			$valid_low = isValidIP($ip_parts[0]);
4501
			while (!$valid_low)
4502
			{
4503
				$ip_parts[0] .= $mode . $min;
4504
				$valid_low = isValidIP($ip_parts[0]);
4505
				$count++;
4506
				if ($count > 9) break;
4507
			}
4508
		}
4509
4510
		$count = 0;
4511
		if (!$valid_high)
4512
		{
4513
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
4514
			$valid_high = isValidIP($ip_parts[1]);
4515
			while (!$valid_high)
4516
			{
4517
				$ip_parts[1] .= $mode . $max;
4518
				$valid_high = isValidIP($ip_parts[1]);
4519
				$count++;
4520
				if ($count > 9) break;
4521
			}
4522
		}
4523
4524
		if ($valid_high && $valid_low)
4525
		{
4526
			$ip_array['low'] = $ip_parts[0];
4527
			$ip_array['high'] = $ip_parts[1];
4528
		}
4529
	}
4530
4531
	return $ip_array;
4532
}
4533
4534
/**
4535
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
4536
 *
4537
 * @param string $ip The IP to get the hostname from
4538
 * @return string The hostname
4539
 */
4540
function host_from_ip($ip)
4541
{
4542
	global $modSettings;
4543
4544
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
4545
		return $host;
4546
	$t = microtime(true);
4547
4548
	// Try the Linux host command, perhaps?
4549
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
4550
	{
4551
		if (!isset($modSettings['host_to_dis']))
4552
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
4553
		else
4554
			$test = @shell_exec('host ' . @escapeshellarg($ip));
4555
4556
		// Did host say it didn't find anything?
4557
		if (strpos($test, 'not found') !== false)
4558
			$host = '';
4559
		// Invalid server option?
4560
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
4561
			updateSettings(array('host_to_dis' => 1));
4562
		// Maybe it found something, after all?
4563
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
4564
			$host = $match[1];
4565
	}
4566
4567
	// This is nslookup; usually only Windows, but possibly some Unix?
4568
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
4569
	{
4570
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
4571
		if (strpos($test, 'Non-existent domain') !== false)
4572
			$host = '';
4573
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
4574
			$host = $match[1];
4575
	}
4576
4577
	// This is the last try :/.
4578
	if (!isset($host) || $host === false)
4579
		$host = @gethostbyaddr($ip);
4580
4581
	// It took a long time, so let's cache it!
4582
	if (microtime(true) - $t > 0.5)
4583
		cache_put_data('hostlookup-' . $ip, $host, 600);
4584
4585
	return $host;
4586
}
4587
4588
/**
4589
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
4590
 *
4591
 * @param string $text The text to split into words
4592
 * @param int $max_chars The maximum number of characters per word
4593
 * @param bool $encrypt Whether to encrypt the results
4594
 * @return array An array of ints or words depending on $encrypt
4595
 */
4596
function text2words($text, $max_chars = 20, $encrypt = false)
4597
{
4598
	global $smcFunc, $context;
4599
4600
	// Upgrader may be working on old DBs...
4601
	if (!isset($context['utf8']))
4602
		$context['utf8'] = false;
4603
4604
	// Step 1: Remove entities/things we don't consider words:
4605
	$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>' => ' ')));
4606
4607
	// Step 2: Entities we left to letters, where applicable, lowercase.
4608
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
4609
4610
	// Step 3: Ready to split apart and index!
4611
	$words = explode(' ', $words);
4612
4613
	if ($encrypt)
4614
	{
4615
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
4616
		$returned_ints = array();
4617
		foreach ($words as $word)
4618
		{
4619
			if (($word = trim($word, '-_\'')) !== '')
4620
			{
4621
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
4622
				$total = 0;
4623
				for ($i = 0; $i < $max_chars; $i++)
4624
					$total += $possible_chars[ord($encrypted[$i])] * pow(63, $i);
4625
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
4626
			}
4627
		}
4628
		return array_unique($returned_ints);
4629
	}
4630
	else
4631
	{
4632
		// Trim characters before and after and add slashes for database insertion.
4633
		$returned_words = array();
4634
		foreach ($words as $word)
4635
			if (($word = trim($word, '-_\'')) !== '')
4636
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
4637
4638
		// Filter out all words that occur more than once.
4639
		return array_unique($returned_words);
4640
	}
4641
}
4642
4643
/**
4644
 * Creates an image/text button
4645
 *
4646
 * @deprecated since 2.1
4647
 * @param string $name The name of the button (should be a main_icons class or the name of an image)
4648
 * @param string $alt The alt text
4649
 * @param string $label The $txt string to use as the label
4650
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
4651
 * @param boolean $force_use Whether to force use of this when template_create_button is available
4652
 * @return string The HTML to display the button
4653
 */
4654
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
4655
{
4656
	global $settings, $txt;
4657
4658
	// Does the current loaded theme have this and we are not forcing the usage of this function?
4659
	if (function_exists('template_create_button') && !$force_use)
4660
		return template_create_button($name, $alt, $label = '', $custom = '');
4661
4662
	if (!$settings['use_image_buttons'])
4663
		return $txt[$alt];
4664
	elseif (!empty($settings['use_buttons']))
4665
		return '<span class="main_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
4666
	else
4667
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
4668
}
4669
4670
/**
4671
 * Sets up all of the top menu buttons
4672
 * Saves them in the cache if it is available and on
4673
 * Places the results in $context
4674
 */
4675
function setupMenuContext()
4676
{
4677
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc, $cache_enable;
4678
4679
	// Set up the menu privileges.
4680
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
4681
	$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'));
4682
4683
	$context['allow_memberlist'] = allowedTo('view_mlist');
4684
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
4685
	$context['allow_moderation_center'] = $context['user']['can_mod'];
4686
	$context['allow_pm'] = allowedTo('pm_read');
4687
4688
	$cacheTime = $modSettings['lastActive'] * 60;
4689
4690
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
4691
	if (!isset($context['allow_calendar_event']))
4692
	{
4693
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
4694
4695
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
4696
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
4697
		{
4698
			$boards_can_post = boardsAllowedTo('post_new');
4699
			$context['allow_calendar_event'] &= !empty($boards_can_post);
4700
		}
4701
	}
4702
4703
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
4704
	if (!$context['user']['is_guest'])
4705
	{
4706
		addInlineJavaScript('
4707
	var user_menus = new smc_PopupMenu();
4708
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
4709
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true);
4710
		if ($context['allow_pm'])
4711
			addInlineJavaScript('
4712
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
4713
4714
		if (!empty($modSettings['enable_ajax_alerts']))
4715
		{
4716
			require_once($sourcedir . '/Subs-Notify.php');
4717
4718
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
4719
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
4720
4721
			addInlineJavaScript('
4722
	var new_alert_title = "' . $context['forum_name'] . '";
4723
	var alert_timeout = ' . $timeout . ';');
4724
			loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts');
4725
		}
4726
	}
4727
4728
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
4729
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
4730
	{
4731
		$buttons = array(
4732
			'home' => array(
4733
				'title' => $txt['home'],
4734
				'href' => $scripturl,
4735
				'show' => true,
4736
				'sub_buttons' => array(
4737
				),
4738
				'is_last' => $context['right_to_left'],
4739
			),
4740
			'search' => array(
4741
				'title' => $txt['search'],
4742
				'href' => $scripturl . '?action=search',
4743
				'show' => $context['allow_search'],
4744
				'sub_buttons' => array(
4745
				),
4746
			),
4747
			'admin' => array(
4748
				'title' => $txt['admin'],
4749
				'href' => $scripturl . '?action=admin',
4750
				'show' => $context['allow_admin'],
4751
				'sub_buttons' => array(
4752
					'featuresettings' => array(
4753
						'title' => $txt['modSettings_title'],
4754
						'href' => $scripturl . '?action=admin;area=featuresettings',
4755
						'show' => allowedTo('admin_forum'),
4756
					),
4757
					'packages' => array(
4758
						'title' => $txt['package'],
4759
						'href' => $scripturl . '?action=admin;area=packages',
4760
						'show' => allowedTo('admin_forum'),
4761
					),
4762
					'errorlog' => array(
4763
						'title' => $txt['errorlog'],
4764
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
4765
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
4766
					),
4767
					'permissions' => array(
4768
						'title' => $txt['edit_permissions'],
4769
						'href' => $scripturl . '?action=admin;area=permissions',
4770
						'show' => allowedTo('manage_permissions'),
4771
					),
4772
					'memberapprove' => array(
4773
						'title' => $txt['approve_members_waiting'],
4774
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
4775
						'show' => !empty($context['unapproved_members']),
4776
						'is_last' => true,
4777
					),
4778
				),
4779
			),
4780
			'moderate' => array(
4781
				'title' => $txt['moderate'],
4782
				'href' => $scripturl . '?action=moderate',
4783
				'show' => $context['allow_moderation_center'],
4784
				'sub_buttons' => array(
4785
					'modlog' => array(
4786
						'title' => $txt['modlog_view'],
4787
						'href' => $scripturl . '?action=moderate;area=modlog',
4788
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4789
					),
4790
					'poststopics' => array(
4791
						'title' => $txt['mc_unapproved_poststopics'],
4792
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
4793
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4794
					),
4795
					'attachments' => array(
4796
						'title' => $txt['mc_unapproved_attachments'],
4797
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
4798
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
4799
					),
4800
					'reports' => array(
4801
						'title' => $txt['mc_reported_posts'],
4802
						'href' => $scripturl . '?action=moderate;area=reportedposts',
4803
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
4804
					),
4805
					'reported_members' => array(
4806
						'title' => $txt['mc_reported_members'],
4807
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
4808
						'show' => allowedTo('moderate_forum'),
4809
						'is_last' => true,
4810
					)
4811
				),
4812
			),
4813
			'calendar' => array(
4814
				'title' => $txt['calendar'],
4815
				'href' => $scripturl . '?action=calendar',
4816
				'show' => $context['allow_calendar'],
4817
				'sub_buttons' => array(
4818
					'view' => array(
4819
						'title' => $txt['calendar_menu'],
4820
						'href' => $scripturl . '?action=calendar',
4821
						'show' => $context['allow_calendar_event'],
4822
					),
4823
					'post' => array(
4824
						'title' => $txt['calendar_post_event'],
4825
						'href' => $scripturl . '?action=calendar;sa=post',
4826
						'show' => $context['allow_calendar_event'],
4827
						'is_last' => true,
4828
					),
4829
				),
4830
			),
4831
			'mlist' => array(
4832
				'title' => $txt['members_title'],
4833
				'href' => $scripturl . '?action=mlist',
4834
				'show' => $context['allow_memberlist'],
4835
				'sub_buttons' => array(
4836
					'mlist_view' => array(
4837
						'title' => $txt['mlist_menu_view'],
4838
						'href' => $scripturl . '?action=mlist',
4839
						'show' => true,
4840
					),
4841
					'mlist_search' => array(
4842
						'title' => $txt['mlist_search'],
4843
						'href' => $scripturl . '?action=mlist;sa=search',
4844
						'show' => true,
4845
						'is_last' => true,
4846
					),
4847
				),
4848
				'is_last' => !$context['right_to_left'] && (!$user_info['is_guest'] || !$context['can_register']),
4849
			),
4850
			'signup' => array(
4851
				'title' => $txt['register'],
4852
				'href' => $scripturl . '?action=signup',
4853
				'show' => $user_info['is_guest'] && $context['can_register'],
4854
				'sub_buttons' => array(
4855
				),
4856
				'is_last' => !$context['right_to_left'],
4857
			),
4858
		);
4859
4860
		// Allow editing menu buttons easily.
4861
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
4862
4863
		// Now we put the buttons in the context so the theme can use them.
4864
		$menu_buttons = array();
4865
		foreach ($buttons as $act => $button)
4866
			if (!empty($button['show']))
4867
			{
4868
				$button['active_button'] = false;
4869
4870
				// This button needs some action.
4871
				if (isset($button['action_hook']))
4872
					$needs_action_hook = true;
4873
4874
				// Make sure the last button truly is the last button.
4875
				if (!empty($button['is_last']))
4876
				{
4877
					if (isset($last_button))
4878
						unset($menu_buttons[$last_button]['is_last']);
4879
					$last_button = $act;
4880
				}
4881
4882
				// Go through the sub buttons if there are any.
4883
				if (!empty($button['sub_buttons']))
4884
					foreach ($button['sub_buttons'] as $key => $subbutton)
4885
					{
4886
						if (empty($subbutton['show']))
4887
							unset($button['sub_buttons'][$key]);
4888
4889
						// 2nd level sub buttons next...
4890
						if (!empty($subbutton['sub_buttons']))
4891
						{
4892
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
4893
							{
4894
								if (empty($sub_button2['show']))
4895
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
4896
							}
4897
						}
4898
					}
4899
4900
				// Does this button have its own icon?
4901
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
4902
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
4903
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
4904
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
4905
				elseif (isset($button['icon']))
4906
					$button['icon'] = '<span class="main_icons ' . $button['icon'] . '"></span>';
4907
				else
4908
					$button['icon'] = '<span class="main_icons ' . $act . '"></span>';
4909
4910
				$menu_buttons[$act] = $button;
4911
			}
4912
4913
		if (!empty($cache_enable) && $cache_enable >= 2)
4914
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
4915
	}
4916
4917
	$context['menu_buttons'] = $menu_buttons;
4918
4919
	// Logging out requires the session id in the url.
4920
	if (isset($context['menu_buttons']['logout']))
4921
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
4922
4923
	// Figure out which action we are doing so we can set the active tab.
4924
	// Default to home.
4925
	$current_action = 'home';
4926
4927
	if (isset($context['menu_buttons'][$context['current_action']]))
4928
		$current_action = $context['current_action'];
4929
	elseif ($context['current_action'] == 'search2')
4930
		$current_action = 'search';
4931
	elseif ($context['current_action'] == 'theme')
4932
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
4933
	elseif ($context['current_action'] == 'register2')
4934
		$current_action = 'register';
4935
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
4936
		$current_action = 'login';
4937
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
4938
		$current_action = 'moderate';
4939
4940
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
4941
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
4942
	{
4943
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
4944
		$context[$current_action] = true;
4945
	}
4946
	elseif ($context['current_action'] == 'pm')
4947
	{
4948
		$current_action = 'self_pm';
4949
		$context['self_pm'] = true;
4950
	}
4951
4952
	$context['total_mod_reports'] = 0;
4953
	$context['total_admin_reports'] = 0;
4954
4955
	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']))
4956
	{
4957
		$context['total_mod_reports'] = $context['open_mod_reports'];
4958
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports'];
4959
	}
4960
4961
	// Show how many errors there are
4962
	if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog']))
4963
	{
4964
		// Get an error count, if necessary
4965
		if (!isset($context['num_errors']))
4966
		{
4967
			$query = $smcFunc['db_query']('', '
4968
				SELECT COUNT(*)
4969
				FROM {db_prefix}log_errors',
4970
				array()
4971
			);
4972
4973
			list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
4974
			$smcFunc['db_free_result']($query);
4975
		}
4976
4977
		if (!empty($context['num_errors']))
4978
		{
4979
			$context['total_admin_reports'] += $context['num_errors'];
4980
			$context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors'];
4981
		}
4982
	}
4983
4984
	// Show number of reported members
4985
	if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members']))
4986
	{
4987
		$context['total_mod_reports'] += $context['open_member_reports'];
4988
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports'];
4989
	}
4990
4991
	if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin']))
4992
	{
4993
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members'];
4994
		$context['total_admin_reports'] += $context['unapproved_members'];
4995
	}
4996
4997
	if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin']))
4998
	{
4999
		$context['menu_buttons']['admin']['amt'] = $context['total_admin_reports'];
5000
	}
5001
5002
	// Do we have any open reports?
5003
	if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate']))
5004
	{
5005
		$context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports'];
5006
	}
5007
5008
	// Not all actions are simple.
5009
	if (!empty($needs_action_hook))
5010
		call_integration_hook('integrate_current_action', array(&$current_action));
5011
5012
	if (isset($context['menu_buttons'][$current_action]))
5013
		$context['menu_buttons'][$current_action]['active_button'] = true;
5014
}
5015
5016
/**
5017
 * Generate a random seed and ensure it's stored in settings.
5018
 */
5019
function smf_seed_generator()
5020
{
5021
	updateSettings(array('rand_seed' => microtime(true)));
5022
}
5023
5024
/**
5025
 * Process functions of an integration hook.
5026
 * calls all functions of the given hook.
5027
 * supports static class method calls.
5028
 *
5029
 * @param string $hook The hook name
5030
 * @param array $parameters An array of parameters this hook implements
5031
 * @return array The results of the functions
5032
 */
5033
function call_integration_hook($hook, $parameters = array())
5034
{
5035
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
5036
	global $context, $txt;
5037
5038
	if ($db_show_debug === true)
5039
		$context['debug']['hooks'][] = $hook;
5040
5041
	// Need to have some control.
5042
	if (!isset($context['instances']))
5043
		$context['instances'] = array();
5044
5045
	$results = array();
5046
	if (empty($modSettings[$hook]))
5047
		return $results;
5048
5049
	$functions = explode(',', $modSettings[$hook]);
5050
	// Loop through each function.
5051
	foreach ($functions as $function)
5052
	{
5053
		// Hook has been marked as "disabled". Skip it!
5054
		if (strpos($function, '!') !== false)
5055
			continue;
5056
5057
		$call = call_helper($function, true);
5058
5059
		// Is it valid?
5060
		if (!empty($call))
5061
			$results[$function] = call_user_func_array($call, $parameters);
5062
		// This failed, but we want to do so silently.
5063
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
5064
			return $results;
5065
		// Whatever it was suppose to call, it failed :(
5066
		elseif (!empty($function))
5067
		{
5068
			loadLanguage('Errors');
5069
5070
			// Get a full path to show on error.
5071
			if (strpos($function, '|') !== false)
5072
			{
5073
				list ($file, $string) = explode('|', $function);
5074
				$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'])));
5075
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
5076
			}
5077
			// "Assume" the file resides on $boarddir somewhere...
5078
			else
5079
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
5080
		}
5081
	}
5082
5083
	return $results;
5084
}
5085
5086
/**
5087
 * Add a function for integration hook.
5088
 * does nothing if the function is already added.
5089
 *
5090
 * @param string $hook The complete hook name.
5091
 * @param string $function The function name. Can be a call to a method via Class::method.
5092
 * @param bool $permanent If true, updates the value in settings table.
5093
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5094
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5095
 */
5096
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5097
{
5098
	global $smcFunc, $modSettings;
5099
5100
	// Any objects?
5101
	if ($object)
5102
		$function = $function . '#';
5103
5104
	// Any files  to load?
5105
	if (!empty($file) && is_string($file))
5106
		$function = $file . (!empty($function) ? '|' . $function : '');
5107
5108
	// Get the correct string.
5109
	$integration_call = $function;
5110
5111
	// Is it going to be permanent?
5112
	if ($permanent)
5113
	{
5114
		$request = $smcFunc['db_query']('', '
5115
			SELECT value
5116
			FROM {db_prefix}settings
5117
			WHERE variable = {string:variable}',
5118
			array(
5119
				'variable' => $hook,
5120
			)
5121
		);
5122
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
5123
		$smcFunc['db_free_result']($request);
5124
5125
		if (!empty($current_functions))
5126
		{
5127
			$current_functions = explode(',', $current_functions);
5128
			if (in_array($integration_call, $current_functions))
5129
				return;
5130
5131
			$permanent_functions = array_merge($current_functions, array($integration_call));
5132
		}
5133
		else
5134
			$permanent_functions = array($integration_call);
5135
5136
		updateSettings(array($hook => implode(',', $permanent_functions)));
5137
	}
5138
5139
	// Make current function list usable.
5140
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5141
5142
	// Do nothing, if it's already there.
5143
	if (in_array($integration_call, $functions))
5144
		return;
5145
5146
	$functions[] = $integration_call;
5147
	$modSettings[$hook] = implode(',', $functions);
5148
}
5149
5150
/**
5151
 * Remove an integration hook function.
5152
 * Removes the given function from the given hook.
5153
 * Does nothing if the function is not available.
5154
 *
5155
 * @param string $hook The complete hook name.
5156
 * @param string $function The function name. Can be a call to a method via Class::method.
5157
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
5158
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5159
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5160
 * @see add_integration_function
5161
 */
5162
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5163
{
5164
	global $smcFunc, $modSettings;
5165
5166
	// Any objects?
5167
	if ($object)
5168
		$function = $function . '#';
5169
5170
	// Any files  to load?
5171
	if (!empty($file) && is_string($file))
5172
		$function = $file . '|' . $function;
5173
5174
	// Get the correct string.
5175
	$integration_call = $function;
5176
5177
	// Get the permanent functions.
5178
	$request = $smcFunc['db_query']('', '
5179
		SELECT value
5180
		FROM {db_prefix}settings
5181
		WHERE variable = {string:variable}',
5182
		array(
5183
			'variable' => $hook,
5184
		)
5185
	);
5186
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
5187
	$smcFunc['db_free_result']($request);
5188
5189
	if (!empty($current_functions))
5190
	{
5191
		$current_functions = explode(',', $current_functions);
5192
5193
		if (in_array($integration_call, $current_functions))
5194
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
5195
	}
5196
5197
	// Turn the function list into something usable.
5198
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5199
5200
	// You can only remove it if it's available.
5201
	if (!in_array($integration_call, $functions))
5202
		return;
5203
5204
	$functions = array_diff($functions, array($integration_call));
5205
	$modSettings[$hook] = implode(',', $functions);
5206
}
5207
5208
/**
5209
 * Receives a string and tries to figure it out if its a method or a function.
5210
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
5211
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
5212
 * Prepare and returns a callable depending on the type of method/function found.
5213
 *
5214
 * @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)
5215
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
5216
 * @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.
5217
 */
5218
function call_helper($string, $return = false)
5219
{
5220
	global $context, $smcFunc, $txt, $db_show_debug;
5221
5222
	// Really?
5223
	if (empty($string))
5224
		return false;
5225
5226
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5227
	// A closure? should be a callable one.
5228
	if (is_array($string) || $string instanceof Closure)
5229
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5230
5231
	// No full objects, sorry! pass a method or a property instead!
5232
	if (is_object($string))
5233
		return false;
5234
5235
	// Stay vitaminized my friends...
5236
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5237
5238
	// Is there a file to load?
5239
	$string = load_file($string);
5240
5241
	// Loaded file failed
5242
	if (empty($string))
5243
		return false;
5244
5245
	// Found a method.
5246
	if (strpos($string, '::') !== false)
5247
	{
5248
		list ($class, $method) = explode('::', $string);
5249
5250
		// Check if a new object will be created.
5251
		if (strpos($method, '#') !== false)
5252
		{
5253
			// Need to remove the # thing.
5254
			$method = str_replace('#', '', $method);
5255
5256
			// Don't need to create a new instance for every method.
5257
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
5258
			{
5259
				$context['instances'][$class] = new $class;
5260
5261
				// Add another one to the list.
5262
				if ($db_show_debug === true)
5263
				{
5264
					if (!isset($context['debug']['instances']))
5265
						$context['debug']['instances'] = array();
5266
5267
					$context['debug']['instances'][$class] = $class;
5268
				}
5269
			}
5270
5271
			$func = array($context['instances'][$class], $method);
5272
		}
5273
5274
		// Right then. This is a call to a static method.
5275
		else
5276
			$func = array($class, $method);
5277
	}
5278
5279
	// Nope! just a plain regular function.
5280
	else
5281
		$func = $string;
5282
5283
	// We can't call this helper, but we want to silently ignore this.
5284
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
5285
		return false;
5286
	// Right, we got what we need, time to do some checks.
5287
	elseif (!is_callable($func, false, $callable_name))
5288
	{
5289
		loadLanguage('Errors');
5290
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
5291
5292
		// Gotta tell everybody.
5293
		return false;
5294
	}
5295
5296
	// Everything went better than expected.
5297
	else
5298
	{
5299
		// What are we gonna do about it?
5300
		if ($return)
5301
			return $func;
5302
5303
		// If this is a plain function, avoid the heat of calling call_user_func().
5304
		else
5305
		{
5306
			if (is_array($func))
5307
				call_user_func($func);
5308
5309
			else
5310
				$func();
5311
		}
5312
	}
5313
}
5314
5315
/**
5316
 * Receives a string and tries to figure it out if it contains info to load a file.
5317
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
5318
 * 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.
5319
 *
5320
 * @param string $string The string containing a valid format.
5321
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
5322
 */
5323
function load_file($string)
5324
{
5325
	global $sourcedir, $txt, $boarddir, $settings, $context;
5326
5327
	if (empty($string))
5328
		return false;
5329
5330
	if (strpos($string, '|') !== false)
5331
	{
5332
		list ($file, $string) = explode('|', $string);
5333
5334
		// Match the wildcards to their regular vars.
5335
		if (empty($settings['theme_dir']))
5336
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
5337
5338
		else
5339
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
5340
5341
		// Load the file if it can be loaded.
5342
		if (file_exists($absPath))
5343
			require_once($absPath);
5344
5345
		// No? try a fallback to $sourcedir
5346
		else
5347
		{
5348
			$absPath = $sourcedir . '/' . $file;
5349
5350
			if (file_exists($absPath))
5351
				require_once($absPath);
5352
5353
			// Sorry, can't do much for you at this point.
5354
			elseif (empty($context['uninstalling']))
5355
			{
5356
				loadLanguage('Errors');
5357
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
5358
5359
				// File couldn't be loaded.
5360
				return false;
5361
			}
5362
		}
5363
	}
5364
5365
	return $string;
5366
}
5367
5368
/**
5369
 * Get the contents of a URL, irrespective of allow_url_fopen.
5370
 *
5371
 * - reads the contents of an http or ftp address and returns the page in a string
5372
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
5373
 * - if post_data is supplied, the value and length is posted to the given url as form data
5374
 * - URL must be supplied in lowercase
5375
 *
5376
 * @param string $url The URL
5377
 * @param string $post_data The data to post to the given URL
5378
 * @param bool $keep_alive Whether to send keepalive info
5379
 * @param int $redirection_level How many levels of redirection
5380
 * @return string|false The fetched data or false on failure
5381
 */
5382
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
5383
{
5384
	global $webmaster_email, $sourcedir;
5385
	static $keep_alive_dom = null, $keep_alive_fp = null;
5386
5387
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
5388
5389
	// No scheme? No data for you!
5390
	if (empty($match[1]))
5391
		return false;
5392
5393
	// An FTP url. We should try connecting and RETRieving it...
5394
	elseif ($match[1] == 'ftp')
5395
	{
5396
		// Include the file containing the ftp_connection class.
5397
		require_once($sourcedir . '/Class-Package.php');
5398
5399
		// Establish a connection and attempt to enable passive mode.
5400
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
5401
		if ($ftp->error !== false || !$ftp->passive())
5402
			return false;
5403
5404
		// I want that one *points*!
5405
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
5406
5407
		// Since passive mode worked (or we would have returned already!) open the connection.
5408
		$fp = @fsockopen($ftp->pasv['ip'], $ftp->pasv['port'], $err, $err, 5);
5409
		if (!$fp)
5410
			return false;
5411
5412
		// The server should now say something in acknowledgement.
5413
		$ftp->check_response(150);
5414
5415
		$data = '';
5416
		while (!feof($fp))
5417
			$data .= fread($fp, 4096);
5418
		fclose($fp);
5419
5420
		// All done, right?  Good.
5421
		$ftp->check_response(226);
5422
		$ftp->close();
5423
	}
5424
5425
	// This is more likely; a standard HTTP URL.
5426
	elseif (isset($match[1]) && $match[1] == 'http')
5427
	{
5428
		// First try to use fsockopen, because it is fastest.
5429
		if ($keep_alive && $match[3] == $keep_alive_dom)
5430
			$fp = $keep_alive_fp;
5431
		if (empty($fp))
5432
		{
5433
			// Open the socket on the port we want...
5434
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
5435
		}
5436
		if (!empty($fp))
5437
		{
5438
			if ($keep_alive)
5439
			{
5440
				$keep_alive_dom = $match[3];
5441
				$keep_alive_fp = $fp;
5442
			}
5443
5444
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
5445
			if (empty($post_data))
5446
			{
5447
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
5448
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5449
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5450
				if ($keep_alive)
5451
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
5452
				else
5453
					fwrite($fp, 'connection: close' . "\r\n\r\n");
5454
			}
5455
			else
5456
			{
5457
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
5458
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5459
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5460
				if ($keep_alive)
5461
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
5462
				else
5463
					fwrite($fp, 'connection: close' . "\r\n");
5464
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
5465
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
5466
				fwrite($fp, $post_data);
5467
			}
5468
5469
			$response = fgets($fp, 768);
5470
5471
			// Redirect in case this location is permanently or temporarily moved.
5472
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
5473
			{
5474
				$header = '';
5475
				$location = '';
5476
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5477
					if (stripos($header, 'location:') !== false)
5478
						$location = trim(substr($header, strpos($header, ':') + 1));
5479
5480
				if (empty($location))
5481
					return false;
5482
				else
5483
				{
5484
					if (!$keep_alive)
5485
						fclose($fp);
5486
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
5487
				}
5488
			}
5489
5490
			// Make sure we get a 200 OK.
5491
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
5492
				return false;
5493
5494
			// Skip the headers...
5495
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5496
			{
5497
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
5498
					$content_length = $match[1];
5499
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
5500
				{
5501
					$keep_alive_dom = null;
5502
					$keep_alive = false;
5503
				}
5504
5505
				continue;
5506
			}
5507
5508
			$data = '';
5509
			if (isset($content_length))
5510
			{
5511
				while (!feof($fp) && strlen($data) < $content_length)
5512
					$data .= fread($fp, $content_length - strlen($data));
5513
			}
5514
			else
5515
			{
5516
				while (!feof($fp))
5517
					$data .= fread($fp, 4096);
5518
			}
5519
5520
			if (!$keep_alive)
5521
				fclose($fp);
5522
		}
5523
5524
		// If using fsockopen didn't work, try to use cURL if available.
5525
		elseif (function_exists('curl_init'))
5526
		{
5527
			// Include the file containing the curl_fetch_web_data class.
5528
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
5529
5530
			$fetch_data = new curl_fetch_web_data();
5531
			$fetch_data->get_url_data($url, $post_data);
5532
5533
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5534
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5535
				$data = $fetch_data->result('body');
5536
			else
5537
				return false;
5538
		}
5539
5540
		// Neither fsockopen nor curl are available. Well, phooey.
5541
		else
5542
			return false;
5543
	}
5544
	else
5545
	{
5546
		// Umm, this shouldn't happen?
5547
		trigger_error('fetch_web_data(): Bad URL', E_USER_NOTICE);
5548
		$data = false;
5549
	}
5550
5551
	return $data;
5552
}
5553
5554
/**
5555
 * Attempts to determine the MIME type of some data or a file.
5556
 *
5557
 * @param string $data The data to check, or the path or URL of a file to check.
5558
 * @param string $is_path If true, $data is a path or URL to a file.
5559
 * @return string|bool A MIME type, or false if we cannot determine it.
5560
 */
5561
function get_mime_type($data, $is_path = false)
5562
{
5563
	global $cachedir;
5564
5565
	$finfo_loaded = extension_loaded('fileinfo');
5566
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
5567
5568
	// Oh well. We tried.
5569
	if (!$finfo_loaded && !$exif_loaded)
5570
		return false;
5571
5572
	// Start with the 'empty' MIME type.
5573
	$mime_type = 'application/x-empty';
5574
5575
	if ($finfo_loaded)
5576
	{
5577
		// Just some nice, simple data to analyze.
5578
		if (empty($is_path))
5579
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5580
5581
		// A file, or maybe a URL?
5582
		else
5583
		{
5584
			// Local file.
5585
			if (file_exists($data))
5586
				$mime_type = mime_content_type($data);
5587
5588
			// URL.
5589
			elseif ($data = fetch_web_data($data))
5590
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5591
		}
5592
	}
5593
	// Workaround using Exif requires a local file.
5594
	else
5595
	{
5596
		// If $data is a URL to fetch, do so.
5597
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
5598
		{
5599
			$data = fetch_web_data($data);
5600
			$is_path = false;
5601
		}
5602
5603
		// If we don't have a local file, create one and use it.
5604
		if (empty($is_path))
5605
		{
5606
			$temp_file = tempnam($cachedir, md5($data));
5607
			file_put_contents($temp_file, $data);
5608
			$is_path = true;
5609
			$data = $temp_file;
5610
		}
5611
5612
		$imagetype = @exif_imagetype($data);
5613
5614
		if (isset($temp_file))
5615
			unlink($temp_file);
5616
5617
		// Unfortunately, this workaround only works for image files.
5618
		if ($imagetype !== false)
5619
			$mime_type = image_type_to_mime_type($imagetype);
5620
	}
5621
5622
	return $mime_type;
5623
}
5624
5625
/**
5626
 * Checks whether a file or data has the expected MIME type.
5627
 *
5628
 * @param string $data The data to check, or the path or URL of a file to check.
5629
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
5630
 * @param string $is_path If true, $data is a path or URL to a file.
5631
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
5632
 */
5633
function check_mime_type($data, $type_pattern, $is_path = false)
5634
{
5635
	// Get the MIME type.
5636
	$mime_type = get_mime_type($data, $is_path);
5637
5638
	// Couldn't determine it.
5639
	if ($mime_type === false)
5640
		return 2;
5641
5642
	// Check whether the MIME type matches expectations.
5643
	return (int) @preg_match('~' . $type_pattern . '~', $mime_type);
5644
}
5645
5646
/**
5647
 * Prepares an array of "likes" info for the topic specified by $topic
5648
 *
5649
 * @param integer $topic The topic ID to fetch the info from.
5650
 * @return array An array of IDs of messages in the specified topic that the current user likes
5651
 */
5652
function prepareLikesContext($topic)
5653
{
5654
	global $user_info, $smcFunc;
5655
5656
	// Make sure we have something to work with.
5657
	if (empty($topic))
5658
		return array();
5659
5660
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
5661
	$user = $user_info['id'];
5662
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
5663
	$ttl = 180;
5664
5665
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
5666
	{
5667
		$temp = array();
5668
		$request = $smcFunc['db_query']('', '
5669
			SELECT content_id
5670
			FROM {db_prefix}user_likes AS l
5671
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
5672
			WHERE l.id_member = {int:current_user}
5673
				AND l.content_type = {literal:msg}
5674
				AND m.id_topic = {int:topic}',
5675
			array(
5676
				'current_user' => $user,
5677
				'topic' => $topic,
5678
			)
5679
		);
5680
		while ($row = $smcFunc['db_fetch_assoc']($request))
5681
			$temp[] = (int) $row['content_id'];
5682
5683
		cache_put_data($cache_key, $temp, $ttl);
5684
	}
5685
5686
	return $temp;
5687
}
5688
5689
/**
5690
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
5691
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
5692
 * that are not normally displayable.  This converts the popular ones that
5693
 * appear from a cut and paste from windows.
5694
 *
5695
 * @param string $string The string
5696
 * @return string The sanitized string
5697
 */
5698
function sanitizeMSCutPaste($string)
5699
{
5700
	global $context;
5701
5702
	if (empty($string))
5703
		return $string;
5704
5705
	// UTF-8 occurences of MS special characters
5706
	$findchars_utf8 = array(
5707
		"\xe2\x80\x9a",	// single low-9 quotation mark
5708
		"\xe2\x80\x9e",	// double low-9 quotation mark
5709
		"\xe2\x80\xa6",	// horizontal ellipsis
5710
		"\xe2\x80\x98",	// left single curly quote
5711
		"\xe2\x80\x99",	// right single curly quote
5712
		"\xe2\x80\x9c",	// left double curly quote
5713
		"\xe2\x80\x9d",	// right double curly quote
5714
	);
5715
5716
	// windows 1252 / iso equivalents
5717
	$findchars_iso = array(
5718
		chr(130),
5719
		chr(132),
5720
		chr(133),
5721
		chr(145),
5722
		chr(146),
5723
		chr(147),
5724
		chr(148),
5725
	);
5726
5727
	// safe replacements
5728
	$replacechars = array(
5729
		',',	// &sbquo;
5730
		',,',	// &bdquo;
5731
		'...',	// &hellip;
5732
		"'",	// &lsquo;
5733
		"'",	// &rsquo;
5734
		'"',	// &ldquo;
5735
		'"',	// &rdquo;
5736
	);
5737
5738
	if ($context['utf8'])
5739
		$string = str_replace($findchars_utf8, $replacechars, $string);
5740
	else
5741
		$string = str_replace($findchars_iso, $replacechars, $string);
5742
5743
	return $string;
5744
}
5745
5746
/**
5747
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
5748
 *
5749
 * Callback function for preg_replace_callback in subs-members
5750
 * Uses capture group 2 in the supplied array
5751
 * Does basic scan to ensure characters are inside a valid range
5752
 *
5753
 * @param array $matches An array of matches (relevant info should be the 3rd item)
5754
 * @return string A fixed string
5755
 */
5756
function replaceEntities__callback($matches)
5757
{
5758
	global $context;
5759
5760
	if (!isset($matches[2]))
5761
		return '';
5762
5763
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
5764
5765
	// remove left to right / right to left overrides
5766
	if ($num === 0x202D || $num === 0x202E)
5767
		return '';
5768
5769
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
5770
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
5771
		return '&#' . $num . ';';
5772
5773
	if (empty($context['utf8']))
5774
	{
5775
		// no control characters
5776
		if ($num < 0x20)
5777
			return '';
5778
		// text is text
5779
		elseif ($num < 0x80)
5780
			return chr($num);
5781
		// all others get html-ised
5782
		else
5783
			return '&#' . $matches[2] . ';';
5784
	}
5785
	else
5786
	{
5787
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
5788
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
5789
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
5790
			return '';
5791
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5792
		elseif ($num < 0x80)
5793
			return chr($num);
5794
		// <0x800 (2048)
5795
		elseif ($num < 0x800)
5796
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5797
		// < 0x10000 (65536)
5798
		elseif ($num < 0x10000)
5799
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5800
		// <= 0x10FFFF (1114111)
5801
		else
5802
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5803
	}
5804
}
5805
5806
/**
5807
 * Converts html entities to utf8 equivalents
5808
 *
5809
 * Callback function for preg_replace_callback
5810
 * Uses capture group 1 in the supplied array
5811
 * Does basic checks to keep characters inside a viewable range.
5812
 *
5813
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
5814
 * @return string The fixed string
5815
 */
5816
function fixchar__callback($matches)
5817
{
5818
	if (!isset($matches[1]))
5819
		return '';
5820
5821
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
5822
5823
	// <0x20 are control characters, > 0x10FFFF is past the end of the utf8 character set
5824
	// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text), 0x202D-E are left to right overrides
5825
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num === 0x202D || $num === 0x202E)
5826
		return '';
5827
	// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
5828
	elseif ($num < 0x80)
5829
		return chr($num);
5830
	// <0x800 (2048)
5831
	elseif ($num < 0x800)
5832
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
5833
	// < 0x10000 (65536)
5834
	elseif ($num < 0x10000)
5835
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5836
	// <= 0x10FFFF (1114111)
5837
	else
5838
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
5839
}
5840
5841
/**
5842
 * Strips out invalid html entities, replaces others with html style &#123; codes
5843
 *
5844
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
5845
 * strpos, strlen, substr etc
5846
 *
5847
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
5848
 * @return string The fixed string
5849
 */
5850
function entity_fix__callback($matches)
5851
{
5852
	if (!isset($matches[2]))
5853
		return '';
5854
5855
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
5856
5857
	// we don't allow control characters, characters out of range, byte markers, etc
5858
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
5859
		return '';
5860
	else
5861
		return '&#' . $num . ';';
5862
}
5863
5864
/**
5865
 * Return a Gravatar URL based on
5866
 * - the supplied email address,
5867
 * - the global maximum rating,
5868
 * - the global default fallback,
5869
 * - maximum sizes as set in the admin panel.
5870
 *
5871
 * It is SSL aware, and caches most of the parameters.
5872
 *
5873
 * @param string $email_address The user's email address
5874
 * @return string The gravatar URL
5875
 */
5876
function get_gravatar_url($email_address)
5877
{
5878
	global $modSettings, $smcFunc;
5879
	static $url_params = null;
5880
5881
	if ($url_params === null)
5882
	{
5883
		$ratings = array('G', 'PG', 'R', 'X');
5884
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
5885
		$url_params = array();
5886
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
5887
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
5888
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
5889
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
5890
		if (!empty($modSettings['avatar_max_width_external']))
5891
			$size_string = (int) $modSettings['avatar_max_width_external'];
5892
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
5893
			if ((int) $modSettings['avatar_max_height_external'] < $size_string)
5894
				$size_string = $modSettings['avatar_max_height_external'];
5895
5896
		if (!empty($size_string))
5897
			$url_params[] = 's=' . $size_string;
5898
	}
5899
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
5900
5901
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
5902
}
5903
5904
/**
5905
 * Get a list of timezones.
5906
 *
5907
 * @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'.
5908
 * @return array An array of timezone info.
5909
 */
5910
function smf_list_timezones($when = 'now')
5911
{
5912
	global $smcFunc, $modSettings, $tztxt, $txt, $cur_profile;
5913
	static $timezones = null, $lastwhen = null;
5914
5915
	// No point doing this over if we already did it once
5916
	if (!empty($timezones) && $when == $lastwhen)
5917
		return $timezones;
5918
	else
5919
		$lastwhen = $when;
5920
5921
	// Parseable datetime string?
5922
	if (is_int($timestamp = strtotime($when)))
5923
		$when = $timestamp;
5924
5925
	// A Unix timestamp?
5926
	elseif (is_numeric($when))
5927
		$when = intval($when);
5928
5929
	// Invalid value? Just get current Unix timestamp.
5930
	else
5931
		$when = time();
5932
5933
	// We'll need these too
5934
	$date_when = date_create('@' . $when);
5935
	$later = strtotime('@' . $when . ' + 1 year');
5936
5937
	// Load up any custom time zone descriptions we might have
5938
	loadLanguage('Timezones');
5939
5940
	// Should we put time zones from certain countries at the top of the list?
5941
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
5942
	$priority_tzids = array();
5943
	foreach ($priority_countries as $country)
5944
	{
5945
		$country_tzids = @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, strtoupper(trim($country)));
5946
		if (!empty($country_tzids))
5947
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
5948
	}
5949
5950
	// Antarctic research stations should be listed last, unless you're running a penguin forum
5951
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
5952
5953
	// Process the preferred timezones first, then the normal ones, then the low priority ones.
5954
	$tzids = array_merge(array_keys($tztxt), array_diff(timezone_identifiers_list(), array_keys($tztxt), $low_priority_tzids), $low_priority_tzids);
5955
5956
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
5957
	foreach ($tzids as $tzid)
5958
	{
5959
		// We don't want UTC right now
5960
		if ($tzid == 'UTC')
5961
			continue;
5962
5963
		$tz = timezone_open($tzid);
5964
5965
		// First, get the set of transition rules for this tzid
5966
		$tzinfo = timezone_transitions_get($tz, $when, $later);
5967
5968
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
5969
		$tzkey = serialize($tzinfo);
5970
5971
		// Next, get the geographic info for this tzid
5972
		$tzgeo = timezone_location_get($tz);
5973
5974
		// Don't overwrite our preferred tzids
5975
		if (empty($zones[$tzkey]['tzid']))
5976
		{
5977
			$zones[$tzkey]['tzid'] = $tzid;
5978
			$zones[$tzkey]['abbr'] = $tzinfo[0]['abbr'];
5979
		}
5980
5981
		// A time zone from a prioritized country?
5982
		if (in_array($tzid, $priority_tzids))
5983
			$priority_zones[$tzkey] = true;
5984
5985
		// Keep track of the location and offset for this tzid
5986
		if (!empty($txt[$tzid]))
5987
			$zones[$tzkey]['locations'][] = $txt[$tzid];
5988
		else
5989
		{
5990
			$tzid_parts = explode('/', $tzid);
5991
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
5992
		}
5993
		$offsets[$tzkey] = $tzinfo[0]['offset'];
5994
		$longitudes[$tzkey] = empty($longitudes[$tzkey]) ? $tzgeo['longitude'] : $longitudes[$tzkey];
5995
5996
		// Remember this for later
5997
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
5998
			$member_tzkey = $tzkey;
5999
	}
6000
6001
	// Sort by offset then longitude
6002
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $longitudes, SORT_ASC, SORT_NUMERIC, $zones);
6003
6004
	// Build the final array of formatted values
6005
	$priority_timezones = array();
6006
	$timezones = array();
6007
	foreach ($zones as $tzkey => $tzvalue)
6008
	{
6009
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
6010
6011
		// Use the custom description, if there is one
6012
		if (!empty($tztxt[$tzvalue['tzid']]))
6013
			$desc = $tztxt[$tzvalue['tzid']];
6014
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6015
		else
6016
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6017
6018
		// Show the UTC offset and the abbreviation, if it's something like 'MST' and not '-06'
6019
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . (!strspn($tzvalue['abbr'], '+-') ? $tzvalue['abbr'] . ' - ' : '') . $desc;
6020
6021
		if (isset($priority_zones[$tzkey]))
6022
			$priority_timezones[$tzvalue['tzid']] = $desc;
6023
		else
6024
			$timezones[$tzvalue['tzid']] = $desc;
6025
6026
		// Automatically fix orphaned timezones on the member profile page
6027
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6028
			$cur_profile['timezone'] = $tzvalue['tzid'];
6029
	}
6030
6031
	if (!empty($priority_timezones))
6032
		$priority_timezones[] = '-----';
6033
6034
	$timezones = array_merge(
6035
		$priority_timezones,
6036
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6037
		$timezones
6038
	);
6039
6040
	return $timezones;
6041
}
6042
6043
/**
6044
 * Converts an IP address into binary
6045
 *
6046
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
6047
 * @return string|false The IP address in binary or false
6048
 */
6049
function inet_ptod($ip_address)
6050
{
6051
	if (!isValidIP($ip_address))
6052
		return $ip_address;
6053
6054
	$bin = inet_pton($ip_address);
6055
	return $bin;
6056
}
6057
6058
/**
6059
 * Converts a binary version of an IP address into a readable format
6060
 *
6061
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
6062
 * @return string|false The IP address in presentation format or false on error
6063
 */
6064
function inet_dtop($bin)
6065
{
6066
	global $db_type;
6067
6068
	if (empty($bin))
6069
		return '';
6070
	elseif ($db_type == 'postgresql')
6071
		return $bin;
6072
	// Already a String?
6073
	elseif (isValidIP($bin))
6074
		return $bin;
6075
	return inet_ntop($bin);
6076
}
6077
6078
/**
6079
 * Safe serialize() and unserialize() replacements
6080
 *
6081
 * @license Public Domain
6082
 *
6083
 * @author anthon (dot) pang (at) gmail (dot) com
6084
 */
6085
6086
/**
6087
 * Safe serialize() replacement. Recursive
6088
 * - output a strict subset of PHP's native serialized representation
6089
 * - does not serialize objects
6090
 *
6091
 * @param mixed $value
6092
 * @return string
6093
 */
6094
function _safe_serialize($value)
6095
{
6096
	if (is_null($value))
6097
		return 'N;';
6098
6099
	if (is_bool($value))
6100
		return 'b:' . (int) $value . ';';
6101
6102
	if (is_int($value))
6103
		return 'i:' . $value . ';';
6104
6105
	if (is_float($value))
6106
		return 'd:' . str_replace(',', '.', $value) . ';';
6107
6108
	if (is_string($value))
6109
		return 's:' . strlen($value) . ':"' . $value . '";';
6110
6111
	if (is_array($value))
6112
	{
6113
		$out = '';
6114
		foreach ($value as $k => $v)
6115
			$out .= _safe_serialize($k) . _safe_serialize($v);
6116
6117
		return 'a:' . count($value) . ':{' . $out . '}';
6118
	}
6119
6120
	// safe_serialize cannot serialize resources or objects.
6121
	return false;
6122
}
6123
6124
/**
6125
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
6126
 *
6127
 * @param mixed $value
6128
 * @return string
6129
 */
6130
function safe_serialize($value)
6131
{
6132
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6133
	if (function_exists('mb_internal_encoding') &&
6134
		(((int) ini_get('mbstring.func_overload')) & 2))
6135
	{
6136
		$mbIntEnc = mb_internal_encoding();
6137
		mb_internal_encoding('ASCII');
6138
	}
6139
6140
	$out = _safe_serialize($value);
6141
6142
	if (isset($mbIntEnc))
6143
		mb_internal_encoding($mbIntEnc);
6144
6145
	return $out;
6146
}
6147
6148
/**
6149
 * Safe unserialize() replacement
6150
 * - accepts a strict subset of PHP's native serialized representation
6151
 * - does not unserialize objects
6152
 *
6153
 * @param string $str
6154
 * @return mixed
6155
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
6156
 */
6157
function _safe_unserialize($str)
6158
{
6159
	// Input  is not a string.
6160
	if (empty($str) || !is_string($str))
6161
		return false;
6162
6163
	$stack = array();
6164
	$expected = array();
6165
6166
	/*
6167
	 * states:
6168
	 *   0 - initial state, expecting a single value or array
6169
	 *   1 - terminal state
6170
	 *   2 - in array, expecting end of array or a key
6171
	 *   3 - in array, expecting value or another array
6172
	 */
6173
	$state = 0;
6174
	while ($state != 1)
6175
	{
6176
		$type = isset($str[0]) ? $str[0] : '';
6177
		if ($type == '}')
6178
			$str = substr($str, 1);
6179
6180
		elseif ($type == 'N' && $str[1] == ';')
6181
		{
6182
			$value = null;
6183
			$str = substr($str, 2);
6184
		}
6185
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
6186
		{
6187
			$value = $matches[1] == '1' ? true : false;
6188
			$str = substr($str, 4);
6189
		}
6190
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
6191
		{
6192
			$value = (int) $matches[1];
6193
			$str = $matches[2];
6194
		}
6195
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
6196
		{
6197
			$value = (float) $matches[1];
6198
			$str = $matches[3];
6199
		}
6200
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
6201
		{
6202
			$value = substr($matches[2], 0, (int) $matches[1]);
6203
			$str = substr($matches[2], (int) $matches[1] + 2);
6204
		}
6205
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
6206
		{
6207
			$expectedLength = (int) $matches[1];
6208
			$str = $matches[2];
6209
		}
6210
6211
		// Object or unknown/malformed type.
6212
		else
6213
			return false;
6214
6215
		switch ($state)
6216
		{
6217
			case 3: // In array, expecting value or another array.
6218
				if ($type == 'a')
6219
				{
6220
					$stack[] = &$list;
6221
					$list[$key] = array();
6222
					$list = &$list[$key];
6223
					$expected[] = $expectedLength;
6224
					$state = 2;
6225
					break;
6226
				}
6227
				if ($type != '}')
6228
				{
6229
					$list[$key] = $value;
6230
					$state = 2;
6231
					break;
6232
				}
6233
6234
				// Missing array value.
6235
				return false;
6236
6237
			case 2: // in array, expecting end of array or a key
6238
				if ($type == '}')
6239
				{
6240
					// Array size is less than expected.
6241
					if (count($list) < end($expected))
6242
						return false;
6243
6244
					unset($list);
6245
					$list = &$stack[count($stack) - 1];
6246
					array_pop($stack);
6247
6248
					// Go to terminal state if we're at the end of the root array.
6249
					array_pop($expected);
6250
6251
					if (count($expected) == 0)
6252
						$state = 1;
6253
6254
					break;
6255
				}
6256
6257
				if ($type == 'i' || $type == 's')
6258
				{
6259
					// Array size exceeds expected length.
6260
					if (count($list) >= end($expected))
6261
						return false;
6262
6263
					$key = $value;
6264
					$state = 3;
6265
					break;
6266
				}
6267
6268
				// Illegal array index type.
6269
				return false;
6270
6271
			// Expecting array or value.
6272
			case 0:
6273
				if ($type == 'a')
6274
				{
6275
					$data = array();
6276
					$list = &$data;
6277
					$expected[] = $expectedLength;
6278
					$state = 2;
6279
					break;
6280
				}
6281
6282
				if ($type != '}')
6283
				{
6284
					$data = $value;
6285
					$state = 1;
6286
					break;
6287
				}
6288
6289
				// Not in array.
6290
				return false;
6291
		}
6292
	}
6293
6294
	// Trailing data in input.
6295
	if (!empty($str))
6296
		return false;
6297
6298
	return $data;
6299
}
6300
6301
/**
6302
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
6303
 *
6304
 * @param string $str
6305
 * @return mixed
6306
 */
6307
function safe_unserialize($str)
6308
{
6309
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6310
	if (function_exists('mb_internal_encoding') &&
6311
		(((int) ini_get('mbstring.func_overload')) & 0x02))
6312
	{
6313
		$mbIntEnc = mb_internal_encoding();
6314
		mb_internal_encoding('ASCII');
6315
	}
6316
6317
	$out = _safe_unserialize($str);
6318
6319
	if (isset($mbIntEnc))
6320
		mb_internal_encoding($mbIntEnc);
6321
6322
	return $out;
6323
}
6324
6325
/**
6326
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
6327
 *
6328
 * @param string $file The file/dir full path.
6329
 * @param int $value Not needed, added for legacy reasons.
6330
 * @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.
6331
 */
6332
function smf_chmod($file, $value = 0)
6333
{
6334
	// No file? no checks!
6335
	if (empty($file))
6336
		return false;
6337
6338
	// Already writable?
6339
	if (is_writable($file))
6340
		return true;
6341
6342
	// Do we have a file or a dir?
6343
	$isDir = is_dir($file);
6344
	$isWritable = false;
6345
6346
	// Set different modes.
6347
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
6348
6349
	foreach ($chmodValues as $val)
6350
	{
6351
		// If it's writable, break out of the loop.
6352
		if (is_writable($file))
6353
		{
6354
			$isWritable = true;
6355
			break;
6356
		}
6357
6358
		else
6359
			@chmod($file, $val);
6360
	}
6361
6362
	return $isWritable;
6363
}
6364
6365
/**
6366
 * Wrapper function for json_decode() with error handling.
6367
 *
6368
 * @param string $json The string to decode.
6369
 * @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.
6370
 * @param bool $logIt To specify if the error will be logged if theres any.
6371
 * @return array Either an empty array or the decoded data as an array.
6372
 */
6373
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
6374
{
6375
	global $txt;
6376
6377
	// Come on...
6378
	if (empty($json) || !is_string($json))
6379
		return array();
6380
6381
	$returnArray = @json_decode($json, $returnAsArray);
6382
6383
	// PHP 5.3 so no json_last_error_msg()
6384
	switch (json_last_error())
6385
	{
6386
		case JSON_ERROR_NONE:
6387
			$jsonError = false;
6388
			break;
6389
		case JSON_ERROR_DEPTH:
6390
			$jsonError = 'JSON_ERROR_DEPTH';
6391
			break;
6392
		case JSON_ERROR_STATE_MISMATCH:
6393
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
6394
			break;
6395
		case JSON_ERROR_CTRL_CHAR:
6396
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
6397
			break;
6398
		case JSON_ERROR_SYNTAX:
6399
			$jsonError = 'JSON_ERROR_SYNTAX';
6400
			break;
6401
		case JSON_ERROR_UTF8:
6402
			$jsonError = 'JSON_ERROR_UTF8';
6403
			break;
6404
		default:
6405
			$jsonError = 'unknown';
6406
			break;
6407
	}
6408
6409
	// Something went wrong!
6410
	if (!empty($jsonError) && $logIt)
6411
	{
6412
		// Being a wrapper means we lost our smf_error_handler() privileges :(
6413
		$jsonDebug = debug_backtrace();
6414
		$jsonDebug = $jsonDebug[0];
6415
		loadLanguage('Errors');
6416
6417
		if (!empty($jsonDebug))
6418
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
6419
6420
		else
6421
			log_error($txt['json_' . $jsonError], 'critical');
6422
6423
		// Everyone expects an array.
6424
		return array();
6425
	}
6426
6427
	return $returnArray;
6428
}
6429
6430
/**
6431
 * Check the given String if he is a valid IPv4 or IPv6
6432
 * return true or false
6433
 *
6434
 * @param string $IPString
6435
 *
6436
 * @return bool
6437
 */
6438
function isValidIP($IPString)
6439
{
6440
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6441
}
6442
6443
/**
6444
 * Outputs a response.
6445
 * It assumes the data is already a string.
6446
 *
6447
 * @param string $data The data to print
6448
 * @param string $type The content type. Defaults to Json.
6449
 * @return void
6450
 */
6451
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6452
{
6453
	global $db_show_debug, $modSettings;
6454
6455
	// Defensive programming anyone?
6456
	if (empty($data))
6457
		return false;
6458
6459
	// Don't need extra stuff...
6460
	$db_show_debug = false;
6461
6462
	// Kill anything else.
6463
	ob_end_clean();
6464
6465
	if (!empty($modSettings['CompressedOutput']))
6466
		@ob_start('ob_gzhandler');
6467
6468
	else
6469
		ob_start();
6470
6471
	// Set the header.
6472
	header($type);
6473
6474
	// Echo!
6475
	echo $data;
6476
6477
	// Done.
6478
	obExit(false);
6479
}
6480
6481
/**
6482
 * Creates an optimized regex to match all known top level domains.
6483
 *
6484
 * The optimized regex is stored in $modSettings['tld_regex'].
6485
 *
6486
 * To update the stored version of the regex to use the latest list of valid
6487
 * TLDs from iana.org, set the $update parameter to true. Updating can take some
6488
 * time, based on network connectivity, so it should normally only be done by
6489
 * calling this function from a background or scheduled task.
6490
 *
6491
 * If $update is not true, but the regex is missing or invalid, the regex will
6492
 * be regenerated from a hard-coded list of TLDs. This regenerated regex will be
6493
 * overwritten on the next scheduled update.
6494
 *
6495
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
6496
 */
6497
function set_tld_regex($update = false)
6498
{
6499
	global $sourcedir, $smcFunc, $modSettings;
6500
	static $done = false;
6501
6502
	// If we don't need to do anything, don't
6503
	if (!$update && $done)
6504
		return;
6505
6506
	// Should we get a new copy of the official list of TLDs?
6507
	if ($update)
6508
	{
6509
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
6510
		$tlds_md5 = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt.md5');
6511
6512
		/**
6513
		 * If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
6514
		 * We're probably running on a server hidden in a bunker deep underground to protect
6515
		 * it from marauding bandits roaming on the surface. We don't want to waste precious
6516
		 * electricity on pointlessly repeating background tasks, so we'll wait until the next
6517
		 * regularly scheduled update to see if civilization has been restored.
6518
		 */
6519
		if ($tlds === false || $tlds_md5 === false)
6520
			$postapocalypticNightmare = true;
6521
6522
		// Make sure nothing went horribly wrong along the way.
6523
		if (md5($tlds) != substr($tlds_md5, 0, 32))
6524
			$tlds = array();
6525
	}
6526
	// If we aren't updating and the regex is valid, we're done
6527
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
6528
	{
6529
		$done = true;
6530
		return;
6531
	}
6532
6533
	// If we successfully got an update, process the list into an array
6534
	if (!empty($tlds))
6535
	{
6536
		// Clean $tlds and convert it to an array
6537
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line)
6538
		{
6539
			$line = trim($line);
6540
			if (empty($line) || strlen($line) != strspn($line, 'abcdefghijklmnopqrstuvwxyz0123456789-'))
6541
				return false;
6542
			else
6543
				return true;
6544
		});
6545
6546
		// Convert Punycode to Unicode
6547
		require_once($sourcedir . '/Class-Punycode.php');
6548
		$Punycode = new Punycode();
6549
		$tlds = array_map(function($input) use ($Punycode)
6550
		{
6551
			return $Punycode->decode($input);
6552
		}, $tlds);
6553
	}
6554
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
6555
	else
6556
	{
6557
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz',
6558
			'cat', 'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post',
6559
			'pro', 'tel', 'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al',
6560
			'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',
6561
			'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
6562
			'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm',
6563
			'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do',
6564
			'dz', 'ec', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo',
6565
			'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp',
6566
			'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',
6567
			'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo',
6568
			'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la',
6569
			'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md',
6570
			'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt',
6571
			'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl',
6572
			'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl',
6573
			'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw',
6574
			'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
6575
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg',
6576
			'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',
6577
			'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
6578
			'ws', 'ye', 'yt', 'za', 'zm', 'zw',
6579
		);
6580
6581
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
6582
		if (empty($postapocalypticNightmare))
6583
		{
6584
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
6585
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
6586
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
6587
			);
6588
		}
6589
	}
6590
6591
	// Tack on some "special use domain names" that aren't in DNS but may possibly resolve.
6592
	// See https://www.iana.org/assignments/special-use-domain-names/ for more info.
6593
	$tlds = array_merge($tlds, array('local', 'onion', 'test'));
6594
6595
	// Get an optimized regex to match all the TLDs
6596
	$tld_regex = build_regex($tlds);
6597
6598
	// Remember the new regex in $modSettings
6599
	updateSettings(array('tld_regex' => $tld_regex));
6600
6601
	// Redundant repetition is redundant
6602
	$done = true;
6603
}
6604
6605
/**
6606
 * Creates optimized regular expressions from an array of strings.
6607
 *
6608
 * An optimized regex built using this function will be much faster than a
6609
 * simple regex built using `implode('|', $strings)` --- anywhere from several
6610
 * times to several orders of magnitude faster.
6611
 *
6612
 * However, the time required to build the optimized regex is approximately
6613
 * equal to the time it takes to execute the simple regex. Therefore, it is only
6614
 * worth calling this function if the resulting regex will be used more than
6615
 * once.
6616
 *
6617
 * Because PHP places an upper limit on the allowed length of a regex, very
6618
 * large arrays of $strings may not fit in a single regex. Normally, the excess
6619
 * strings will simply be dropped. However, if the $returnArray parameter is set
6620
 * to true, this function will build as many regexes as necessary to accommodate
6621
 * everything in $strings and return them in an array. You will need to iterate
6622
 * through all elements of the returned array in order to test all possible
6623
 * matches.
6624
 *
6625
 * @param array $strings An array of strings to make a regex for.
6626
 * @param string $delim An optional delimiter character to pass to preg_quote().
6627
 * @param bool $returnArray If true, returns an array of regexes.
6628
 * @return string|array One or more regular expressions to match any of the input strings.
6629
 */
6630
function build_regex($strings, $delim = null, $returnArray = false)
6631
{
6632
	global $smcFunc;
6633
	static $regexes = array();
6634
6635
	// If it's not an array, there's not much to do. ;)
6636
	if (!is_array($strings))
6637
		return preg_quote(@strval($strings), $delim);
6638
6639
	$regex_key = md5(json_encode(array($strings, $delim, $returnArray)));
6640
6641
	if (isset($regexes[$regex_key]))
6642
		return $regexes[$regex_key];
6643
6644
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
6645
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
6646
	{
6647
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
6648
		{
6649
			$current_encoding = mb_internal_encoding();
6650
			mb_internal_encoding($string_encoding);
6651
		}
6652
6653
		$strlen = 'mb_strlen';
6654
		$substr = 'mb_substr';
6655
	}
6656
	else
6657
	{
6658
		$strlen = $smcFunc['strlen'];
6659
		$substr = $smcFunc['substr'];
6660
	}
6661
6662
	// This recursive function creates the index array from the strings
6663
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
6664
	{
6665
		static $depth = 0;
6666
		$depth++;
6667
6668
		$first = (string) @$substr($string, 0, 1);
6669
6670
		// No first character? That's no good.
6671
		if ($first === '')
6672
		{
6673
			// A nested array? Really? Ugh. Fine.
6674
			if (is_array($string) && $depth < 20)
6675
			{
6676
				foreach ($string as $str)
6677
					$index = $add_string_to_index($str, $index);
6678
			}
6679
6680
			$depth--;
6681
			return $index;
6682
		}
6683
6684
		if (empty($index[$first]))
6685
			$index[$first] = array();
6686
6687
		if ($strlen($string) > 1)
6688
		{
6689
			// Sanity check on recursion
6690
			if ($depth > 99)
6691
				$index[$first][$substr($string, 1)] = '';
6692
6693
			else
6694
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
6695
		}
6696
		else
6697
			$index[$first][''] = '';
6698
6699
		$depth--;
6700
		return $index;
6701
	};
6702
6703
	// This recursive function turns the index array into a regular expression
6704
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
6705
	{
6706
		static $depth = 0;
6707
		$depth++;
6708
6709
		// Absolute max length for a regex is 32768, but we might need wiggle room
6710
		$max_length = 30000;
6711
6712
		$regex = array();
6713
		$length = 0;
6714
6715
		foreach ($index as $key => $value)
6716
		{
6717
			$key_regex = preg_quote($key, $delim);
6718
			$new_key = $key;
6719
6720
			if (empty($value))
6721
				$sub_regex = '';
6722
			else
6723
			{
6724
				$sub_regex = $index_to_regex($value, $delim);
6725
6726
				if (count(array_keys($value)) == 1)
6727
				{
6728
					$new_key_array = explode('(?' . '>', $sub_regex);
6729
					$new_key .= $new_key_array[0];
6730
				}
6731
				else
6732
					$sub_regex = '(?' . '>' . $sub_regex . ')';
6733
			}
6734
6735
			if ($depth > 1)
6736
				$regex[$new_key] = $key_regex . $sub_regex;
6737
			else
6738
			{
6739
				if (($length += strlen($key_regex) + 1) < $max_length || empty($regex))
6740
				{
6741
					$regex[$new_key] = $key_regex . $sub_regex;
6742
					unset($index[$key]);
6743
				}
6744
				else
6745
					break;
6746
			}
6747
		}
6748
6749
		// Sort by key length and then alphabetically
6750
		uksort($regex, function($k1, $k2) use (&$strlen)
6751
		{
6752
			$l1 = $strlen($k1);
6753
			$l2 = $strlen($k2);
6754
6755
			if ($l1 == $l2)
6756
				return strcmp($k1, $k2) > 0 ? 1 : -1;
6757
			else
6758
				return $l1 > $l2 ? -1 : 1;
6759
		});
6760
6761
		$depth--;
6762
		return implode('|', $regex);
6763
	};
6764
6765
	// Now that the functions are defined, let's do this thing
6766
	$index = array();
6767
	$regex = '';
6768
6769
	foreach ($strings as $string)
6770
		$index = $add_string_to_index($string, $index);
6771
6772
	if ($returnArray === true)
6773
	{
6774
		$regex = array();
6775
		while (!empty($index))
6776
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6777
	}
6778
	else
6779
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
6780
6781
	// Restore PHP's internal character encoding to whatever it was originally
6782
	if (!empty($current_encoding))
6783
		mb_internal_encoding($current_encoding);
6784
6785
	$regexes[$regex_key] = $regex;
6786
	return $regex;
6787
}
6788
6789
/**
6790
 * Check if the passed url has an SSL certificate.
6791
 *
6792
 * Returns true if a cert was found & false if not.
6793
 *
6794
 * @param string $url to check, in $boardurl format (no trailing slash).
6795
 */
6796
function ssl_cert_found($url)
6797
{
6798
	// This check won't work without OpenSSL
6799
	if (!extension_loaded('openssl'))
6800
		return true;
6801
6802
	// First, strip the subfolder from the passed url, if any
6803
	$parsedurl = parse_url($url);
6804
	$url = 'ssl://' . $parsedurl['host'] . ':443';
6805
6806
	// Next, check the ssl stream context for certificate info
6807
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
6808
		$ssloptions = array("capture_peer_cert" => true);
6809
	else
6810
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
6811
6812
	$result = false;
6813
	$context = stream_context_create(array("ssl" => $ssloptions));
6814
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
6815
	if ($stream !== false)
6816
	{
6817
		$params = stream_context_get_params($stream);
6818
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
6819
	}
6820
	return $result;
6821
}
6822
6823
/**
6824
 * Check if the passed url has a redirect to https:// by querying headers.
6825
 *
6826
 * Returns true if a redirect was found & false if not.
6827
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
6828
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
6829
 *
6830
 * @param string $url to check, in $boardurl format (no trailing slash).
6831
 */
6832
function https_redirect_active($url)
6833
{
6834
	// Ask for the headers for the passed url, but via http...
6835
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
6836
	$url = str_ireplace('https://', 'http://', $url) . '/';
6837
	$headers = @get_headers($url);
6838
	if ($headers === false)
6839
		return false;
6840
6841
	// Now to see if it came back https...
6842
	// First check for a redirect status code in first row (301, 302, 307)
6843
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
6844
		return false;
6845
6846
	// Search for the location entry to confirm https
6847
	$result = false;
6848
	foreach ($headers as $header)
6849
	{
6850
		if (stristr($header, 'Location: https://') !== false)
6851
		{
6852
			$result = true;
6853
			break;
6854
		}
6855
	}
6856
	return $result;
6857
}
6858
6859
/**
6860
 * Build query_wanna_see_board and query_see_board for a userid
6861
 *
6862
 * Returns array with keys query_wanna_see_board and query_see_board
6863
 *
6864
 * @param int $userid of the user
6865
 */
6866
function build_query_board($userid)
6867
{
6868
	global $user_info, $modSettings, $smcFunc, $db_prefix;
6869
6870
	$query_part = array();
6871
6872
	// If we come from cron, we can't have a $user_info.
6873
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
6874
	{
6875
		$groups = $user_info['groups'];
6876
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
6877
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
6878
	}
6879
	else
6880
	{
6881
		$request = $smcFunc['db_query']('', '
6882
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
6883
			FROM {db_prefix}members AS mem
6884
			WHERE mem.id_member = {int:id_member}
6885
			LIMIT 1',
6886
			array(
6887
				'id_member' => $userid,
6888
			)
6889
		);
6890
6891
		$row = $smcFunc['db_fetch_assoc']($request);
6892
6893
		if (empty($row['additional_groups']))
6894
			$groups = array($row['id_group'], $row['id_post_group']);
6895
		else
6896
			$groups = array_merge(
6897
				array($row['id_group'], $row['id_post_group']),
6898
				explode(',', $row['additional_groups'])
6899
			);
6900
6901
		// Because history has proven that it is possible for groups to go bad - clean up in case.
6902
		foreach ($groups as $k => $v)
6903
			$groups[$k] = (int) $v;
6904
6905
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
6906
6907
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
6908
	}
6909
6910
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
6911
	if ($can_see_all_boards)
6912
		$query_part['query_see_board'] = '1=1';
6913
	// Otherwise just the groups in $user_info['groups'].
6914
	else
6915
	{
6916
		$query_part['query_see_board'] = '
6917
			EXISTS (
6918
				SELECT bpv.id_board
6919
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
6920
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
6921
					AND bpv.deny = 0
6922
					AND bpv.id_board = b.id_board
6923
			)';
6924
6925
		if (!empty($modSettings['deny_boards_access']))
6926
			$query_part['query_see_board'] .= '
6927
			AND NOT EXISTS (
6928
				SELECT bpv.id_board
6929
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
6930
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
6931
					AND bpv.deny = 1
6932
					AND bpv.id_board = b.id_board
6933
			)';
6934
	}
6935
6936
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
6937
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
6938
6939
	// Build the list of boards they WANT to see.
6940
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
6941
6942
	// If they aren't ignoring any boards then they want to see all the boards they can see
6943
	if (empty($ignoreboards))
6944
	{
6945
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
6946
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
6947
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
6948
	}
6949
	// Ok I guess they don't want to see all the boards
6950
	else
6951
	{
6952
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
6953
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
6954
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
6955
	}
6956
6957
	return $query_part;
6958
}
6959
6960
/**
6961
 * Check if the connection is using https.
6962
 *
6963
 * @return boolean true if connection used https
6964
 */
6965
function httpsOn()
6966
{
6967
	$secure = false;
6968
6969
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
6970
		$secure = true;
6971
	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')
6972
		$secure = true;
6973
6974
	return $secure;
6975
}
6976
6977
/**
6978
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
6979
 * with international characters (a.k.a. IRIs)
6980
 *
6981
 * @param string $iri The IRI to test.
6982
 * @param int $flags Optional flags to pass to filter_var()
6983
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
6984
 */
6985
function validate_iri($iri, $flags = null)
6986
{
6987
	$url = iri_to_url($iri);
6988
6989
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
6990
		return $iri;
6991
	else
6992
		return false;
6993
}
6994
6995
/**
6996
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
6997
 * with international characters (a.k.a. IRIs)
6998
 *
6999
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
7000
 * feed the result of this function to iri_to_url()
7001
 *
7002
 * @param string $iri The IRI to sanitize.
7003
 * @return string|bool The sanitized version of the IRI
7004
 */
7005
function sanitize_iri($iri)
7006
{
7007
	// Encode any non-ASCII characters (but not space or control characters of any sort)
7008
	$iri = preg_replace_callback('~[^\x00-\x7F\pZ\pC]~u', function($matches)
7009
	{
7010
		return rawurlencode($matches[0]);
7011
	}, $iri);
7012
7013
	// Perform normal sanitization
7014
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
7015
7016
	// Decode the non-ASCII characters
7017
	$iri = rawurldecode($iri);
7018
7019
	return $iri;
7020
}
7021
7022
/**
7023
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
7024
 *
7025
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
7026
 * standard URL encoding on the rest.
7027
 *
7028
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
7029
 * @return string|bool The URL version of the IRI.
7030
 */
7031
function iri_to_url($iri)
7032
{
7033
	global $sourcedir;
7034
7035
	$host = parse_url((strpos($iri, '://') === false ? 'http://' : '') . ltrim($iri, ':/'), PHP_URL_HOST);
7036
7037
	if (empty($host))
7038
		return $iri;
7039
7040
	// Convert the domain using the Punycode algorithm
7041
	require_once($sourcedir . '/Class-Punycode.php');
7042
	$Punycode = new Punycode();
7043
	$encoded_host = $Punycode->encode($host);
7044
	$pos = strpos($iri, $host);
7045
	$iri = substr_replace($iri, $encoded_host, $pos, strlen($host));
7046
7047
	// Encode any disallowed characters in the rest of the URL
7048
	$unescaped = array(
7049
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
7050
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
7051
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
7052
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
7053
		'%25' => '%',
7054
	);
7055
	$iri = strtr(rawurlencode($iri), $unescaped);
7056
7057
	return $iri;
7058
}
7059
7060
/**
7061
 * Decodes a URL containing encoded international characters to UTF-8
7062
 *
7063
 * Decodes any Punycode encoded characters in the domain name, then uses
7064
 * standard URL decoding on the rest.
7065
 *
7066
 * @param string $url The pure ASCII version of a URL.
7067
 * @return string|bool The UTF-8 version of the URL.
7068
 */
7069
function url_to_iri($url)
7070
{
7071
	global $sourcedir;
7072
7073
	$host = parse_url((strpos($url, '://') === false ? 'http://' : '') . ltrim($url, ':/'), PHP_URL_HOST);
7074
7075
	if (empty($host))
7076
		return $url;
7077
7078
	// Decode the domain from Punycode
7079
	require_once($sourcedir . '/Class-Punycode.php');
7080
	$Punycode = new Punycode();
7081
	$decoded_host = $Punycode->decode($host);
7082
	$pos = strpos($url, $host);
7083
	$url = substr_replace($url, $decoded_host, $pos, strlen($host));
7084
7085
	// Decode the rest of the URL
7086
	$url = rawurldecode($url);
7087
7088
	return $url;
7089
}
7090
7091
/**
7092
 * Ensures SMF's scheduled tasks are being run as intended
7093
 *
7094
 * If the admin activated the cron_is_real_cron setting, but the cron job is
7095
 * not running things at least once per day, we need to go back to SMF's default
7096
 * behaviour using "web cron" JavaScript calls.
7097
 */
7098
function check_cron()
7099
{
7100
	global $modSettings, $smcFunc, $txt;
7101
7102
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
7103
	{
7104
		$request = $smcFunc['db_query']('', '
7105
			SELECT COUNT(*)
7106
			FROM {db_prefix}scheduled_tasks
7107
			WHERE disabled = {int:not_disabled}
7108
				AND next_time < {int:yesterday}',
7109
			array(
7110
				'not_disabled' => 0,
7111
				'yesterday' => time() - 84600,
7112
			)
7113
		);
7114
		list($overdue) = $smcFunc['db_fetch_row']($request);
7115
		$smcFunc['db_free_result']($request);
7116
7117
		// If we have tasks more than a day overdue, cron isn't doing its job.
7118
		if (!empty($overdue))
7119
		{
7120
			loadLanguage('ManageScheduledTasks');
7121
			log_error($txt['cron_not_working']);
7122
			updateSettings(array('cron_is_real_cron' => 0));
7123
		}
7124
		else
7125
			updateSettings(array('cron_last_checked' => time()));
7126
	}
7127
}
7128
7129
/**
7130
 * Sends an appropriate HTTP status header based on a given status code
7131
 *
7132
 * @param int $code The status code
7133
 * @param string $status The string for the status. Set automatically if not provided.
7134
 */
7135
function send_http_status($code, $status = '')
7136
{
7137
	$statuses = array(
7138
		206 => 'Partial Content',
7139
		304 => 'Not Modified',
7140
		400 => 'Bad Request',
7141
		403 => 'Forbidden',
7142
		404 => 'Not Found',
7143
		410 => 'Gone',
7144
		500 => 'Internal Server Error',
7145
		503 => 'Service Unavailable',
7146
	);
7147
7148
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
7149
7150
	if (!isset($statuses[$code]) && empty($status))
7151
		header($protocol . ' 500 Internal Server Error');
7152
	else
7153
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
7154
}
7155
7156
/**
7157
 * Concatenates an array of strings into a grammatically correct sentence list
7158
 *
7159
 * Uses formats defined in the language files to build the list appropropriately
7160
 * for the currently loaded language.
7161
 *
7162
 * @param array $list An array of strings to concatenate.
7163
 * @return string The localized sentence list.
7164
 */
7165
function sentence_list($list)
7166
{
7167
	global $txt;
7168
7169
	// Make sure the bare necessities are defined
7170
	if (empty($txt['sentence_list_format']['n']))
7171
		$txt['sentence_list_format']['n'] = '{series}';
7172
	if (!isset($txt['sentence_list_separator']))
7173
		$txt['sentence_list_separator'] = ', ';
7174
	if (!isset($txt['sentence_list_separator_alt']))
7175
		$txt['sentence_list_separator_alt'] = '; ';
7176
7177
	// Which format should we use?
7178
	if (isset($txt['sentence_list_format'][count($list)]))
7179
		$format = $txt['sentence_list_format'][count($list)];
7180
	else
7181
		$format = $txt['sentence_list_format']['n'];
7182
7183
	// Do we want the normal separator or the alternate?
7184
	$separator = $txt['sentence_list_separator'];
7185
	foreach ($list as $item)
7186
	{
7187
		if (strpos($item, $separator) !== false)
7188
		{
7189
			$separator = $txt['sentence_list_separator_alt'];
7190
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
7191
			break;
7192
		}
7193
	}
7194
7195
	$replacements = array();
7196
7197
	// Special handling for the last items on the list
7198
	$i = 0;
7199
	while (empty($done))
7200
	{
7201
		if (strpos($format, '{'. --$i . '}') !== false)
7202
			$replacements['{'. $i . '}'] = array_pop($list);
7203
		else
7204
			$done = true;
7205
	}
7206
	unset($done);
7207
7208
	// Special handling for the first items on the list
7209
	$i = 0;
7210
	while (empty($done))
7211
	{
7212
		if (strpos($format, '{'. ++$i . '}') !== false)
7213
			$replacements['{'. $i . '}'] = array_shift($list);
7214
		else
7215
			$done = true;
7216
	}
7217
	unset($done);
7218
7219
	// Whatever is left
7220
	$replacements['{series}'] = implode($separator, $list);
7221
7222
	// Do the deed
7223
	return strtr($format, $replacements);
7224
}
7225
7226
/**
7227
 * Truncate an array to a specified length
7228
 *
7229
 * @param array $array The array to truncate
7230
 * @param int $max_length The upperbound on the length
7231
 * @param int $deep How levels in an multidimensional array should the function take into account.
7232
 * @return array The truncated array
7233
 */
7234
function truncate_array($array, $max_length = 1900, $deep = 3)
7235
{
7236
    $array = (array) $array;
7237
7238
    $curr_length = array_length($array, $deep);
7239
7240
    if ($curr_length <= $max_length)
7241
        return $array;
7242
7243
    else
7244
    {
7245
        // Truncate each element's value to a reasonable length
7246
        $param_max = floor($max_length / count($array));
7247
7248
        $current_deep = $deep - 1;
7249
7250
        foreach ($array as $key => &$value)
7251
        {
7252
            if (is_array($value))
7253
                if ($current_deep > 0)
7254
                    $value = truncate_array($value, $current_deep);
7255
7256
            else
7257
                $value = substr($value, 0, $param_max - strlen($key) - 5);
7258
        }
7259
7260
        return $array;
7261
    }
7262
}
7263
7264
/**
7265
 * array_length Recursive
7266
 * @param $array
7267
 * @param int $deep How many levels should the function
7268
 * @return int
7269
 */
7270
function array_length($array, $deep = 3)
7271
{
7272
    // Work with arrays
7273
    $array = (array) $array;
7274
    $length = 0;
7275
7276
    $deep_count = $deep - 1;
7277
7278
    foreach ($array as $value)
7279
    {
7280
        // Recursive?
7281
        if (is_array($value))
7282
        {
7283
            // No can't do
7284
            if ($deep_count <= 0)
7285
                continue;
7286
7287
            $length += array_length($value, $deep_count);
7288
        }
7289
7290
        else
7291
            $length += strlen($value);
7292
    }
7293
7294
    return $length;
7295
}
7296
7297
?>