Passed
Pull Request — release-2.1 (#6786)
by Jon
05:08
created

smf_entity_decode()   B

Complexity

Conditions 8
Paths 18

Size

Total Lines 35
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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

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

711
	return number_format($number, /** @scrutinizer ignore-type */ (float) $number === $number ? ($override_decimal_count === false ? $decimal_count : $override_decimal_count) : 0, $decimal_separator, $thousands_separator);
Loading history...
712
}
713
714
/**
715
 * Format a time to make it look purdy.
716
 *
717
 * - returns a pretty formatted version of time based on the user's format in $user_info['time_format'].
718
 * - applies all necessary time offsets to the timestamp, unless offset_type is set.
719
 * - if todayMod is set and show_today was not not specified or true, an
720
 *   alternate format string is used to show the date with something to show it is "today" or "yesterday".
721
 * - performs localization (more than just strftime would do alone.)
722
 *
723
 * @param int $log_time A timestamp
724
 * @param bool|string $show_today Whether to show "Today"/"Yesterday" or just a date. If a string is specified, that is used to temporarily override the date format.
725
 * @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.
726
 * @param bool $process_safe Activate setlocale check for changes at runtime. Slower, but safer.
727
 * @return string A formatted timestamp
728
 */
729
function timeformat($log_time, $show_today = true, $offset_type = false, $process_safe = false)
730
{
731
	global $context, $user_info, $txt, $modSettings;
732
	static $non_twelve_hour, $locale, $now;
733
	static $unsupportedFormats, $finalizedFormats;
734
735
	$unsupportedFormatsWindows = array('z', 'Z');
736
737
	// Ensure required values are set
738
	$user_info['time_format'] = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %H:%M');
739
740
	// Offset the time.
741
	if (!$offset_type)
742
		$log_time = forum_time(true, $log_time);
743
	// Just the forum offset?
744
	elseif ($offset_type == 'forum')
745
		$log_time = forum_time(false, $log_time);
746
747
	// We can't have a negative date (on Windows, at least.)
748
	if ($log_time < 0)
749
		$log_time = 0;
750
751
	// Today and Yesterday?
752
	$prefix = '';
753
	if ($modSettings['todayMod'] >= 1 && $show_today === true)
754
	{
755
		$now_time = forum_time();
756
757
		if ($now_time - $log_time < (86400 * $modSettings['todayMod']))
758
		{
759
			$then = @getdate($log_time);
760
			$now = (!empty($now) ? $now : @getdate($now_time));
761
762
			// Same day of the year, same year.... Today!
763
			if ($then['yday'] == $now['yday'] && $then['year'] == $now['year'])
764
			{
765
				$prefix = $txt['today'];
766
			}
767
			// 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...
768
			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))
769
			{
770
				$prefix = $txt['yesterday'];
771
			}
772
		}
773
	}
774
775
	// If $show_today is not a bool, use it as the date format & don't use $user_info. Allows for temp override of the format.
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
 * Converts 4-byte UTF-8 characters to entities if required for the database.
1100
 *
1101
 * Does nothing if the current character set is not UTF-8 or if the database
1102
 * can handle 4-byte UTF-8 characters.
1103
 *
1104
 * @param string $string A string possibly containing 4-byte UTF-8 characters.
1105
 * @return string A string that won't break the database.
1106
 */
1107
function fix_utf8mb4($string)
1108
{
1109
	global $context, $smcFunc;
1110
1111
	if (empty($context['utf8']) || $smcFunc['db_mb4'])
1112
		return $string;
1113
1114
	// The quick and easy way.
1115
	if (is_callable('mb_encode_numericentity'))
1116
		return mb_encode_numericentity($string, array(0x010000, 0x10FFFF, 0, 0xFFFFFF), 'UTF-8');
1117
1118
	// The slow and hard way.
1119
	$i = 0;
1120
	$len = strlen($string);
1121
	$new_string = '';
1122
	while ($i < $len)
1123
	{
1124
		$ord = ord($string[$i]);
1125
		if ($ord < 128)
1126
		{
1127
			$new_string .= $string[$i];
1128
			$i++;
1129
		}
1130
		elseif ($ord < 224)
1131
		{
1132
			$new_string .= $string[$i] . $string[$i + 1];
1133
			$i += 2;
1134
		}
1135
		elseif ($ord < 240)
1136
		{
1137
			$new_string .= $string[$i] . $string[$i + 1] . $string[$i + 2];
1138
			$i += 3;
1139
		}
1140
		elseif ($ord < 248)
1141
		{
1142
			// Magic happens.
1143
			$val = (ord($string[$i]) & 0x07) << 18;
1144
			$val += (ord($string[$i + 1]) & 0x3F) << 12;
1145
			$val += (ord($string[$i + 2]) & 0x3F) << 6;
1146
			$val += (ord($string[$i + 3]) & 0x3F);
1147
			$new_string .= '&#' . $val . ';';
1148
			$i += 4;
1149
		}
1150
	}
1151
	return $new_string;
1152
}
1153
1154
/**
1155
 * Decodes and sanitizes HTML entities.
1156
 *
1157
 * If database does not support 4-byte UTF-8 characters, entities for 4-byte
1158
 * characters are left in place.
1159
 *
1160
 * @param string $string The string in which to decode entities.
1161
 * @param bool $nbsp_to_space If true, decode '&nbsp;' to space character.
1162
 * 		Default: false.
1163
 * @param integer $flags Flags to pass to html_entity_decode.
1164
 * 		Default: ENT_QUOTES | ENT_HTML5.
1165
 * @return string The string with the entities decoded.
1166
 */
1167
function smf_entity_decode($string, $nbsp_to_space = false, $flags = ENT_QUOTES | ENT_HTML5)
1168
{
1169
	global $context, $smcFunc;
1170
1171
	// Basic parameter sanitization.
1172
	$string = (string) $string;
1173
	$nbsp_to_space = (bool) $nbsp_to_space;
1174
	$flags = !is_int($flags) ? ENT_QUOTES | ENT_HTML5 : $flags;
1175
1176
	// Don't waste time on empty strings.
1177
	if (trim($string) === '')
1178
		return $string;
1179
1180
	// In theory this is always UTF-8, but...
1181
	if (empty($context['character_set']))
1182
		$charset = is_callable('mb_detect_encoding') ? mb_detect_encoding($string) : 'UTF-8';
1183
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1184
		$charset = 'ISO-8859-1';
1185
	else
1186
		$charset = $context['character_set'];
1187
1188
	// Enables consistency with the behaviour of un_htmlspecialchars.
1189
	if ($nbsp_to_space)
1190
		$string = strtr($string, array('&nbsp;' => ' ', '&#xA0;' => ' ', '&#160;' => ' '));
1191
1192
	// Do the deed.
1193
	$string = html_entity_decode($string, $flags, $charset);
1194
1195
	// Remove any illegal character entities.
1196
	$string = sanitize_entities($string);
1197
1198
	// Finally, make sure we don't break the database.
1199
	$string = fix_utf8mb4($string);
1200
1201
	return $string;
1202
}
1203
1204
/**
1205
 * Replaces HTML entities for invalid characters with a substitute.
1206
 *
1207
 * The default substitute is the entity for the replacement character U+FFFD
1208
 * (a.k.a. the question-mark-in-a-box).
1209
 *
1210
 * !!! Warning !!! Setting $substitute to '' in order to delete invalid
1211
 * entities from the string can create unexpected security problems. See
1212
 * https://www.unicode.org/reports/tr36/#Deletion_of_Noncharacters for an
1213
 * explanation.
1214
 *
1215
 * @param string $string The string to sanitize.
1216
 * @param string $substitute Replacement for the invalid entities.
1217
 *      Default: '&#65533;'
1218
 * @return string The sanitized string.
1219
 */
1220
function sanitize_entities($string, $substitute = '&#65533;')
1221
{
1222
	static $disallowed;
1223
1224
	$string = (string) $string;
1225
	$substitute = (string) $substitute;
1226
1227
	if (strpos($string, '&#') === false)
1228
		return $string;
1229
1230
	if (empty($disallowed))
1231
	{
1232
		$disallowed['dec'] = array();
1233
1234
		// Control characters.
1235
		for ($i = 0x0; $i < 0x20; $i++)
1236
		{
1237
			// Allow \t, \n, and \r
1238
			if ($i === 0x9 || $i === 0xA || $i === 0xD)
1239
				continue;
1240
1241
			$disallowed['dec'][] = $i;
1242
		}
1243
		for ($i = 0x7F; $i < 0xA0; $i++)
1244
			$disallowed['dec'][] = $i;
1245
1246
		// UTF-16 surrogate pairs.
1247
		for ($i = 0xD800; $i < 0xE000; $i++)
1248
			$disallowed['dec'][] = $i;
1249
1250
		// Non-character code points.
1251
		for ($i = 0xFDD0; $i <= 0xFDEF; $i++)
1252
			$disallowed['dec'][] = $i;
1253
1254
		$disallowed['dec'] = array_merge($disallowed['dec'], array(
1255
			0xFFFE, 0xFFFF,
1256
			0x1FFFE, 0x1FFFF,
1257
			0x2FFFE, 0x2FFFF,
1258
			0x3FFFE, 0x3FFFF,
1259
			0x4FFFE, 0x4FFFF,
1260
			0x5FFFE, 0x5FFFF,
1261
			0x6FFFE, 0x6FFFF,
1262
			0x7FFFE, 0x7FFFF,
1263
			0x8FFFE, 0x8FFFF,
1264
			0x9FFFE, 0x9FFFF,
1265
			0xAFFFE, 0xAFFFF,
1266
			0xBFFFE, 0xBFFFF,
1267
			0xCFFFE, 0xCFFFF,
1268
			0xDFFFE, 0xDFFFF,
1269
			0xEFFFE, 0xEFFFF,
1270
			0xFFFFE, 0xFFFFF,
1271
			0x10FFFE, 0x10FFFF,
1272
		));
1273
1274
		// Now build the regexes to match these illegal entities.
1275
		$disallowed['hex'] = build_regex(array_map('dechex', $disallowed['dec']));
1276
		$disallowed['dec'] = build_regex($disallowed['dec']);
1277
	}
1278
1279
	// Replace all illegal entities with the entity for the replacement character.
1280
	$string = preg_replace('/&#(?'.'>x0*' . $disallowed['hex'] . '|0*' . $disallowed['dec'] . ');/i', $substitute, $string);
1281
1282
	return $string;
1283
}
1284
1285
/**
1286
 * Replaces invalid characters with a substitute.
1287
 *
1288
 * !!! Warning !!! Setting $substitute to '' in order to delete invalid
1289
 * characters from the string can create unexpected security problems. See
1290
 * https://www.unicode.org/reports/tr36/#Deletion_of_Noncharacters for an
1291
 * explanation.
1292
 *
1293
 * @param string $string The string to sanitize.
1294
 * @param string|null $substitute Replacement string for the invalid characters.
1295
 *      If not set, the Unicode replacement character "�" (U+FFFD) will be used
1296
 *      (or a fallback like "?" if necessary).
1297
 * @return string The sanitized string.
1298
 */
1299
function sanitize_chars($string, $substitute = null)
1300
{
1301
	global $context;
1302
1303
	$string = (string) $string;
1304
1305
	// What substitute character should we use?
1306
	if (isset($substitute))
1307
	{
1308
		$substitute = strval($substitute);
1309
	}
1310
	elseif (!empty($context['utf8']))
1311
	{
1312
		// Raw UTF-8 bytes for the replacement character.
1313
		$substitute = chr(239) . chr(191) . chr(189);
1314
	}
1315
	elseif (!empty($context['character_set']) && is_callable('mb_decode_numericentity'))
1316
	{
1317
		$substitute = mb_decode_numericentity('&#xFFFD;', array(0xFFFD,0xFFFD,0,0xFFFF), $context['character_set']);
1318
	}
1319
	else
1320
		$substitute = '?';
1321
1322
	// Fix any invalid byte sequences.
1323
	if (!empty($context['character_set']) && is_callable('mb_scrub'))
1324
	{
1325
		// For UTF-8, this preg_match test is much faster than mb_check_encoding.
1326
		$malformed = !empty($context['utf8']) ? @preg_match('//u', $string) === false && preg_last_error() === PREG_BAD_UTF8_ERROR : is_callable('mb_check_encoding') && !mb_check_encoding($string, $context['character_set']);
1327
1328
		if ($malformed)
1329
			$string = mb_scrub($string, $context['character_set']);
1330
	}
1331
1332
	// Fix any weird vertical space characters.
1333
	$string = normalize_spaces($string, true);
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type true; however, parameter $string of normalize_spaces() does only seem to accept string, 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

1333
	$string = normalize_spaces(/** @scrutinizer ignore-type */ $string, true);
Loading history...
1334
1335
	// Fix any unwanted control characters and other creepy-crawlies.
1336
	$string = preg_replace('/[^\P{C}\t\r\n]/' . (!empty($context['utf8']) ? 'u' : ''), $substitute, $string);
1337
1338
	return $string;
1339
}
1340
1341
/**
1342
 * Normalizes space characters and line breaks.
1343
 *
1344
 * @param string $string The string to sanitize.
1345
 * @param bool $vspace If true, replaces all line breaks and vertical space
1346
 *      characters with "\n". Default: true.
1347
 * @param bool $hspace If true, replaces horizontal space characters with a
1348
 *      plain " " character. (Note: tabs are not replaced unless the
1349
 *      'replace_tabs' option is supplied.) Default: false.
1350
 * @param array $options An array of boolean options. Possible values are:
1351
 *      - no_breaks: Vertical spaces are replaced by " " instead of "\n".
1352
 *      - replace_tabs: If true, tabs are are replaced by " " chars.
1353
 *      - collapse_hspace: If true, removes extra horizontal spaces.
1354
 * @return string The sanitized string.
1355
 */
1356
function normalize_spaces($string, $vspace = true, $hspace = false, $options = array())
1357
{
1358
	global $context;
1359
1360
	$string = (string) $string;
1361
	$vspace = !empty($vspace);
1362
	$hspace = !empty($hspace);
1363
1364
	if (!$vspace && !$hspace)
1365
		return $string;
1366
1367
	$options['no_breaks'] = !empty($options['no_breaks']);
1368
	$options['collapse_hspace'] = !empty($options['collapse_hspace']);
1369
	$options['replace_tabs'] = !empty($options['replace_tabs']);
1370
1371
	$patterns = array();
1372
	$replacements = array();
1373
1374
	if ($vspace)
1375
	{
1376
		// \R is like \v, except it handles "\r\n" as a single unit.
1377
		$patterns[] = '/\R/' . ($context['utf8'] ? 'u' : '');
1378
		$replacements[] = $options['no_breaks'] ? ' ' : "\n";
1379
	}
1380
1381
	if ($hspace)
1382
	{
1383
		// Interesting fact: Unicode properties like \p{Zs} work even when not in UTF-8 mode.
1384
		$patterns[] = '/' . ($options['replace_tabs'] ? '\h' : '\p{Zs}') . ($options['collapse_hspace'] ? '+' : '') . '/' . ($context['utf8'] ? 'u' : '');
1385
		$replacements[] = ' ';
1386
	}
1387
1388
	return preg_replace($patterns, $replacements, $string);
1389
}
1390
1391
/**
1392
 * Shorten a subject + internationalization concerns.
1393
 *
1394
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
1395
 * - respects internationalization characters and entities as one character.
1396
 * - avoids trailing entities.
1397
 * - returns the shortened string.
1398
 *
1399
 * @param string $subject The subject
1400
 * @param int $len How many characters to limit it to
1401
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
1402
 */
1403
function shorten_subject($subject, $len)
1404
{
1405
	global $smcFunc;
1406
1407
	// It was already short enough!
1408
	if ($smcFunc['strlen']($subject) <= $len)
1409
		return $subject;
1410
1411
	// Shorten it by the length it was too long, and strip off junk from the end.
1412
	return $smcFunc['substr']($subject, 0, $len) . '...';
1413
}
1414
1415
/**
1416
 * Gets the current time with offset.
1417
 *
1418
 * - always applies the offset in the time_offset setting.
1419
 *
1420
 * @param bool $use_user_offset Whether to apply the user's offset as well
1421
 * @param int $timestamp A timestamp (null to use current time)
1422
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
1423
 */
1424
function forum_time($use_user_offset = true, $timestamp = null)
1425
{
1426
	global $user_info, $modSettings;
1427
1428
	// Ensure required values are set
1429
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
1430
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
1431
1432
	if ($timestamp === null)
1433
		$timestamp = time();
1434
	elseif ($timestamp == 0)
1435
		return 0;
1436
1437
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
1438
}
1439
1440
/**
1441
 * Calculates all the possible permutations (orders) of array.
1442
 * should not be called on huge arrays (bigger than like 10 elements.)
1443
 * returns an array containing each permutation.
1444
 *
1445
 * @deprecated since 2.1
1446
 * @param array $array An array
1447
 * @return array An array containing each permutation
1448
 */
1449
function permute($array)
1450
{
1451
	$orders = array($array);
1452
1453
	$n = count($array);
1454
	$p = range(0, $n);
1455
	for ($i = 1; $i < $n; null)
1456
	{
1457
		$p[$i]--;
1458
		$j = $i % 2 != 0 ? $p[$i] : 0;
1459
1460
		$temp = $array[$i];
1461
		$array[$i] = $array[$j];
1462
		$array[$j] = $temp;
1463
1464
		for ($i = 1; $p[$i] == 0; $i++)
1465
			$p[$i] = 1;
1466
1467
		$orders[] = $array;
1468
	}
1469
1470
	return $orders;
1471
}
1472
1473
/**
1474
 * Parse bulletin board code in a string, as well as smileys optionally.
1475
 *
1476
 * - only parses bbc tags which are not disabled in disabledBBC.
1477
 * - handles basic HTML, if enablePostHTML is on.
1478
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1479
 * - only parses smileys if smileys is true.
1480
 * - does nothing if the enableBBC setting is off.
1481
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1482
 * - returns the modified message.
1483
 *
1484
 * @param string|bool $message The message.
1485
 *		When a empty string, nothing is done.
1486
 *		When false we provide a list of BBC codes available.
1487
 *		When a string, the message is parsed and bbc handled.
1488
 * @param bool $smileys Whether to parse smileys as well
1489
 * @param string $cache_id The cache ID
1490
 * @param array $parse_tags If set, only parses these tags rather than all of them
1491
 * @return string The parsed message
1492
 */
1493
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1494
{
1495
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable;
1496
	static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array();
1497
	static $disabled, $alltags_regex = '', $param_regexes = array();
1498
1499
	// Don't waste cycles
1500
	if ($message === '')
1501
		return '';
1502
1503
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1504
	if (!isset($context['utf8']))
1505
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1506
1507
	// Clean up any cut/paste issues we may have
1508
	$message = sanitizeMSCutPaste($message);
1509
1510
	// If the load average is too high, don't parse the BBC.
1511
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1512
	{
1513
		$context['disabled_parse_bbc'] = true;
1514
		return $message;
1515
	}
1516
1517
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1518
		$smileys = (bool) $smileys;
1519
1520
	if (empty($modSettings['enableBBC']) && $message !== false)
1521
	{
1522
		if ($smileys === true)
1523
			parsesmileys($message);
1524
1525
		return $message;
1526
	}
1527
1528
	// If we already have a version of the BBCodes for the current language, use that. Otherwise, make one.
1529
	if (!empty($bbc_lang_locales[$txt['lang_locale']]))
1530
		$bbc_codes = $bbc_lang_locales[$txt['lang_locale']];
1531
	else
1532
		$bbc_codes = array();
1533
1534
	// If we are not doing every tag then we don't cache this run.
1535
	if (!empty($parse_tags))
1536
		$bbc_codes = array();
1537
1538
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1539
	if (!empty($modSettings['autoLinkUrls']))
1540
		set_tld_regex();
1541
1542
	// Allow mods access before entering the main parse_bbc loop
1543
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1544
1545
	// Sift out the bbc for a performance improvement.
1546
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1547
	{
1548
		if (!empty($modSettings['disabledBBC']))
1549
		{
1550
			$disabled = array();
1551
1552
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1553
1554
			foreach ($temp as $tag)
1555
				$disabled[trim($tag)] = true;
1556
1557
			if (in_array('color', $disabled))
1558
				$disabled = array_merge($disabled, array(
1559
					'black' => true,
1560
					'white' => true,
1561
					'red' => true,
1562
					'green' => true,
1563
					'blue' => true,
1564
					)
1565
				);
1566
		}
1567
1568
		// The YouTube bbc needs this for its origin parameter
1569
		$scripturl_parts = parse_url($scripturl);
1570
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1571
1572
		/* The following bbc are formatted as an array, with keys as follows:
1573
1574
			tag: the tag's name - should be lowercase!
1575
1576
			type: one of...
1577
				- (missing): [tag]parsed content[/tag]
1578
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1579
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1580
				- unparsed_content: [tag]unparsed content[/tag]
1581
				- closed: [tag], [tag/], [tag /]
1582
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1583
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1584
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1585
1586
			parameters: an optional array of parameters, for the form
1587
			  [tag abc=123]content[/tag].  The array is an associative array
1588
			  where the keys are the parameter names, and the values are an
1589
			  array which may contain the following:
1590
				- match: a regular expression to validate and match the value.
1591
				- quoted: true if the value should be quoted.
1592
				- validate: callback to evaluate on the data, which is $data.
1593
				- value: a string in which to replace $1 with the data.
1594
					Either value or validate may be used, not both.
1595
				- optional: true if the parameter is optional.
1596
				- default: a default value for missing optional parameters.
1597
1598
			test: a regular expression to test immediately after the tag's
1599
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1600
			  Optional.
1601
1602
			content: only available for unparsed_content, closed,
1603
			  unparsed_commas_content, and unparsed_equals_content.
1604
			  $1 is replaced with the content of the tag.  Parameters
1605
			  are replaced in the form {param}.  For unparsed_commas_content,
1606
			  $2, $3, ..., $n are replaced.
1607
1608
			before: only when content is not used, to go before any
1609
			  content.  For unparsed_equals, $1 is replaced with the value.
1610
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1611
1612
			after: similar to before in every way, except that it is used
1613
			  when the tag is closed.
1614
1615
			disabled_content: used in place of content when the tag is
1616
			  disabled.  For closed, default is '', otherwise it is '$1' if
1617
			  block_level is false, '<div>$1</div>' elsewise.
1618
1619
			disabled_before: used in place of before when disabled.  Defaults
1620
			  to '<div>' if block_level, '' if not.
1621
1622
			disabled_after: used in place of after when disabled.  Defaults
1623
			  to '</div>' if block_level, '' if not.
1624
1625
			block_level: set to true the tag is a "block level" tag, similar
1626
			  to HTML.  Block level tags cannot be nested inside tags that are
1627
			  not block level, and will not be implicitly closed as easily.
1628
			  One break following a block level tag may also be removed.
1629
1630
			trim: if set, and 'inside' whitespace after the begin tag will be
1631
			  removed.  If set to 'outside', whitespace after the end tag will
1632
			  meet the same fate.
1633
1634
			validate: except when type is missing or 'closed', a callback to
1635
			  validate the data as $data.  Depending on the tag's type, $data
1636
			  may be a string or an array of strings (corresponding to the
1637
			  replacement.)
1638
1639
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1640
			  may be not set, 'optional', or 'required' corresponding to if
1641
			  the content may be quoted.  This allows the parser to read
1642
			  [tag="abc]def[esdf]"] properly.
1643
1644
			require_parents: an array of tag names, or not set.  If set, the
1645
			  enclosing tag *must* be one of the listed tags, or parsing won't
1646
			  occur.
1647
1648
			require_children: similar to require_parents, if set children
1649
			  won't be parsed if they are not in the list.
1650
1651
			disallow_children: similar to, but very different from,
1652
			  require_children, if it is set the listed tags will not be
1653
			  parsed inside the tag.
1654
1655
			parsed_tags_allowed: an array restricting what BBC can be in the
1656
			  parsed_equals parameter, if desired.
1657
		*/
1658
1659
		$codes = array(
1660
			array(
1661
				'tag' => 'abbr',
1662
				'type' => 'unparsed_equals',
1663
				'before' => '<abbr title="$1">',
1664
				'after' => '</abbr>',
1665
				'quoted' => 'optional',
1666
				'disabled_after' => ' ($1)',
1667
			),
1668
			// Legacy (and just an alias for [abbr] even when enabled)
1669
			array(
1670
				'tag' => 'acronym',
1671
				'type' => 'unparsed_equals',
1672
				'before' => '<abbr title="$1">',
1673
				'after' => '</abbr>',
1674
				'quoted' => 'optional',
1675
				'disabled_after' => ' ($1)',
1676
			),
1677
			array(
1678
				'tag' => 'anchor',
1679
				'type' => 'unparsed_equals',
1680
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1681
				'before' => '<span id="post_$1">',
1682
				'after' => '</span>',
1683
			),
1684
			array(
1685
				'tag' => 'attach',
1686
				'type' => 'unparsed_content',
1687
				'parameters' => array(
1688
					'id' => array('match' => '(\d+)'),
1689
					'alt' => array('optional' => true),
1690
					'width' => array('optional' => true, 'match' => '(\d+)'),
1691
					'height' => array('optional' => true, 'match' => '(\d+)'),
1692
					'display' => array('optional' => true, 'match' => '(link|embed)'),
1693
				),
1694
				'content' => '$1',
1695
				'validate' => function(&$tag, &$data, $disabled, $params) use ($modSettings, $context, $sourcedir, $txt, $smcFunc)
0 ignored issues
show
Unused Code introduced by
The import $context is not used and could be removed.

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

Loading history...
1696
				{
1697
					$returnContext = '';
1698
1699
					// BBC or the entire attachments feature is disabled
1700
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1701
						return $data;
1702
1703
					// Save the attach ID.
1704
					$attachID = $params['{id}'];
1705
1706
					// Kinda need this.
1707
					require_once($sourcedir . '/Subs-Attachments.php');
1708
1709
					$currentAttachment = parseAttachBBC($attachID);
1710
1711
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1712
					if (is_string($currentAttachment))
1713
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1714
1715
					// We need a display mode.
1716
					if (empty($params['{display}']))
1717
					{
1718
						// Images, video, and audio are embedded by default.
1719
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1720
							$params['{display}'] = 'embed';
1721
						// Anything else shows a link by default.
1722
						else
1723
							$params['{display}'] = 'link';
1724
					}
1725
1726
					// Embedded file.
1727
					if ($params['{display}'] == 'embed')
1728
					{
1729
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1730
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1731
1732
						// Image.
1733
						if (!empty($currentAttachment['is_image']))
1734
						{
1735
							if (empty($params['{width}']) && empty($params['{height}']))
1736
								$returnContext .= '<img src="' . $currentAttachment['href'] . '"' . $alt . $title . ' class="bbc_img"></a>';
1737
							else
1738
							{
1739
								$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"': '';
1740
								$height = !empty($params['{height}']) ? 'height="' . $params['{height}'] . '"' : '';
1741
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img resized"/>';
1742
							}
1743
						}
1744
						// Video.
1745
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1746
						{
1747
							$width = !empty($width) ? ' width="' . $width . '"' : '';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $width seems to never exist and therefore empty should always be true.
Loading history...
1748
							$height = !empty($height) ? ' height="' . $height . '"' : '';
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $height seems to never exist and therefore empty should always be true.
Loading history...
1749
1750
							$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>' : '');
1751
						}
1752
						// Audio.
1753
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1754
						{
1755
							$width = 'max-width:100%; width: ' . (!empty($width) ? $width : '400') . 'px;';
1756
							$height = !empty($height) ? 'height: ' . $height . 'px;' : '';
1757
1758
							$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>';
1759
						}
1760
						// Anything else.
1761
						else
1762
						{
1763
							$width = !empty($width) ? ' width="' . $width . '"' : '';
1764
							$height = !empty($height) ? ' height="' . $height . '"' : '';
1765
1766
							$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>';
1767
						}
1768
					}
1769
1770
					// No image. Show a link.
1771
					else
1772
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1773
1774
					// Use this hook to adjust the HTML output of the attach BBCode.
1775
					// If you want to work with the attachment data itself, use one of these:
1776
					// - integrate_pre_parseAttachBBC
1777
					// - integrate_post_parseAttachBBC
1778
					call_integration_hook('integrate_attach_bbc_validate', array(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params));
1779
1780
					// Gotta append what we just did.
1781
					$data = $returnContext;
1782
				},
1783
			),
1784
			array(
1785
				'tag' => 'b',
1786
				'before' => '<b>',
1787
				'after' => '</b>',
1788
			),
1789
			// Legacy (equivalent to [ltr] or [rtl])
1790
			array(
1791
				'tag' => 'bdo',
1792
				'type' => 'unparsed_equals',
1793
				'before' => '<bdo dir="$1">',
1794
				'after' => '</bdo>',
1795
				'test' => '(rtl|ltr)\]',
1796
				'block_level' => true,
1797
			),
1798
			// Legacy (alias of [color=black])
1799
			array(
1800
				'tag' => 'black',
1801
				'before' => '<span style="color: black;" class="bbc_color">',
1802
				'after' => '</span>',
1803
			),
1804
			// Legacy (alias of [color=blue])
1805
			array(
1806
				'tag' => 'blue',
1807
				'before' => '<span style="color: blue;" class="bbc_color">',
1808
				'after' => '</span>',
1809
			),
1810
			array(
1811
				'tag' => 'br',
1812
				'type' => 'closed',
1813
				'content' => '<br>',
1814
			),
1815
			array(
1816
				'tag' => 'center',
1817
				'before' => '<div class="centertext">',
1818
				'after' => '</div>',
1819
				'block_level' => true,
1820
			),
1821
			array(
1822
				'tag' => 'code',
1823
				'type' => 'unparsed_content',
1824
				'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>',
1825
				// @todo Maybe this can be simplified?
1826
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1827
				{
1828
					if (!isset($disabled['code']))
1829
					{
1830
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1831
1832
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1833
						{
1834
							// Do PHP code coloring?
1835
							if ($php_parts[$php_i] != '&lt;?php')
1836
								continue;
1837
1838
							$php_string = '';
1839
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1840
							{
1841
								$php_string .= $php_parts[$php_i];
1842
								$php_parts[$php_i++] = '';
1843
							}
1844
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1845
						}
1846
1847
						// Fix the PHP code stuff...
1848
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1849
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1850
1851
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1852
						if (!empty($context['browser']['is_opera']))
1853
							$data .= '&nbsp;';
1854
					}
1855
				},
1856
				'block_level' => true,
1857
			),
1858
			array(
1859
				'tag' => 'code',
1860
				'type' => 'unparsed_equals_content',
1861
				'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>',
1862
				// @todo Maybe this can be simplified?
1863
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1864
				{
1865
					if (!isset($disabled['code']))
1866
					{
1867
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1868
1869
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1870
						{
1871
							// Do PHP code coloring?
1872
							if ($php_parts[$php_i] != '&lt;?php')
1873
								continue;
1874
1875
							$php_string = '';
1876
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1877
							{
1878
								$php_string .= $php_parts[$php_i];
1879
								$php_parts[$php_i++] = '';
1880
							}
1881
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1882
						}
1883
1884
						// Fix the PHP code stuff...
1885
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1886
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1887
1888
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1889
						if (!empty($context['browser']['is_opera']))
1890
							$data[0] .= '&nbsp;';
1891
					}
1892
				},
1893
				'block_level' => true,
1894
			),
1895
			array(
1896
				'tag' => 'color',
1897
				'type' => 'unparsed_equals',
1898
				'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]?)\))\]',
1899
				'before' => '<span style="color: $1;" class="bbc_color">',
1900
				'after' => '</span>',
1901
			),
1902
			array(
1903
				'tag' => 'email',
1904
				'type' => 'unparsed_content',
1905
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1906
				// @todo Should this respect guest_hideContacts?
1907
				'validate' => function(&$tag, &$data, $disabled)
1908
				{
1909
					$data = strtr($data, array('<br>' => ''));
1910
				},
1911
			),
1912
			array(
1913
				'tag' => 'email',
1914
				'type' => 'unparsed_equals',
1915
				'before' => '<a href="mailto:$1" class="bbc_email">',
1916
				'after' => '</a>',
1917
				// @todo Should this respect guest_hideContacts?
1918
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1919
				'disabled_after' => ' ($1)',
1920
			),
1921
			// Legacy (and just a link even when not disabled)
1922
			array(
1923
				'tag' => 'flash',
1924
				'type' => 'unparsed_commas_content',
1925
				'test' => '\d+,\d+\]',
1926
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1927
				'validate' => function (&$tag, &$data, $disabled)
1928
				{
1929
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1930
					if (empty($scheme))
1931
						$data[0] = '//' . ltrim($data[0], ':/');
1932
				},
1933
			),
1934
			array(
1935
				'tag' => 'float',
1936
				'type' => 'unparsed_equals',
1937
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1938
				'before' => '<div $1>',
1939
				'after' => '</div>',
1940
				'validate' => function(&$tag, &$data, $disabled)
1941
				{
1942
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1943
1944
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1945
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1946
					else
1947
						$css = '';
1948
1949
					$data = $class . $css;
1950
				},
1951
				'trim' => 'outside',
1952
				'block_level' => true,
1953
			),
1954
			// Legacy (alias of [url] with an FTP URL)
1955
			array(
1956
				'tag' => 'ftp',
1957
				'type' => 'unparsed_content',
1958
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1959
				'validate' => function(&$tag, &$data, $disabled)
1960
				{
1961
					$data = strtr($data, array('<br>' => ''));
1962
					$scheme = parse_url($data, PHP_URL_SCHEME);
1963
					if (empty($scheme))
1964
						$data = 'ftp://' . ltrim($data, ':/');
1965
				},
1966
			),
1967
			// Legacy (alias of [url] with an FTP URL)
1968
			array(
1969
				'tag' => 'ftp',
1970
				'type' => 'unparsed_equals',
1971
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1972
				'after' => '</a>',
1973
				'validate' => function(&$tag, &$data, $disabled)
1974
				{
1975
					$scheme = parse_url($data, PHP_URL_SCHEME);
1976
					if (empty($scheme))
1977
						$data = 'ftp://' . ltrim($data, ':/');
1978
				},
1979
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1980
				'disabled_after' => ' ($1)',
1981
			),
1982
			array(
1983
				'tag' => 'font',
1984
				'type' => 'unparsed_equals',
1985
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1986
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1987
				'after' => '</span>',
1988
			),
1989
			// Legacy (one of those things that should not be done)
1990
			array(
1991
				'tag' => 'glow',
1992
				'type' => 'unparsed_commas',
1993
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1994
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1995
				'after' => '</span>',
1996
			),
1997
			// Legacy (alias of [color=green])
1998
			array(
1999
				'tag' => 'green',
2000
				'before' => '<span style="color: green;" class="bbc_color">',
2001
				'after' => '</span>',
2002
			),
2003
			array(
2004
				'tag' => 'html',
2005
				'type' => 'unparsed_content',
2006
				'content' => '<div>$1</div>',
2007
				'block_level' => true,
2008
				'disabled_content' => '$1',
2009
			),
2010
			array(
2011
				'tag' => 'hr',
2012
				'type' => 'closed',
2013
				'content' => '<hr>',
2014
				'block_level' => true,
2015
			),
2016
			array(
2017
				'tag' => 'i',
2018
				'before' => '<i>',
2019
				'after' => '</i>',
2020
			),
2021
			array(
2022
				'tag' => 'img',
2023
				'type' => 'unparsed_content',
2024
				'parameters' => array(
2025
					'alt' => array('optional' => true),
2026
					'title' => array('optional' => true),
2027
				),
2028
				'content' => '<img src="$1" alt="{alt}" title="{title}" class="bbc_img" loading="lazy">',
2029
				'validate' => function(&$tag, &$data, $disabled)
2030
				{
2031
					$data = strtr($data, array('<br>' => ''));
2032
2033
					if (parse_url($data, PHP_URL_SCHEME) === null)
2034
						$data = '//' . ltrim($data, ':/');
2035
					else
2036
						$data = get_proxied_url($data);
2037
				},
2038
				'disabled_content' => '($1)',
2039
			),
2040
			array(
2041
				'tag' => 'img',
2042
				'type' => 'unparsed_content',
2043
				'parameters' => array(
2044
					'alt' => array('optional' => true),
2045
					'title' => array('optional' => true),
2046
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
2047
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
2048
				),
2049
				'content' => '<img src="$1" alt="{alt}" title="{title}"{width}{height} class="bbc_img resized" loading="lazy">',
2050
				'validate' => function(&$tag, &$data, $disabled)
2051
				{
2052
					$data = strtr($data, array('<br>' => ''));
2053
2054
					if (parse_url($data, PHP_URL_SCHEME) === null)
2055
						$data = '//' . ltrim($data, ':/');
2056
					else
2057
						$data = get_proxied_url($data);
2058
				},
2059
				'disabled_content' => '($1)',
2060
			),
2061
			array(
2062
				'tag' => 'iurl',
2063
				'type' => 'unparsed_content',
2064
				'content' => '<a href="$1" class="bbc_link">$1</a>',
2065
				'validate' => function(&$tag, &$data, $disabled)
2066
				{
2067
					$data = strtr($data, array('<br>' => ''));
2068
					$scheme = parse_url($data, PHP_URL_SCHEME);
2069
					if (empty($scheme))
2070
						$data = '//' . ltrim($data, ':/');
2071
				},
2072
			),
2073
			array(
2074
				'tag' => 'iurl',
2075
				'type' => 'unparsed_equals',
2076
				'quoted' => 'optional',
2077
				'before' => '<a href="$1" class="bbc_link">',
2078
				'after' => '</a>',
2079
				'validate' => function(&$tag, &$data, $disabled)
2080
				{
2081
					if (substr($data, 0, 1) == '#')
2082
						$data = '#post_' . substr($data, 1);
2083
					else
2084
					{
2085
						$scheme = parse_url($data, PHP_URL_SCHEME);
2086
						if (empty($scheme))
2087
							$data = '//' . ltrim($data, ':/');
2088
					}
2089
				},
2090
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2091
				'disabled_after' => ' ($1)',
2092
			),
2093
			array(
2094
				'tag' => 'justify',
2095
				'before' => '<div class="justifytext">',
2096
				'after' => '</div>',
2097
				'block_level' => true,
2098
			),
2099
			array(
2100
				'tag' => 'left',
2101
				'before' => '<div class="lefttext">',
2102
				'after' => '</div>',
2103
				'block_level' => true,
2104
			),
2105
			array(
2106
				'tag' => 'li',
2107
				'before' => '<li>',
2108
				'after' => '</li>',
2109
				'trim' => 'outside',
2110
				'require_parents' => array('list'),
2111
				'block_level' => true,
2112
				'disabled_before' => '',
2113
				'disabled_after' => '<br>',
2114
			),
2115
			array(
2116
				'tag' => 'list',
2117
				'before' => '<ul class="bbc_list">',
2118
				'after' => '</ul>',
2119
				'trim' => 'inside',
2120
				'require_children' => array('li', 'list'),
2121
				'block_level' => true,
2122
			),
2123
			array(
2124
				'tag' => 'list',
2125
				'parameters' => array(
2126
					'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)'),
2127
				),
2128
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
2129
				'after' => '</ul>',
2130
				'trim' => 'inside',
2131
				'require_children' => array('li'),
2132
				'block_level' => true,
2133
			),
2134
			array(
2135
				'tag' => 'ltr',
2136
				'before' => '<bdo dir="ltr">',
2137
				'after' => '</bdo>',
2138
				'block_level' => true,
2139
			),
2140
			array(
2141
				'tag' => 'me',
2142
				'type' => 'unparsed_equals',
2143
				'before' => '<div class="meaction">* $1 ',
2144
				'after' => '</div>',
2145
				'quoted' => 'optional',
2146
				'block_level' => true,
2147
				'disabled_before' => '/me ',
2148
				'disabled_after' => '<br>',
2149
			),
2150
			array(
2151
				'tag' => 'member',
2152
				'type' => 'unparsed_equals',
2153
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
2154
				'after' => '</a>',
2155
			),
2156
			// Legacy (horrible memories of the 1990s)
2157
			array(
2158
				'tag' => 'move',
2159
				'before' => '<marquee>',
2160
				'after' => '</marquee>',
2161
				'block_level' => true,
2162
				'disallow_children' => array('move'),
2163
			),
2164
			array(
2165
				'tag' => 'nobbc',
2166
				'type' => 'unparsed_content',
2167
				'content' => '$1',
2168
			),
2169
			array(
2170
				'tag' => 'php',
2171
				'type' => 'unparsed_content',
2172
				'content' => '<span class="phpcode">$1</span>',
2173
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
2174
				{
2175
					if (!isset($disabled['php']))
2176
					{
2177
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
2178
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
2179
						if ($add_begin)
2180
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
2181
					}
2182
				},
2183
				'block_level' => false,
2184
				'disabled_content' => '$1',
2185
			),
2186
			array(
2187
				'tag' => 'pre',
2188
				'before' => '<pre>',
2189
				'after' => '</pre>',
2190
			),
2191
			array(
2192
				'tag' => 'quote',
2193
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
2194
				'after' => '</blockquote>',
2195
				'trim' => 'both',
2196
				'block_level' => true,
2197
			),
2198
			array(
2199
				'tag' => 'quote',
2200
				'parameters' => array(
2201
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
2202
				),
2203
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
2204
				'after' => '</blockquote>',
2205
				'trim' => 'both',
2206
				'block_level' => true,
2207
			),
2208
			array(
2209
				'tag' => 'quote',
2210
				'type' => 'parsed_equals',
2211
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
2212
				'after' => '</blockquote>',
2213
				'trim' => 'both',
2214
				'quoted' => 'optional',
2215
				// Don't allow everything to be embedded with the author name.
2216
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
2217
				'block_level' => true,
2218
			),
2219
			array(
2220
				'tag' => 'quote',
2221
				'parameters' => array(
2222
					'author' => array('match' => '([^<>]{1,192}?)'),
2223
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
2224
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
2225
				),
2226
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
2227
				'after' => '</blockquote>',
2228
				'trim' => 'both',
2229
				'block_level' => true,
2230
			),
2231
			array(
2232
				'tag' => 'quote',
2233
				'parameters' => array(
2234
					'author' => array('match' => '(.{1,192}?)'),
2235
				),
2236
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
2237
				'after' => '</blockquote>',
2238
				'trim' => 'both',
2239
				'block_level' => true,
2240
			),
2241
			// Legacy (alias of [color=red])
2242
			array(
2243
				'tag' => 'red',
2244
				'before' => '<span style="color: red;" class="bbc_color">',
2245
				'after' => '</span>',
2246
			),
2247
			array(
2248
				'tag' => 'right',
2249
				'before' => '<div class="righttext">',
2250
				'after' => '</div>',
2251
				'block_level' => true,
2252
			),
2253
			array(
2254
				'tag' => 'rtl',
2255
				'before' => '<bdo dir="rtl">',
2256
				'after' => '</bdo>',
2257
				'block_level' => true,
2258
			),
2259
			array(
2260
				'tag' => 's',
2261
				'before' => '<s>',
2262
				'after' => '</s>',
2263
			),
2264
			// Legacy (never a good idea)
2265
			array(
2266
				'tag' => 'shadow',
2267
				'type' => 'unparsed_commas',
2268
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
2269
				'before' => '<span style="text-shadow: $1 $2">',
2270
				'after' => '</span>',
2271
				'validate' => function(&$tag, &$data, $disabled)
2272
				{
2273
2274
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
2275
						$data[1] = '0 -2px 1px';
2276
2277
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
2278
						$data[1] = '2px 0 1px';
2279
2280
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
2281
						$data[1] = '0 2px 1px';
2282
2283
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
2284
						$data[1] = '-2px 0 1px';
2285
2286
					else
2287
						$data[1] = '1px 1px 1px';
2288
				},
2289
			),
2290
			array(
2291
				'tag' => 'size',
2292
				'type' => 'unparsed_equals',
2293
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
2294
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2295
				'after' => '</span>',
2296
			),
2297
			array(
2298
				'tag' => 'size',
2299
				'type' => 'unparsed_equals',
2300
				'test' => '[1-7]\]',
2301
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2302
				'after' => '</span>',
2303
				'validate' => function(&$tag, &$data, $disabled)
2304
				{
2305
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
2306
					$data = $sizes[$data] . 'em';
2307
				},
2308
			),
2309
			array(
2310
				'tag' => 'sub',
2311
				'before' => '<sub>',
2312
				'after' => '</sub>',
2313
			),
2314
			array(
2315
				'tag' => 'sup',
2316
				'before' => '<sup>',
2317
				'after' => '</sup>',
2318
			),
2319
			array(
2320
				'tag' => 'table',
2321
				'before' => '<table class="bbc_table">',
2322
				'after' => '</table>',
2323
				'trim' => 'inside',
2324
				'require_children' => array('tr'),
2325
				'block_level' => true,
2326
			),
2327
			array(
2328
				'tag' => 'td',
2329
				'before' => '<td>',
2330
				'after' => '</td>',
2331
				'require_parents' => array('tr'),
2332
				'trim' => 'outside',
2333
				'block_level' => true,
2334
				'disabled_before' => '',
2335
				'disabled_after' => '',
2336
			),
2337
			array(
2338
				'tag' => 'time',
2339
				'type' => 'unparsed_content',
2340
				'content' => '$1',
2341
				'validate' => function(&$tag, &$data, $disabled)
2342
				{
2343
					if (is_numeric($data))
2344
						$data = timeformat($data);
2345
2346
					$tag['content'] = '<span class="bbc_time">$1</span>';
2347
				},
2348
			),
2349
			array(
2350
				'tag' => 'tr',
2351
				'before' => '<tr>',
2352
				'after' => '</tr>',
2353
				'require_parents' => array('table'),
2354
				'require_children' => array('td'),
2355
				'trim' => 'both',
2356
				'block_level' => true,
2357
				'disabled_before' => '',
2358
				'disabled_after' => '',
2359
			),
2360
			// Legacy (the <tt> element is dead)
2361
			array(
2362
				'tag' => 'tt',
2363
				'before' => '<span class="monospace">',
2364
				'after' => '</span>',
2365
			),
2366
			array(
2367
				'tag' => 'u',
2368
				'before' => '<u>',
2369
				'after' => '</u>',
2370
			),
2371
			array(
2372
				'tag' => 'url',
2373
				'type' => 'unparsed_content',
2374
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2375
				'validate' => function(&$tag, &$data, $disabled)
2376
				{
2377
					$data = strtr($data, array('<br>' => ''));
2378
					$scheme = parse_url($data, PHP_URL_SCHEME);
2379
					if (empty($scheme))
2380
						$data = '//' . ltrim($data, ':/');
2381
				},
2382
			),
2383
			array(
2384
				'tag' => 'url',
2385
				'type' => 'unparsed_equals',
2386
				'quoted' => 'optional',
2387
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2388
				'after' => '</a>',
2389
				'validate' => function(&$tag, &$data, $disabled)
2390
				{
2391
					$scheme = parse_url($data, PHP_URL_SCHEME);
2392
					if (empty($scheme))
2393
						$data = '//' . ltrim($data, ':/');
2394
				},
2395
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2396
				'disabled_after' => ' ($1)',
2397
			),
2398
			// Legacy (alias of [color=white])
2399
			array(
2400
				'tag' => 'white',
2401
				'before' => '<span style="color: white;" class="bbc_color">',
2402
				'after' => '</span>',
2403
			),
2404
			array(
2405
				'tag' => 'youtube',
2406
				'type' => 'unparsed_content',
2407
				'content' => '<div class="videocontainer"><div><iframe frameborder="0" src="https://www.youtube.com/embed/$1?origin=' . $hosturl . '&wmode=opaque" data-youtube-id="$1" allowfullscreen loading="lazy"></iframe></div></div>',
2408
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2409
				'block_level' => true,
2410
			),
2411
		);
2412
2413
		// Inside these tags autolink is not recommendable.
2414
		$no_autolink_tags = array(
2415
			'url',
2416
			'iurl',
2417
			'email',
2418
			'img',
2419
			'html',
2420
		);
2421
2422
		// Let mods add new BBC without hassle.
2423
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2424
2425
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2426
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
2427
		{
2428
			usort($codes, function($a, $b)
2429
			{
2430
				return strcmp($a['tag'], $b['tag']);
2431
			});
2432
			return $codes;
2433
		}
2434
2435
		// So the parser won't skip them.
2436
		$itemcodes = array(
2437
			'*' => 'disc',
2438
			'@' => 'disc',
2439
			'+' => 'square',
2440
			'x' => 'square',
2441
			'#' => 'square',
2442
			'o' => 'circle',
2443
			'O' => 'circle',
2444
			'0' => 'circle',
2445
		);
2446
		if (!isset($disabled['li']) && !isset($disabled['list']))
2447
		{
2448
			foreach ($itemcodes as $c => $dummy)
2449
				$bbc_codes[$c] = array();
2450
		}
2451
2452
		// Shhhh!
2453
		if (!isset($disabled['color']))
2454
		{
2455
			$codes[] = array(
2456
				'tag' => 'chrissy',
2457
				'before' => '<span style="color: #cc0099;">',
2458
				'after' => ' :-*</span>',
2459
			);
2460
			$codes[] = array(
2461
				'tag' => 'kissy',
2462
				'before' => '<span style="color: #cc0099;">',
2463
				'after' => ' :-*</span>',
2464
			);
2465
		}
2466
		$codes[] = array(
2467
			'tag' => 'cowsay',
2468
			'parameters' => array(
2469
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2470
					{
2471
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
2472
					},
2473
				),
2474
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
2475
					{
2476
						return $smcFunc['substr']($tongue . '  ', 0, 2);
2477
					},
2478
				),
2479
			),
2480
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
2481
			'after' => '</div><script>' . '$("head").append("<style>" + ' . JavaScriptEscape(base64_decode('cHJlW2RhdGEtZV1bZGF0YS10XXt3aGl0ZS1zcGFjZTpwcmUtd3JhcDtsaW5lLWhlaWdodDppbml0aWFsO31wcmVbZGF0YS1lXVtkYXRhLXRdID4gZGl2e2Rpc3BsYXk6dGFibGU7Ym9yZGVyOjFweCBzb2xpZDtib3JkZXItcmFkaXVzOjAuNWVtO3BhZGRpbmc6MWNoO21heC13aWR0aDo4MGNoO21pbi13aWR0aDoxMmNoO31wcmVbZGF0YS1lXVtkYXRhLXRdOjphZnRlcntkaXNwbGF5OmlubGluZS1ibG9jazttYXJnaW4tbGVmdDo4Y2g7bWluLXdpZHRoOjIwY2g7ZGlyZWN0aW9uOmx0cjtjb250ZW50OidcNUMgJycgJycgXl9fXlxBICcnIFw1QyAnJyAoJyBhdHRyKGRhdGEtZSkgJylcNUNfX19fX19fXEEgJycgJycgJycgKF9fKVw1QyAnJyAnJyAnJyAnJyAnJyAnJyAnJyApXDVDL1w1Q1xBICcnICcnICcnICcnICcgYXR0cihkYXRhLXQpICcgfHwtLS0tdyB8XEEgJycgJycgJycgJycgJycgJycgJycgfHwgJycgJycgJycgJycgfHwnO30=')) . ' + "</style>");' . '</script></pre>',
2482
			'block_level' => true,
2483
		);
2484
2485
		foreach ($codes as $code)
2486
		{
2487
			// Make it easier to process parameters later
2488
			if (!empty($code['parameters']))
2489
				ksort($code['parameters'], SORT_STRING);
2490
2491
			// If we are not doing every tag only do ones we are interested in.
2492
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2493
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2494
		}
2495
		$codes = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
2496
	}
2497
2498
	// Shall we take the time to cache this?
2499
	if ($cache_id != '' && !empty($cache_enable) && (($cache_enable >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2500
	{
2501
		// It's likely this will change if the message is modified.
2502
		$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']);
2503
2504
		if (($temp = cache_get_data($cache_key, 240)) != null)
2505
			return $temp;
2506
2507
		$cache_t = microtime(true);
2508
	}
2509
2510
	if ($smileys === 'print')
0 ignored issues
show
introduced by
The condition $smileys === 'print' is always false.
Loading history...
2511
	{
2512
		// [glow], [shadow], and [move] can't really be printed.
2513
		$disabled['glow'] = true;
2514
		$disabled['shadow'] = true;
2515
		$disabled['move'] = true;
2516
2517
		// Colors can't well be displayed... supposed to be black and white.
2518
		$disabled['color'] = true;
2519
		$disabled['black'] = true;
2520
		$disabled['blue'] = true;
2521
		$disabled['white'] = true;
2522
		$disabled['red'] = true;
2523
		$disabled['green'] = true;
2524
		$disabled['me'] = true;
2525
2526
		// Color coding doesn't make sense.
2527
		$disabled['php'] = true;
2528
2529
		// Links are useless on paper... just show the link.
2530
		$disabled['ftp'] = true;
2531
		$disabled['url'] = true;
2532
		$disabled['iurl'] = true;
2533
		$disabled['email'] = true;
2534
		$disabled['flash'] = true;
2535
2536
		// @todo Change maybe?
2537
		if (!isset($_GET['images']))
2538
		{
2539
			$disabled['img'] = true;
2540
			$disabled['attach'] = true;
2541
		}
2542
2543
		// Maybe some custom BBC need to be disabled for printing.
2544
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2545
	}
2546
2547
	$open_tags = array();
2548
	$message = strtr($message, array("\n" => '<br>'));
2549
2550
	if (!empty($parse_tags))
2551
	{
2552
		$real_alltags_regex = $alltags_regex;
2553
		$alltags_regex = '';
2554
	}
2555
	if (empty($alltags_regex))
2556
	{
2557
		$alltags = array();
2558
		foreach ($bbc_codes as $section)
2559
		{
2560
			foreach ($section as $code)
2561
				$alltags[] = $code['tag'];
2562
		}
2563
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
0 ignored issues
show
Bug introduced by
Are you sure build_regex(array_keys($itemcodes)) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

2563
		$alltags_regex = '(?' . '>\b' . build_regex(array_unique($alltags)) . '\b|' . /** @scrutinizer ignore-type */ build_regex(array_keys($itemcodes)) . ')';
Loading history...
Bug introduced by
Are you sure build_regex(array_unique($alltags)) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

2563
		$alltags_regex = '(?' . '>\b' . /** @scrutinizer ignore-type */ build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
Loading history...
2564
	}
2565
2566
	$pos = -1;
2567
	while ($pos !== false)
2568
	{
2569
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2570
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2571
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2572
2573
		// Failsafe.
2574
		if ($pos === false || $last_pos > $pos)
2575
			$pos = strlen($message) + 1;
2576
2577
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2578
		if ($last_pos < $pos - 1)
2579
		{
2580
			// Make sure the $last_pos is not negative.
2581
			$last_pos = max($last_pos, 0);
2582
2583
			// Pick a block of data to do some raw fixing on.
2584
			$data = substr($message, $last_pos, $pos - $last_pos);
2585
2586
			$placeholders = array();
2587
			$placeholders_counter = 0;
2588
2589
			// Take care of some HTML!
2590
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2591
			{
2592
				$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);
2593
2594
				// <br> should be empty.
2595
				$empty_tags = array('br', 'hr');
2596
				foreach ($empty_tags as $tag)
2597
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2598
2599
				// b, u, i, s, pre... basic tags.
2600
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2601
				foreach ($closable_tags as $tag)
2602
				{
2603
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2604
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2605
2606
					if ($diff > 0)
2607
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2608
				}
2609
2610
				// Do <img ...> - with security... action= -> action-.
2611
				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);
2612
				if (!empty($matches[0]))
2613
				{
2614
					$replaces = array();
2615
					foreach ($matches[2] as $match => $imgtag)
2616
					{
2617
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2618
2619
						// Remove action= from the URL - no funny business, now.
2620
						// @todo Testing this preg_match seems pointless
2621
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2622
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2623
2624
						$placeholder = '<placeholder ' . ++$placeholders_counter . '>';
2625
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2626
2627
						$replaces[$matches[0][$match]] = $placeholder;
2628
					}
2629
2630
					$data = strtr($data, $replaces);
2631
				}
2632
			}
2633
2634
			if (!empty($modSettings['autoLinkUrls']))
2635
			{
2636
				// Are we inside tags that should be auto linked?
2637
				$no_autolink_area = false;
2638
				if (!empty($open_tags))
2639
				{
2640
					foreach ($open_tags as $open_tag)
2641
						if (in_array($open_tag['tag'], $no_autolink_tags))
2642
							$no_autolink_area = true;
2643
				}
2644
2645
				// Don't go backwards.
2646
				// @todo Don't think is the real solution....
2647
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2648
				if ($pos < $lastAutoPos)
2649
					$no_autolink_area = true;
2650
				$lastAutoPos = $pos;
2651
2652
				if (!$no_autolink_area)
2653
				{
2654
					// An &nbsp; right after a URL can break the autolinker
2655
					if (strpos($data, '&nbsp;') !== false)
2656
					{
2657
						$placeholders['<placeholder non-breaking-space>'] = '&nbsp;';
2658
						$data = strtr($data, array('&nbsp;' => '<placeholder non-breaking-space>'));
2659
					}
2660
2661
					// Parse any URLs
2662
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2663
					{
2664
						// For efficiency, first define the TLD regex in a PCRE subroutine
2665
						$url_regex = '(?(DEFINE)(?<tlds>' . $modSettings['tld_regex'] . '))';
2666
2667
						// Now build the rest of the regex
2668
						$url_regex .=
2669
						// 1. IRI scheme and domain components
2670
						'(?:' .
2671
							// 1a. IRIs with a scheme, or at least an opening "//"
2672
							'(?:' .
2673
2674
								// URI scheme (or lack thereof for schemeless URLs)
2675
								'(?:' .
2676
									// URL scheme and colon
2677
									'\b[a-z][\w\-]+:' .
2678
									// or
2679
									'|' .
2680
									// A boundary followed by two slashes for schemeless URLs
2681
									'(?<=^|\W)(?=//)' .
2682
								')' .
2683
2684
								// IRI "authority" chunk
2685
								'(?:' .
2686
									// 2 slashes for IRIs with an "authority"
2687
									'//' .
2688
									// then a domain name
2689
									'(?:' .
2690
										// Either the reserved "localhost" domain name
2691
										'localhost' .
2692
										// or
2693
										'|' .
2694
										// a run of IRI characters, a dot, and a TLD
2695
										'[\p{L}\p{M}\p{N}\-.:@]+\.(?P>tlds)' .
2696
									')' .
2697
									// followed by a non-domain character or end of line
2698
									'(?=[^\p{L}\p{N}\-.]|$)' .
2699
2700
									// or, if no "authority" per se (e.g. "mailto:" URLs)...
2701
									'|' .
2702
2703
									// a run of IRI characters
2704
									'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.:@]+[\p{L}\p{M}\p{N}]' .
2705
									// and then a dot and a closing IRI label
2706
									'\.[\p{L}\p{M}\p{N}\-]+' .
2707
								')' .
2708
							')' .
2709
2710
							// Or
2711
							'|' .
2712
2713
							// 1b. Naked domains (e.g. "example.com" in "Go to example.com for an example.")
2714
							'(?:' .
2715
								// Preceded by start of line or a non-domain character
2716
								'(?<=^|[^\p{L}\p{M}\p{N}\-:@])' .
2717
								// A run of Unicode domain name characters (excluding [:@])
2718
								'[\p{L}\p{N}][\p{L}\p{M}\p{N}\-.]+[\p{L}\p{M}\p{N}]' .
2719
								// and then a dot and a valid TLD
2720
								'\.(?P>tlds)' .
2721
								// Followed by either:
2722
								'(?=' .
2723
									// end of line or a non-domain character (excluding [.:@])
2724
									'$|[^\p{L}\p{N}\-]' .
2725
									// or
2726
									'|' .
2727
									// a dot followed by end of line or a non-domain character (excluding [.:@])
2728
									'\.(?=$|[^\p{L}\p{N}\-])' .
2729
								')' .
2730
							')' .
2731
						')' .
2732
2733
						// 2. IRI path, query, and fragment components (if present)
2734
						'(?:' .
2735
2736
							// If any of these parts exist, must start with a single "/"
2737
							'/' .
2738
2739
							// And then optionally:
2740
							'(?:' .
2741
								// One or more of:
2742
								'(?:' .
2743
									// a run of non-space, non-()<>
2744
									'[^\s()<>]+' .
2745
									// or
2746
									'|' .
2747
									// balanced parentheses, up to 2 levels
2748
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2749
								')+' .
2750
								// Ending with:
2751
								'(?:' .
2752
									// balanced parentheses, up to 2 levels
2753
									'\(([^\s()<>]+|(\([^\s()<>]+\)))*\)' .
2754
									// or
2755
									'|' .
2756
									// not a space or one of these punctuation characters
2757
									'[^\s`!()\[\]{};:\'".,<>?«»“”‘’/]' .
2758
									// or
2759
									'|' .
2760
									// a trailing slash (but not two in a row)
2761
									'(?<!/)/' .
2762
								')' .
2763
							')?' .
2764
						')?';
2765
2766
						$data = preg_replace_callback('~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''), function($matches)
2767
						{
2768
							$url = array_shift($matches);
2769
2770
							// If this isn't a clean URL, bail out
2771
							if ($url != sanitize_iri($url))
2772
								return $url;
2773
2774
							$scheme = parse_url($url, PHP_URL_SCHEME);
2775
2776
							if ($scheme == 'mailto')
2777
							{
2778
								$email_address = str_replace('mailto:', '', $url);
2779
								if (!isset($disabled['email']) && filter_var($email_address, FILTER_VALIDATE_EMAIL) !== false)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $disabled seems to never exist and therefore isset should always be false.
Loading history...
2780
									return '[email=' . $email_address . ']' . $url . '[/email]';
2781
								else
2782
									return $url;
2783
							}
2784
2785
							// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2786
							if (empty($scheme))
2787
								$fullUrl = '//' . ltrim($url, ':/');
2788
							else
2789
								$fullUrl = $url;
2790
2791
							// Make sure that $fullUrl really is valid
2792
							if (validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false)
2793
								return $url;
2794
2795
							return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2796
						}, $data);
2797
					}
2798
2799
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
2800
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
2801
					{
2802
						$email_regex = '
2803
						# Preceded by a non-domain character or start of line
2804
						(?<=^|[^\p{L}\p{M}\p{N}\-\.])
2805
2806
						# An email address
2807
						[\p{L}\p{M}\p{N}_\-.]{1,80}
2808
						@
2809
						[\p{L}\p{M}\p{N}\-.]+
2810
						\.
2811
						' . $modSettings['tld_regex'] . '
2812
2813
						# Followed by either:
2814
						(?=
2815
							# end of line or a non-domain character (excluding the dot)
2816
							$|[^\p{L}\p{M}\p{N}\-]
2817
							| # or
2818
							# a dot followed by end of line or a non-domain character
2819
							\.(?=$|[^\p{L}\p{M}\p{N}\-])
2820
						)';
2821
2822
						$data = preg_replace('~' . $email_regex . '~xi' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
2823
					}
2824
				}
2825
			}
2826
2827
			// Restore any placeholders
2828
			$data = strtr($data, $placeholders);
2829
2830
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
2831
2832
			// If it wasn't changed, no copying or other boring stuff has to happen!
2833
			if ($data != substr($message, $last_pos, $pos - $last_pos))
2834
			{
2835
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
2836
2837
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
2838
				$old_pos = strlen($data) + $last_pos;
2839
				$pos = strpos($message, '[', $last_pos);
2840
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
2841
			}
2842
		}
2843
2844
		// Are we there yet?  Are we there yet?
2845
		if ($pos >= strlen($message) - 1)
2846
			break;
2847
2848
		$tag_character = strtolower($message[$pos + 1]);
2849
2850
		if ($tag_character == '/' && !empty($open_tags))
2851
		{
2852
			$pos2 = strpos($message, ']', $pos + 1);
2853
			if ($pos2 == $pos + 2)
2854
				continue;
2855
2856
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
2857
2858
			// A closing tag that doesn't match any open tags? Skip it.
2859
			if (!in_array($look_for, array_map(function($code)
2860
			{
2861
				return $code['tag'];
2862
			}, $open_tags)))
2863
				continue;
2864
2865
			$to_close = array();
2866
			$block_level = null;
2867
2868
			do
2869
			{
2870
				$tag = array_pop($open_tags);
2871
				if (!$tag)
2872
					break;
2873
2874
				if (!empty($tag['block_level']))
2875
				{
2876
					// Only find out if we need to.
2877
					if ($block_level === false)
2878
					{
2879
						array_push($open_tags, $tag);
2880
						break;
2881
					}
2882
2883
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
2884
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
2885
					{
2886
						foreach ($bbc_codes[$look_for[0]] as $temp)
2887
							if ($temp['tag'] == $look_for)
2888
							{
2889
								$block_level = !empty($temp['block_level']);
2890
								break;
2891
							}
2892
					}
2893
2894
					if ($block_level !== true)
2895
					{
2896
						$block_level = false;
2897
						array_push($open_tags, $tag);
2898
						break;
2899
					}
2900
				}
2901
2902
				$to_close[] = $tag;
2903
			}
2904
			while ($tag['tag'] != $look_for);
2905
2906
			// Did we just eat through everything and not find it?
2907
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
2908
			{
2909
				$open_tags = $to_close;
2910
				continue;
2911
			}
2912
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
2913
			{
2914
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
2915
				{
2916
					foreach ($bbc_codes[$look_for[0]] as $temp)
2917
						if ($temp['tag'] == $look_for)
2918
						{
2919
							$block_level = !empty($temp['block_level']);
2920
							break;
2921
						}
2922
				}
2923
2924
				// We're not looking for a block level tag (or maybe even a tag that exists...)
2925
				if (!$block_level)
0 ignored issues
show
Bug Best Practice introduced by
The expression $block_level of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
2926
				{
2927
					foreach ($to_close as $tag)
2928
						array_push($open_tags, $tag);
2929
					continue;
2930
				}
2931
			}
2932
2933
			foreach ($to_close as $tag)
2934
			{
2935
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
2936
				$pos += strlen($tag['after']) + 2;
2937
				$pos2 = $pos - 1;
2938
2939
				// See the comment at the end of the big loop - just eating whitespace ;).
2940
				$whitespace_regex = '';
2941
				if (!empty($tag['block_level']))
2942
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
2943
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
2944
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
2945
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
2946
2947
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
2948
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
2949
			}
2950
2951
			if (!empty($to_close))
2952
			{
2953
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
2954
				$pos--;
2955
			}
2956
2957
			continue;
2958
		}
2959
2960
		// No tags for this character, so just keep going (fastest possible course.)
2961
		if (!isset($bbc_codes[$tag_character]))
2962
			continue;
2963
2964
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
2965
		$tag = null;
2966
		foreach ($bbc_codes[$tag_character] as $possible)
2967
		{
2968
			$pt_strlen = strlen($possible['tag']);
2969
2970
			// Not a match?
2971
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
2972
				continue;
2973
2974
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
2975
2976
			// A tag is the last char maybe
2977
			if ($next_c == '')
2978
				break;
2979
2980
			// A test validation?
2981
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
2982
				continue;
2983
			// Do we want parameters?
2984
			elseif (!empty($possible['parameters']))
2985
			{
2986
				// Are all the parameters optional?
2987
				$param_required = false;
2988
				foreach ($possible['parameters'] as $param)
2989
				{
2990
					if (empty($param['optional']))
2991
					{
2992
						$param_required = true;
2993
						break;
2994
					}
2995
				}
2996
2997
				if ($param_required && $next_c != ' ')
2998
					continue;
2999
			}
3000
			elseif (isset($possible['type']))
3001
			{
3002
				// Do we need an equal sign?
3003
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
3004
					continue;
3005
				// Maybe we just want a /...
3006
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
3007
					continue;
3008
				// An immediate ]?
3009
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
3010
					continue;
3011
			}
3012
			// No type means 'parsed_content', which demands an immediate ] without parameters!
3013
			elseif ($next_c != ']')
3014
				continue;
3015
3016
			// Check allowed tree?
3017
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
3018
				continue;
3019
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
3020
				continue;
3021
			// If this is in the list of disallowed child tags, don't parse it.
3022
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
3023
				continue;
3024
3025
			$pos1 = $pos + 1 + $pt_strlen + 1;
3026
3027
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
3028
			if ($possible['tag'] == 'quote')
3029
			{
3030
				// Start with standard
3031
				$quote_alt = false;
3032
				foreach ($open_tags as $open_quote)
3033
				{
3034
					// Every parent quote this quote has flips the styling
3035
					if ($open_quote['tag'] == 'quote')
3036
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
3037
				}
3038
				// Add a class to the quote to style alternating blockquotes
3039
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
3040
			}
3041
3042
			// This is long, but it makes things much easier and cleaner.
3043
			if (!empty($possible['parameters']))
3044
			{
3045
				// Build a regular expression for each parameter for the current tag.
3046
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
3047
				if (!isset($params_regexes[$regex_key]))
3048
				{
3049
					$params_regexes[$regex_key] = '';
3050
3051
					foreach ($possible['parameters'] as $p => $info)
3052
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
3053
				}
3054
3055
				// Extract the string that potentially holds our parameters.
3056
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
3057
				$blobs = preg_split('~\]~i', $blob[1]);
3058
3059
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
3060
3061
				// Progressively append more blobs until we find our parameters or run out of blobs
3062
				$blob_counter = 1;
3063
				while ($blob_counter <= count($blobs))
3064
				{
3065
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
3066
3067
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
3068
					sort($given_params, SORT_STRING);
3069
3070
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
3071
3072
					if ($match)
3073
						break;
3074
				}
3075
3076
				// Didn't match our parameter list, try the next possible.
3077
				if (!$match)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $match does not seem to be defined for all execution paths leading up to this point.
Loading history...
3078
					continue;
3079
3080
				$params = array();
3081
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
3082
				{
3083
					$key = strtok(ltrim($matches[$i]), '=');
3084
					if ($key === false)
3085
						continue;
3086
					elseif (isset($possible['parameters'][$key]['value']))
3087
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
3088
					elseif (isset($possible['parameters'][$key]['validate']))
3089
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
3090
					else
3091
						$params['{' . $key . '}'] = $matches[$i + 1];
3092
3093
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
3094
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
3095
				}
3096
3097
				foreach ($possible['parameters'] as $p => $info)
3098
				{
3099
					if (!isset($params['{' . $p . '}']))
3100
					{
3101
						if (!isset($info['default']))
3102
							$params['{' . $p . '}'] = '';
3103
						elseif (isset($possible['parameters'][$p]['value']))
3104
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
3105
						elseif (isset($possible['parameters'][$p]['validate']))
3106
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
3107
						else
3108
							$params['{' . $p . '}'] = $info['default'];
3109
					}
3110
				}
3111
3112
				$tag = $possible;
3113
3114
				// Put the parameters into the string.
3115
				if (isset($tag['before']))
3116
					$tag['before'] = strtr($tag['before'], $params);
3117
				if (isset($tag['after']))
3118
					$tag['after'] = strtr($tag['after'], $params);
3119
				if (isset($tag['content']))
3120
					$tag['content'] = strtr($tag['content'], $params);
3121
3122
				$pos1 += strlen($given_param_string);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $given_param_string does not seem to be defined for all execution paths leading up to this point.
Loading history...
3123
			}
3124
			else
3125
			{
3126
				$tag = $possible;
3127
				$params = array();
3128
			}
3129
			break;
3130
		}
3131
3132
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
3133
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
3134
		{
3135
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
3136
				continue;
3137
3138
			$tag = $itemcodes[$message[$pos + 1]];
3139
3140
			// First let's set up the tree: it needs to be in a list, or after an li.
3141
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
3142
			{
3143
				$open_tags[] = array(
3144
					'tag' => 'list',
3145
					'after' => '</ul>',
3146
					'block_level' => true,
3147
					'require_children' => array('li'),
3148
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3149
				);
3150
				$code = '<ul class="bbc_list">';
3151
			}
3152
			// We're in a list item already: another itemcode?  Close it first.
3153
			elseif ($inside['tag'] == 'li')
3154
			{
3155
				array_pop($open_tags);
3156
				$code = '</li>';
3157
			}
3158
			else
3159
				$code = '';
3160
3161
			// Now we open a new tag.
3162
			$open_tags[] = array(
3163
				'tag' => 'li',
3164
				'after' => '</li>',
3165
				'trim' => 'outside',
3166
				'block_level' => true,
3167
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3168
			);
3169
3170
			// First, open the tag...
3171
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
3172
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
3173
			$pos += strlen($code) - 1 + 2;
3174
3175
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
3176
			$pos2 = strpos($message, '<br>', $pos);
3177
			$pos3 = strpos($message, '[/', $pos);
3178
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
3179
			{
3180
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
3181
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
3182
3183
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
3184
			}
3185
			// Tell the [list] that it needs to close specially.
3186
			else
3187
			{
3188
				// Move the li over, because we're not sure what we'll hit.
3189
				$open_tags[count($open_tags) - 1]['after'] = '';
3190
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
3191
			}
3192
3193
			continue;
3194
		}
3195
3196
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
3197
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
3198
		{
3199
			array_pop($open_tags);
3200
3201
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
3202
			$pos += strlen($inside['after']) - 1 + 2;
3203
		}
3204
3205
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
3206
		if ($tag === null)
3207
			continue;
3208
3209
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
3210
		if (isset($inside['disallow_children']))
3211
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
3212
3213
		// Is this tag disabled?
3214
		if (isset($disabled[$tag['tag']]))
3215
		{
3216
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
3217
			{
3218
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
3219
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
3220
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
3221
			}
3222
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
3223
			{
3224
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
3225
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
3226
			}
3227
			else
3228
				$tag['content'] = $tag['disabled_content'];
3229
		}
3230
3231
		// we use this a lot
3232
		$tag_strlen = strlen($tag['tag']);
3233
3234
		// The only special case is 'html', which doesn't need to close things.
3235
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
3236
		{
3237
			$n = count($open_tags) - 1;
3238
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
3239
				$n--;
3240
3241
			// Close all the non block level tags so this tag isn't surrounded by them.
3242
			for ($i = count($open_tags) - 1; $i > $n; $i--)
3243
			{
3244
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
3245
				$ot_strlen = strlen($open_tags[$i]['after']);
3246
				$pos += $ot_strlen + 2;
3247
				$pos1 += $ot_strlen + 2;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $pos1 does not seem to be defined for all execution paths leading up to this point.
Loading history...
3248
3249
				// Trim or eat trailing stuff... see comment at the end of the big loop.
3250
				$whitespace_regex = '';
3251
				if (!empty($tag['block_level']))
3252
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
3253
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3254
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3255
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3256
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3257
3258
				array_pop($open_tags);
3259
			}
3260
		}
3261
3262
		// Can't read past the end of the message
3263
		$pos1 = min(strlen($message), $pos1);
3264
3265
		// No type means 'parsed_content'.
3266
		if (!isset($tag['type']))
3267
		{
3268
			$open_tags[] = $tag;
3269
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
3270
			$pos += strlen($tag['before']) - 1 + 2;
3271
		}
3272
		// Don't parse the content, just skip it.
3273
		elseif ($tag['type'] == 'unparsed_content')
3274
		{
3275
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
3276
			if ($pos2 === false)
3277
				continue;
3278
3279
			$data = substr($message, $pos1, $pos2 - $pos1);
3280
3281
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
3282
				$data = substr($data, 4);
3283
3284
			if (isset($tag['validate']))
3285
				$tag['validate']($tag, $data, $disabled, $params);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $params does not seem to be defined for all execution paths leading up to this point.
Loading history...
3286
3287
			$code = strtr($tag['content'], array('$1' => $data));
3288
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
3289
3290
			$pos += strlen($code) - 1 + 2;
3291
			$last_pos = $pos + 1;
3292
		}
3293
		// Don't parse the content, just skip it.
3294
		elseif ($tag['type'] == 'unparsed_equals_content')
3295
		{
3296
			// The value may be quoted for some tags - check.
3297
			if (isset($tag['quoted']))
3298
			{
3299
				$quoted = substr($message, $pos1, 6) == '&quot;';
3300
				if ($tag['quoted'] != 'optional' && !$quoted)
3301
					continue;
3302
3303
				if ($quoted)
3304
					$pos1 += 6;
3305
			}
3306
			else
3307
				$quoted = false;
3308
3309
			$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1);
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
3310
			if ($pos2 === false)
3311
				continue;
3312
3313
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3314
			if ($pos3 === false)
3315
				continue;
3316
3317
			$data = array(
3318
				substr($message, $pos2 + ($quoted == false ? 1 : 7), $pos3 - ($pos2 + ($quoted == false ? 1 : 7))),
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
3319
				substr($message, $pos1, $pos2 - $pos1)
3320
			);
3321
3322
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3323
				$data[0] = substr($data[0], 4);
3324
3325
			// Validation for my parking, please!
3326
			if (isset($tag['validate']))
3327
				$tag['validate']($tag, $data, $disabled, $params);
3328
3329
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3330
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3331
			$pos += strlen($code) - 1 + 2;
3332
		}
3333
		// A closed tag, with no content or value.
3334
		elseif ($tag['type'] == 'closed')
3335
		{
3336
			$pos2 = strpos($message, ']', $pos);
3337
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3338
			$pos += strlen($tag['content']) - 1 + 2;
3339
		}
3340
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3341
		elseif ($tag['type'] == 'unparsed_commas_content')
3342
		{
3343
			$pos2 = strpos($message, ']', $pos1);
3344
			if ($pos2 === false)
3345
				continue;
3346
3347
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3348
			if ($pos3 === false)
3349
				continue;
3350
3351
			// We want $1 to be the content, and the rest to be csv.
3352
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3353
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3354
3355
			if (isset($tag['validate']))
3356
				$tag['validate']($tag, $data, $disabled, $params);
3357
3358
			$code = $tag['content'];
3359
			foreach ($data as $k => $d)
3360
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3361
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3362
			$pos += strlen($code) - 1 + 2;
3363
		}
3364
		// This has parsed content, and a csv value which is unparsed.
3365
		elseif ($tag['type'] == 'unparsed_commas')
3366
		{
3367
			$pos2 = strpos($message, ']', $pos1);
3368
			if ($pos2 === false)
3369
				continue;
3370
3371
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3372
3373
			if (isset($tag['validate']))
3374
				$tag['validate']($tag, $data, $disabled, $params);
3375
3376
			// Fix after, for disabled code mainly.
3377
			foreach ($data as $k => $d)
3378
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3379
3380
			$open_tags[] = $tag;
3381
3382
			// Replace them out, $1, $2, $3, $4, etc.
3383
			$code = $tag['before'];
3384
			foreach ($data as $k => $d)
3385
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3386
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3387
			$pos += strlen($code) - 1 + 2;
3388
		}
3389
		// A tag set to a value, parsed or not.
3390
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3391
		{
3392
			// The value may be quoted for some tags - check.
3393
			if (isset($tag['quoted']))
3394
			{
3395
				$quoted = substr($message, $pos1, 6) == '&quot;';
3396
				if ($tag['quoted'] != 'optional' && !$quoted)
3397
					continue;
3398
3399
				if ($quoted)
3400
					$pos1 += 6;
3401
			}
3402
			else
3403
				$quoted = false;
3404
3405
			if ($quoted)
3406
			{
3407
				$end_of_value = strpos($message, '&quot;]', $pos1);
3408
				$nested_tag = strpos($message, '=&quot;', $pos1);
3409
				if ($nested_tag && $nested_tag < $end_of_value)
3410
					// Nested tag with quoted value detected, use next end tag
3411
					$nested_tag_pos = strpos($message, $quoted == false ? ']' : '&quot;]', $pos1) + 6;
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
3412
			}
3413
3414
			$pos2 = strpos($message, $quoted == false ? ']' : '&quot;]', isset($nested_tag_pos) ? $nested_tag_pos : $pos1);
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
3415
			if ($pos2 === false)
3416
				continue;
3417
3418
			$data = substr($message, $pos1, $pos2 - $pos1);
3419
3420
			// Validation for my parking, please!
3421
			if (isset($tag['validate']))
3422
				$tag['validate']($tag, $data, $disabled, $params);
3423
3424
			// For parsed content, we must recurse to avoid security problems.
3425
			if ($tag['type'] != 'unparsed_equals')
3426
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3427
3428
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3429
3430
			$open_tags[] = $tag;
3431
3432
			$code = strtr($tag['before'], array('$1' => $data));
3433
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 7));
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
3434
			$pos += strlen($code) - 1 + 2;
3435
		}
3436
3437
		// If this is block level, eat any breaks after it.
3438
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3439
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3440
3441
		// Are we trimming outside this tag?
3442
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3443
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3444
	}
3445
3446
	// Close any remaining tags.
3447
	while ($tag = array_pop($open_tags))
3448
		$message .= "\n" . $tag['after'] . "\n";
3449
3450
	// Parse the smileys within the parts where it can be done safely.
3451
	if ($smileys === true)
3452
	{
3453
		$message_parts = explode("\n", $message);
3454
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3455
			parsesmileys($message_parts[$i]);
3456
3457
		$message = implode('', $message_parts);
3458
	}
3459
3460
	// No smileys, just get rid of the markers.
3461
	else
3462
		$message = strtr($message, array("\n" => ''));
3463
3464
	if ($message !== '' && $message[0] === ' ')
3465
		$message = '&nbsp;' . substr($message, 1);
3466
3467
	// Cleanup whitespace.
3468
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3469
3470
	// Allow mods access to what parse_bbc created
3471
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3472
3473
	// Cache the output if it took some time...
3474
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3475
		cache_put_data($cache_key, $message, 240);
3476
3477
	// If this was a force parse revert if needed.
3478
	if (!empty($parse_tags))
3479
	{
3480
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3481
		unset($real_alltags_regex);
3482
	}
3483
	elseif (!empty($bbc_codes))
3484
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3485
3486
	return $message;
3487
}
3488
3489
/**
3490
 * Parse smileys in the passed message.
3491
 *
3492
 * The smiley parsing function which makes pretty faces appear :).
3493
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3494
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3495
 * Caches the smileys from the database or array in memory.
3496
 * Doesn't return anything, but rather modifies message directly.
3497
 *
3498
 * @param string &$message The message to parse smileys in
3499
 */
3500
function parsesmileys(&$message)
3501
{
3502
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3503
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3504
3505
	// No smiley set at all?!
3506
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3507
		return;
3508
3509
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3510
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3511
3512
	// If smileyPregSearch hasn't been set, do it now.
3513
	if (empty($smileyPregSearch))
3514
	{
3515
		// Cache for longer when customized smiley codes aren't enabled
3516
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3517
3518
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3519
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3520
		{
3521
			$result = $smcFunc['db_query']('', '
3522
				SELECT s.code, f.filename, s.description
3523
				FROM {db_prefix}smileys AS s
3524
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3525
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3526
					AND s.code IN ({array_string:default_codes})' : '') . '
3527
				ORDER BY LENGTH(s.code) DESC',
3528
				array(
3529
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3530
					'smiley_set' => $user_info['smiley_set'],
3531
				)
3532
			);
3533
			$smileysfrom = array();
3534
			$smileysto = array();
3535
			$smileysdescs = array();
3536
			while ($row = $smcFunc['db_fetch_assoc']($result))
3537
			{
3538
				$smileysfrom[] = $row['code'];
3539
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3540
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3541
			}
3542
			$smcFunc['db_free_result']($result);
3543
3544
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3545
		}
3546
		else
3547
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3548
3549
		// The non-breaking-space is a complex thing...
3550
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3551
3552
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3553
		$smileyPregReplacements = array();
3554
		$searchParts = array();
3555
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3556
3557
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3558
		{
3559
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3560
			$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">';
3561
3562
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3563
3564
			$searchParts[] = $smileysfrom[$i];
3565
			if ($smileysfrom[$i] != $specialChars)
3566
			{
3567
				$smileyPregReplacements[$specialChars] = $smileyCode;
3568
				$searchParts[] = $specialChars;
3569
3570
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3571
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3572
				if ($specialChars2 != $specialChars)
3573
				{
3574
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3575
					$searchParts[] = $specialChars2;
3576
				}
3577
			}
3578
		}
3579
3580
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
0 ignored issues
show
Bug introduced by
Are you sure build_regex($searchParts, '~') of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

3580
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . /** @scrutinizer ignore-type */ build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
Loading history...
3581
	}
3582
3583
	// Replace away!
3584
	$message = preg_replace_callback($smileyPregSearch, function($matches) use ($smileyPregReplacements)
3585
		{
3586
			return $smileyPregReplacements[$matches[1]];
3587
		}, $message);
3588
}
3589
3590
/**
3591
 * Highlight any code.
3592
 *
3593
 * Uses PHP's highlight_string() to highlight PHP syntax
3594
 * does special handling to keep the tabs in the code available.
3595
 * used to parse PHP code from inside [code] and [php] tags.
3596
 *
3597
 * @param string $code The code
3598
 * @return string The code with highlighted HTML.
3599
 */
3600
function highlight_php_code($code)
3601
{
3602
	// Remove special characters.
3603
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3604
3605
	$oldlevel = error_reporting(0);
3606
3607
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3608
3609
	error_reporting($oldlevel);
3610
3611
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3612
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3613
3614
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3615
}
3616
3617
/**
3618
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3619
 *
3620
 * The returned URL may or may not be a proxied URL, depending on the situation.
3621
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3622
 *
3623
 * @param string $url The original URL of the requested resource
3624
 * @return string The URL to use
3625
 */
3626
function get_proxied_url($url)
3627
{
3628
	global $boardurl, $image_proxy_enabled, $image_proxy_secret, $user_info;
3629
3630
	// Only use the proxy if enabled, and never for robots
3631
	if (empty($image_proxy_enabled) || !empty($user_info['possibly_robot']))
3632
		return $url;
3633
3634
	$parsedurl = parse_url($url);
3635
3636
	// Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs
3637
	if (empty($parsedurl['scheme']) || empty($parsedurl['host']) || empty($parsedurl['path']) || $parsedurl['scheme'] === 'https')
3638
		return $url;
3639
3640
	// We don't need to proxy our own resources
3641
	if ($parsedurl['host'] === parse_url($boardurl, PHP_URL_HOST))
3642
		return strtr($url, array('http://' => 'https://'));
3643
3644
	// By default, use SMF's own image proxy script
3645
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . hash_hmac('sha1', $url, $image_proxy_secret);
3646
3647
	// Allow mods to easily implement an alternative proxy
3648
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
3649
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
3650
3651
	return $proxied_url;
3652
}
3653
3654
/**
3655
 * Make sure the browser doesn't come back and repost the form data.
3656
 * Should be used whenever anything is posted.
3657
 *
3658
 * @param string $setLocation The URL to redirect them to
3659
 * @param bool $refresh Whether to use a meta refresh instead
3660
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
3661
 */
3662
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
3663
{
3664
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
3665
3666
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
3667
	if (!empty($context['flush_mail']))
3668
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3669
		AddMailQueue(true);
3670
3671
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
3672
3673
	if ($add)
3674
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
3675
3676
	// Put the session ID in.
3677
	if (defined('SID') && SID != '')
3678
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
3679
	// Keep that debug in their for template debugging!
3680
	elseif (isset($_GET['debug']))
3681
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
3682
3683
	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'])))
3684
	{
3685
		if (defined('SID') && SID != '')
3686
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
3687
				function($m) use ($scripturl)
3688
				{
3689
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
3690
				}, $setLocation);
3691
		else
3692
			$setLocation = preg_replace_callback('~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
3693
				function($m) use ($scripturl)
3694
				{
3695
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
3696
				}, $setLocation);
3697
	}
3698
3699
	// Maybe integrations want to change where we are heading?
3700
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
3701
3702
	// Set the header.
3703
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
3704
3705
	// Debugging.
3706
	if (isset($db_show_debug) && $db_show_debug === true)
3707
		$_SESSION['debug_redirect'] = $db_cache;
3708
3709
	obExit(false);
3710
}
3711
3712
/**
3713
 * Ends execution.  Takes care of template loading and remembering the previous URL.
3714
 *
3715
 * @param bool $header Whether to do the header
3716
 * @param bool $do_footer Whether to do the footer
3717
 * @param bool $from_index Whether we're coming from the board index
3718
 * @param bool $from_fatal_error Whether we're coming from a fatal error
3719
 */
3720
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
3721
{
3722
	global $context, $settings, $modSettings, $txt, $smcFunc, $should_log;
3723
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
3724
3725
	// Attempt to prevent a recursive loop.
3726
	++$level;
3727
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
3728
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

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

Loading history...
3729
	if ($from_fatal_error)
3730
		$has_fatal_error = true;
3731
3732
	// Clear out the stat cache.
3733
	if (function_exists('trackStats'))
3734
		trackStats();
3735
3736
	// If we have mail to send, send it.
3737
	if (function_exists('AddMailQueue') && !empty($context['flush_mail']))
3738
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3739
		AddMailQueue(true);
3740
3741
	$do_header = $header === null ? !$header_done : $header;
3742
	if ($do_footer === null)
3743
		$do_footer = $do_header;
3744
3745
	// Has the template/header been done yet?
3746
	if ($do_header)
3747
	{
3748
		// Was the page title set last minute? Also update the HTML safe one.
3749
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
3750
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3751
3752
		// Start up the session URL fixer.
3753
		ob_start('ob_sessrewrite');
3754
3755
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
3756
			$buffers = explode(',', $settings['output_buffers']);
3757
		elseif (!empty($settings['output_buffers']))
3758
			$buffers = $settings['output_buffers'];
3759
		else
3760
			$buffers = array();
3761
3762
		if (isset($modSettings['integrate_buffer']))
3763
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
3764
3765
		if (!empty($buffers))
3766
			foreach ($buffers as $function)
3767
			{
3768
				$call = call_helper($function, true);
3769
3770
				// Is it valid?
3771
				if (!empty($call))
3772
					ob_start($call);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $callback of ob_start() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

3772
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
3773
			}
3774
3775
		// Display the screen in the logical order.
3776
		template_header();
3777
		$header_done = true;
3778
	}
3779
	if ($do_footer)
3780
	{
3781
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
3782
3783
		// Anything special to put out?
3784
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
3785
			echo $context['insert_after_template'];
3786
3787
		// Just so we don't get caught in an endless loop of errors from the footer...
3788
		if (!$footer_done)
3789
		{
3790
			$footer_done = true;
3791
			template_footer();
3792
3793
			// (since this is just debugging... it's okay that it's after </html>.)
3794
			if (!isset($_REQUEST['xml']))
3795
				displayDebug();
3796
		}
3797
	}
3798
3799
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
3800
	if ($should_log)
3801
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
3802
3803
	// For session check verification.... don't switch browsers...
3804
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
3805
3806
	// Hand off the output to the portal, etc. we're integrated with.
3807
	call_integration_hook('integrate_exit', array($do_footer));
3808
3809
	// Don't exit if we're coming from index.php; that will pass through normally.
3810
	if (!$from_index)
3811
		exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

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

Loading history...
3812
}
3813
3814
/**
3815
 * Get the size of a specified image with better error handling.
3816
 *
3817
 * @todo see if it's better in Subs-Graphics, but one step at the time.
3818
 * Uses getimagesize() to determine the size of a file.
3819
 * Attempts to connect to the server first so it won't time out.
3820
 *
3821
 * @param string $url The URL of the image
3822
 * @return array|false The image size as array (width, height), or false on failure
3823
 */
3824
function url_image_size($url)
3825
{
3826
	global $sourcedir;
3827
3828
	// Make sure it is a proper URL.
3829
	$url = str_replace(' ', '%20', $url);
3830
3831
	// Can we pull this from the cache... please please?
3832
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
3833
		return $temp;
3834
	$t = microtime(true);
3835
3836
	// Get the host to pester...
3837
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
3838
3839
	// Can't figure it out, just try the image size.
3840
	if ($url == '' || $url == 'http://' || $url == 'https://')
3841
	{
3842
		return false;
3843
	}
3844
	elseif (!isset($match[1]))
3845
	{
3846
		$size = @getimagesize($url);
3847
	}
3848
	else
3849
	{
3850
		// Try to connect to the server... give it half a second.
3851
		$temp = 0;
3852
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
3853
3854
		// Successful?  Continue...
3855
		if ($fp != false)
3856
		{
3857
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
3858
			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");
3859
3860
			// Read in the HTTP/1.1 or whatever.
3861
			$test = substr(fgets($fp, 11), -1);
3862
			fclose($fp);
3863
3864
			// See if it returned a 404/403 or something.
3865
			if ($test < 4)
3866
			{
3867
				$size = @getimagesize($url);
3868
3869
				// This probably means allow_url_fopen is off, let's try GD.
3870
				if ($size === false && function_exists('imagecreatefromstring'))
3871
				{
3872
					// It's going to hate us for doing this, but another request...
3873
					$image = @imagecreatefromstring(fetch_web_data($url));
0 ignored issues
show
Bug introduced by
It seems like fetch_web_data($url) can also be of type false; however, parameter $image of imagecreatefromstring() does only seem to accept string, 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

3873
					$image = @imagecreatefromstring(/** @scrutinizer ignore-type */ fetch_web_data($url));
Loading history...
3874
					if ($image !== false)
3875
					{
3876
						$size = array(imagesx($image), imagesy($image));
3877
						imagedestroy($image);
3878
					}
3879
				}
3880
			}
3881
		}
3882
	}
3883
3884
	// If we didn't get it, we failed.
3885
	if (!isset($size))
3886
		$size = false;
3887
3888
	// If this took a long time, we may never have to do it again, but then again we might...
3889
	if (microtime(true) - $t > 0.8)
3890
		cache_put_data('url_image_size-' . md5($url), $size, 240);
3891
3892
	// Didn't work.
3893
	return $size;
3894
}
3895
3896
/**
3897
 * Sets up the basic theme context stuff.
3898
 *
3899
 * @param bool $forceload Whether to load the theme even if it's already loaded
3900
 */
3901
function setupThemeContext($forceload = false)
3902
{
3903
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
3904
	global $smcFunc;
3905
	static $loaded = false;
3906
3907
	// Under SSI this function can be called more then once.  That can cause some problems.
3908
	//   So only run the function once unless we are forced to run it again.
3909
	if ($loaded && !$forceload)
3910
		return;
3911
3912
	$loaded = true;
3913
3914
	$context['in_maintenance'] = !empty($maintenance);
3915
	$context['current_time'] = timeformat(time(), false);
3916
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
3917
	$context['random_news_line'] = array();
3918
3919
	// Get some news...
3920
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
3921
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
3922
	{
3923
		if (trim($context['news_lines'][$i]) == '')
3924
			continue;
3925
3926
		// Clean it up for presentation ;).
3927
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
3928
	}
3929
3930
	if (!empty($context['news_lines']) && (!empty($modSettings['allow_guestAccess']) || $context['user']['is_logged']))
3931
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
3932
3933
	if (!$user_info['is_guest'])
3934
	{
3935
		$context['user']['messages'] = &$user_info['messages'];
3936
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
3937
		$context['user']['alerts'] = &$user_info['alerts'];
3938
3939
		// Personal message popup...
3940
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
3941
			$context['user']['popup_messages'] = true;
3942
		else
3943
			$context['user']['popup_messages'] = false;
3944
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
3945
3946
		if (allowedTo('moderate_forum'))
3947
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
3948
3949
		$context['user']['avatar'] = array();
3950
3951
		// Check for gravatar first since we might be forcing them...
3952
		if (!empty($modSettings['gravatarEnabled']) && (substr($user_info['avatar']['url'], 0, 11) == 'gravatar://' || !empty($modSettings['gravatarOverride'])))
3953
		{
3954
			if (!empty($modSettings['gravatarAllowExtraEmail']) && stristr($user_info['avatar']['url'], 'gravatar://') && strlen($user_info['avatar']['url']) > 11)
3955
				$context['user']['avatar']['href'] = get_gravatar_url($smcFunc['substr']($user_info['avatar']['url'], 11));
3956
			else
3957
				$context['user']['avatar']['href'] = get_gravatar_url($user_info['email']);
3958
		}
3959
		// Uploaded?
3960
		elseif ($user_info['avatar']['url'] == '' && !empty($user_info['avatar']['id_attach']))
3961
			$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';
3962
		// Full URL?
3963
		elseif (strpos($user_info['avatar']['url'], 'http://') === 0 || strpos($user_info['avatar']['url'], 'https://') === 0)
3964
			$context['user']['avatar']['href'] = $user_info['avatar']['url'];
3965
		// Otherwise we assume it's server stored.
3966
		elseif ($user_info['avatar']['url'] != '')
3967
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/' . $smcFunc['htmlspecialchars']($user_info['avatar']['url']);
3968
		// No avatar at all? Fine, we have a big fat default avatar ;)
3969
		else
3970
			$context['user']['avatar']['href'] = $modSettings['avatar_url'] . '/default.png';
3971
3972
		if (!empty($context['user']['avatar']))
3973
			$context['user']['avatar']['image'] = '<img src="' . $context['user']['avatar']['href'] . '" alt="" class="avatar">';
3974
3975
		// Figure out how long they've been logged in.
3976
		$context['user']['total_time_logged_in'] = array(
3977
			'days' => floor($user_info['total_time_logged_in'] / 86400),
3978
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
3979
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
3980
		);
3981
	}
3982
	else
3983
	{
3984
		$context['user']['messages'] = 0;
3985
		$context['user']['unread_messages'] = 0;
3986
		$context['user']['avatar'] = array();
3987
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
3988
		$context['user']['popup_messages'] = false;
3989
3990
		// If we've upgraded recently, go easy on the passwords.
3991
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
3992
			$context['disable_login_hashing'] = true;
3993
	}
3994
3995
	// Setup the main menu items.
3996
	setupMenuContext();
3997
3998
	// This is here because old index templates might still use it.
3999
	$context['show_news'] = !empty($settings['enable_news']);
4000
4001
	// This is done to allow theme authors to customize it as they want.
4002
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
4003
4004
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
4005
	if ($context['show_pm_popup'])
4006
		addInlineJavaScript('
4007
		jQuery(document).ready(function($) {
4008
			new smc_Popup({
4009
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
4010
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
4011
				icon_class: \'main_icons mail_new\'
4012
			});
4013
		});');
4014
4015
	// Add a generic "Are you sure?" confirmation message.
4016
	addInlineJavaScript('
4017
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
4018
4019
	// Now add the capping code for avatars.
4020
	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')
4021
		addInlineCss('
4022
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px; max-height: ' . $modSettings['avatar_max_height_external'] . 'px; }');
4023
4024
	// Add max image limits
4025
	if (!empty($modSettings['max_image_width']))
4026
		addInlineCss('
4027
	.postarea .bbc_img, .list_posts .bbc_img, .post .inner .bbc_img, form#reported_posts .bbc_img, #preview_body .bbc_img { max-width: ' . $modSettings['max_image_width'] . 'px; }');
4028
4029
	if (!empty($modSettings['max_image_height']))
4030
		addInlineCss('
4031
	.postarea .bbc_img, .list_posts .bbc_img, .post .inner .bbc_img, form#reported_posts .bbc_img, #preview_body .bbc_img { max-height: ' . $modSettings['max_image_height'] . 'px; }');
4032
4033
	// This looks weird, but it's because BoardIndex.php references the variable.
4034
	$context['common_stats']['latest_member'] = array(
4035
		'id' => $modSettings['latestMember'],
4036
		'name' => $modSettings['latestRealName'],
4037
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
4038
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
4039
	);
4040
	$context['common_stats'] = array(
4041
		'total_posts' => comma_format($modSettings['totalMessages']),
4042
		'total_topics' => comma_format($modSettings['totalTopics']),
4043
		'total_members' => comma_format($modSettings['totalMembers']),
4044
		'latest_member' => $context['common_stats']['latest_member'],
4045
	);
4046
	$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']);
4047
4048
	if (empty($settings['theme_version']))
4049
		addJavaScriptVar('smf_scripturl', $scripturl);
4050
4051
	if (!isset($context['page_title']))
4052
		$context['page_title'] = '';
4053
4054
	// Set some specific vars.
4055
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](un_htmlspecialchars($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
4056
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
4057
4058
	// Content related meta tags, including Open Graph
4059
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
4060
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
4061
4062
	if (!empty($context['meta_keywords']))
4063
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
4064
4065
	if (!empty($context['canonical_url']))
4066
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
4067
4068
	if (!empty($settings['og_image']))
4069
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
4070
4071
	if (!empty($context['meta_description']))
4072
	{
4073
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
4074
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
4075
	}
4076
	else
4077
	{
4078
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
4079
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
4080
	}
4081
4082
	call_integration_hook('integrate_theme_context');
4083
}
4084
4085
/**
4086
 * Helper function to set the system memory to a needed value
4087
 * - If the needed memory is greater than current, will attempt to get more
4088
 * - if in_use is set to true, will also try to take the current memory usage in to account
4089
 *
4090
 * @param string $needed The amount of memory to request, if needed, like 256M
4091
 * @param bool $in_use Set to true to account for current memory usage of the script
4092
 * @return boolean True if we have at least the needed memory
4093
 */
4094
function setMemoryLimit($needed, $in_use = false)
4095
{
4096
	// everything in bytes
4097
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
4098
	$memory_needed = memoryReturnBytes($needed);
4099
4100
	// should we account for how much is currently being used?
4101
	if ($in_use)
4102
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
4103
4104
	// if more is needed, request it
4105
	if ($memory_current < $memory_needed)
4106
	{
4107
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
4108
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
4109
	}
4110
4111
	$memory_current = max($memory_current, memoryReturnBytes(get_cfg_var('memory_limit')));
0 ignored issues
show
Bug introduced by
It seems like get_cfg_var('memory_limit') can also be of type array; however, parameter $val of memoryReturnBytes() does only seem to accept string, 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

4111
	$memory_current = max($memory_current, memoryReturnBytes(/** @scrutinizer ignore-type */ get_cfg_var('memory_limit')));
Loading history...
4112
4113
	// return success or not
4114
	return (bool) ($memory_current >= $memory_needed);
4115
}
4116
4117
/**
4118
 * Helper function to convert memory string settings to bytes
4119
 *
4120
 * @param string $val The byte string, like 256M or 1G
4121
 * @return integer The string converted to a proper integer in bytes
4122
 */
4123
function memoryReturnBytes($val)
4124
{
4125
	if (is_integer($val))
0 ignored issues
show
introduced by
The condition is_integer($val) is always false.
Loading history...
4126
		return $val;
4127
4128
	// Separate the number from the designator
4129
	$val = trim($val);
4130
	$num = intval(substr($val, 0, strlen($val) - 1));
4131
	$last = strtolower(substr($val, -1));
4132
4133
	// convert to bytes
4134
	switch ($last)
4135
	{
4136
		case 'g':
4137
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
4138
		case 'm':
4139
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
4140
		case 'k':
4141
			$num *= 1024;
4142
	}
4143
	return $num;
4144
}
4145
4146
/**
4147
 * The header template
4148
 */
4149
function template_header()
4150
{
4151
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir, $cache_enable, $language;
4152
4153
	setupThemeContext();
4154
4155
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
4156
	if (empty($context['no_last_modified']))
4157
	{
4158
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
4159
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
4160
4161
		// Are we debugging the template/html content?
4162
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
4163
			header('content-type: application/xhtml+xml');
4164
		elseif (!isset($_REQUEST['xml']))
4165
			header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
4166
	}
4167
4168
	header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
4169
4170
	// We need to splice this in after the body layer, or after the main layer for older stuff.
4171
	if ($context['in_maintenance'] && $context['user']['is_admin'])
4172
	{
4173
		$position = array_search('body', $context['template_layers']);
4174
		if ($position === false)
4175
			$position = array_search('main', $context['template_layers']);
4176
4177
		if ($position !== false)
4178
		{
4179
			$before = array_slice($context['template_layers'], 0, $position + 1);
4180
			$after = array_slice($context['template_layers'], $position + 1);
4181
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
4182
		}
4183
	}
4184
4185
	$checked_securityFiles = false;
4186
	$showed_banned = false;
4187
	foreach ($context['template_layers'] as $layer)
4188
	{
4189
		loadSubTemplate($layer . '_above', true);
4190
4191
		// May seem contrived, but this is done in case the body and main layer aren't there...
4192
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
4193
		{
4194
			$checked_securityFiles = true;
4195
4196
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
4197
4198
			// Add your own files.
4199
			call_integration_hook('integrate_security_files', array(&$securityFiles));
4200
4201
			foreach ($securityFiles as $i => $securityFile)
4202
			{
4203
				if (!file_exists($boarddir . '/' . $securityFile))
4204
					unset($securityFiles[$i]);
4205
			}
4206
4207
			// We are already checking so many files...just few more doesn't make any difference! :P
4208
			if (!empty($modSettings['currentAttachmentUploadDir']))
4209
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
4210
4211
			else
4212
				$path = $modSettings['attachmentUploadDir'];
4213
4214
			secureDirectory($path, true);
4215
			secureDirectory($cachedir);
4216
4217
			// If agreement is enabled, at least the english version shall exist
4218
			if (!empty($modSettings['requireAgreement']))
4219
				$agreement = !file_exists($boarddir . '/agreement.txt');
4220
4221
			// If privacy policy is enabled, at least the default language version shall exist
4222
			if (!empty($modSettings['requirePolicyAgreement']))
4223
				$policy_agreement = empty($modSettings['policy_' . $language]);
4224
4225
			if (!empty($securityFiles) ||
4226
				(!empty($cache_enable) && !is_writable($cachedir)) ||
4227
				!empty($agreement) ||
4228
				!empty($policy_agreement) ||
4229
				!empty($context['auth_secret_missing']))
4230
			{
4231
				echo '
4232
		<div class="errorbox">
4233
			<p class="alert">!!</p>
4234
			<h3>', empty($securityFiles) && empty($context['auth_secret_missing']) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
4235
			<p>';
4236
4237
				foreach ($securityFiles as $securityFile)
4238
				{
4239
					echo '
4240
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
4241
4242
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
4243
						echo '
4244
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
4245
				}
4246
4247
				if (!empty($cache_enable) && !is_writable($cachedir))
4248
					echo '
4249
				<strong>', $txt['cache_writable'], '</strong><br>';
4250
4251
				if (!empty($agreement))
4252
					echo '
4253
				<strong>', $txt['agreement_missing'], '</strong><br>';
4254
4255
				if (!empty($policy_agreement))
4256
					echo '
4257
				<strong>', $txt['policy_agreement_missing'], '</strong><br>';
4258
4259
				if (!empty($context['auth_secret_missing']))
4260
					echo '
4261
				<strong>', $txt['auth_secret_missing'], '</strong><br>';
4262
4263
				echo '
4264
			</p>
4265
		</div>';
4266
			}
4267
		}
4268
		// If the user is banned from posting inform them of it.
4269
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
4270
		{
4271
			$showed_banned = true;
4272
			echo '
4273
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
4274
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
4275
4276
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
4277
				echo '
4278
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
4279
4280
			if (!empty($_SESSION['ban']['expire_time']))
4281
				echo '
4282
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
4283
			else
4284
				echo '
4285
					<div>', $txt['your_ban_expires_never'], '</div>';
4286
4287
			echo '
4288
				</div>';
4289
		}
4290
	}
4291
}
4292
4293
/**
4294
 * Show the copyright.
4295
 */
4296
function theme_copyright()
4297
{
4298
	global $forum_copyright, $scripturl;
4299
4300
	// Don't display copyright for things like SSI.
4301
	if (SMF !== 1)
0 ignored issues
show
introduced by
The condition SMF !== 1 is always true.
Loading history...
4302
		return;
4303
4304
	// Put in the version...
4305
	printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl);
4306
}
4307
4308
/**
4309
 * The template footer
4310
 */
4311
function template_footer()
4312
{
4313
	global $context, $modSettings, $db_count;
4314
4315
	// Show the load time?  (only makes sense for the footer.)
4316
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
4317
	$context['load_time'] = round(microtime(true) - TIME_START, 3);
4318
	$context['load_queries'] = $db_count;
4319
4320
	if (!empty($context['template_layers']) && is_array($context['template_layers']))
4321
		foreach (array_reverse($context['template_layers']) as $layer)
4322
			loadSubTemplate($layer . '_below', true);
4323
}
4324
4325
/**
4326
 * Output the Javascript files
4327
 * 	- tabbing in this function is to make the HTML source look good and proper
4328
 *  - if deferred is set function will output all JS set to load at page end
4329
 *
4330
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
4331
 */
4332
function template_javascript($do_deferred = false)
4333
{
4334
	global $context, $modSettings, $settings;
4335
4336
	// Use this hook to minify/optimize Javascript files and vars
4337
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
4338
4339
	$toMinify = array(
4340
		'standard' => array(),
4341
		'defer' => array(),
4342
		'async' => array(),
4343
	);
4344
4345
	// Ouput the declared Javascript variables.
4346
	if (!empty($context['javascript_vars']) && !$do_deferred)
4347
	{
4348
		echo '
4349
	<script>';
4350
4351
		foreach ($context['javascript_vars'] as $key => $value)
4352
		{
4353
			if (empty($value))
4354
			{
4355
				echo '
4356
		var ', $key, ';';
4357
			}
4358
			else
4359
			{
4360
				echo '
4361
		var ', $key, ' = ', $value, ';';
4362
			}
4363
		}
4364
4365
		echo '
4366
	</script>';
4367
	}
4368
4369
	// In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body.
4370
	// Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance.
4371
	if (!$do_deferred)
4372
	{
4373
		// While we have JavaScript files to place in the template.
4374
		foreach ($context['javascript_files'] as $id => $js_file)
4375
		{
4376
			// Last minute call! allow theme authors to disable single files.
4377
			if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4378
				continue;
4379
4380
			// By default files don't get minimized unless the file explicitly says so!
4381
			if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
4382
			{
4383
				if (!empty($js_file['options']['async']))
4384
					$toMinify['async'][] = $js_file;
4385
4386
				elseif (!empty($js_file['options']['defer']))
4387
					$toMinify['defer'][] = $js_file;
4388
4389
				else
4390
					$toMinify['standard'][] = $js_file;
4391
4392
				// Grab a random seed.
4393
				if (!isset($minSeed) && isset($js_file['options']['seed']))
4394
					$minSeed = $js_file['options']['seed'];
4395
			}
4396
4397
			else
4398
			{
4399
				echo '
4400
	<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' : '';
4401
4402
				if (!empty($js_file['options']['attributes']))
4403
					foreach ($js_file['options']['attributes'] as $key => $value)
4404
					{
4405
						if (is_bool($value))
4406
							echo !empty($value) ? ' ' . $key : '';
4407
4408
						else
4409
							echo ' ', $key, '="', $value, '"';
4410
					}
4411
4412
				echo '></script>';
4413
			}
4414
		}
4415
4416
		foreach ($toMinify as $js_files)
4417
		{
4418
			if (!empty($js_files))
4419
			{
4420
				$result = custMinify($js_files, 'js');
4421
4422
				$minSuccessful = array_keys($result) === array('smf_minified');
4423
4424
				foreach ($result as $minFile)
4425
					echo '
4426
	<script src="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '"', !empty($minFile['options']['async']) ? ' async' : '', !empty($minFile['options']['defer']) ? ' defer' : '', '></script>';
4427
			}
4428
		}
4429
	}
4430
4431
	// Inline JavaScript - Actually useful some times!
4432
	if (!empty($context['javascript_inline']))
4433
	{
4434
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
4435
		{
4436
			echo '
4437
<script>
4438
window.addEventListener("DOMContentLoaded", function() {';
4439
4440
			foreach ($context['javascript_inline']['defer'] as $js_code)
4441
				echo $js_code;
4442
4443
			echo '
4444
});
4445
</script>';
4446
		}
4447
4448
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
4449
		{
4450
			echo '
4451
	<script>';
4452
4453
			foreach ($context['javascript_inline']['standard'] as $js_code)
4454
				echo $js_code;
4455
4456
			echo '
4457
	</script>';
4458
		}
4459
	}
4460
}
4461
4462
/**
4463
 * Output the CSS files
4464
 */
4465
function template_css()
4466
{
4467
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
4468
4469
	// Use this hook to minify/optimize CSS files
4470
	call_integration_hook('integrate_pre_css_output');
4471
4472
	$toMinify = array();
4473
	$normal = array();
4474
4475
	uasort($context['css_files'], function ($a, $b)
4476
	{
4477
		return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0);
4478
	});
4479
	foreach ($context['css_files'] as $id => $file)
4480
	{
4481
		// Last minute call! allow theme authors to disable single files.
4482
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4483
			continue;
4484
4485
		// Files are minimized unless they explicitly opt out.
4486
		if (!isset($file['options']['minimize']))
4487
			$file['options']['minimize'] = true;
4488
4489
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss']))
4490
		{
4491
			$toMinify[] = $file;
4492
4493
			// Grab a random seed.
4494
			if (!isset($minSeed) && isset($file['options']['seed']))
4495
				$minSeed = $file['options']['seed'];
4496
		}
4497
		else
4498
			$normal[] = array(
4499
				'url' => $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : ''),
4500
				'attributes' => !empty($file['options']['attributes']) ? $file['options']['attributes'] : array()
4501
			);
4502
	}
4503
4504
	if (!empty($toMinify))
4505
	{
4506
		$result = custMinify($toMinify, 'css');
4507
4508
		$minSuccessful = array_keys($result) === array('smf_minified');
4509
4510
		foreach ($result as $minFile)
4511
			echo '
4512
	<link rel="stylesheet" href="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '">';
4513
	}
4514
4515
	// Print the rest after the minified files.
4516
	if (!empty($normal))
4517
		foreach ($normal as $nf)
4518
		{
4519
			echo '
4520
	<link rel="stylesheet" href="', $nf['url'], '"';
4521
4522
			if (!empty($nf['attributes']))
4523
				foreach ($nf['attributes'] as $key => $value)
4524
				{
4525
					if (is_bool($value))
4526
						echo !empty($value) ? ' ' . $key : '';
4527
					else
4528
						echo ' ', $key, '="', $value, '"';
4529
				}
4530
4531
			echo '>';
4532
		}
4533
4534
	if ($db_show_debug === true)
4535
	{
4536
		// Try to keep only what's useful.
4537
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
4538
		foreach ($context['css_files'] as $file)
4539
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
4540
	}
4541
4542
	if (!empty($context['css_header']))
4543
	{
4544
		echo '
4545
	<style>';
4546
4547
		foreach ($context['css_header'] as $css)
4548
			echo $css . '
4549
	';
4550
4551
		echo '
4552
	</style>';
4553
	}
4554
}
4555
4556
/**
4557
 * Get an array of previously defined files and adds them to our main minified files.
4558
 * Sets a one day cache to avoid re-creating a file on every request.
4559
 *
4560
 * @param array $data The files to minify.
4561
 * @param string $type either css or js.
4562
 * @return array Info about the minified file, or about the original files if the minify process failed.
4563
 */
4564
function custMinify($data, $type)
4565
{
4566
	global $settings, $txt;
4567
4568
	$types = array('css', 'js');
4569
	$type = !empty($type) && in_array($type, $types) ? $type : false;
4570
	$data = is_array($data) ? $data : array();
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
4571
4572
	if (empty($type) || empty($data))
4573
		return $data;
4574
4575
	// Different pages include different files, so we use a hash to label the different combinations
4576
	$hash = md5(implode(' ', array_map(function($file)
4577
	{
4578
		return $file['filePath'] . '-' . $file['mtime'];
4579
	}, $data)));
4580
4581
	// Is this a deferred or asynchronous JavaScript file?
4582
	$async = $type === 'js';
4583
	$defer = $type === 'js';
4584
	if ($type === 'js')
4585
	{
4586
		foreach ($data as $id => $file)
4587
		{
4588
			// A minified script should only be loaded asynchronously if all its components wanted to be.
4589
			if (empty($file['options']['async']))
4590
				$async = false;
4591
4592
			// A minified script should only be deferred if all its components wanted to be.
4593
			if (empty($file['options']['defer']))
4594
				$defer = false;
4595
		}
4596
	}
4597
4598
	// Did we already do this?
4599
	$minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type;
4600
	$already_exists = file_exists($minified_file);
4601
4602
	// Already done?
4603
	if ($already_exists)
4604
	{
4605
		return array('smf_minified' => array(
4606
			'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4607
			'filePath' => $minified_file,
4608
			'fileName' => basename($minified_file),
4609
			'options' => array('async' => !empty($async), 'defer' => !empty($defer)),
4610
		));
4611
	}
4612
	// File has to exist. If it doesn't, try to create it.
4613
	elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file))
4614
	{
4615
		loadLanguage('Errors');
4616
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4617
4618
		// The process failed, so roll back to print each individual file.
4619
		return $data;
4620
	}
4621
4622
	// No namespaces, sorry!
4623
	$classType = 'MatthiasMullie\\Minify\\' . strtoupper($type);
4624
4625
	$minifier = new $classType();
4626
4627
	foreach ($data as $id => $file)
4628
	{
4629
		$toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false;
4630
4631
		// The file couldn't be located so it won't be added. Log this error.
4632
		if (empty($toAdd))
4633
		{
4634
			loadLanguage('Errors');
4635
			log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general');
4636
			continue;
4637
		}
4638
4639
		// Add this file to the list.
4640
		$minifier->add($toAdd);
4641
	}
4642
4643
	// Create the file.
4644
	$minifier->minify($minified_file);
4645
	unset($minifier);
4646
	clearstatcache();
4647
4648
	// Minify process failed.
4649
	if (!filesize($minified_file))
4650
	{
4651
		loadLanguage('Errors');
4652
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4653
4654
		// The process failed so roll back to print each individual file.
4655
		return $data;
4656
	}
4657
4658
	return array('smf_minified' => array(
4659
		'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4660
		'filePath' => $minified_file,
4661
		'fileName' => basename($minified_file),
4662
		'options' => array('async' => $async, 'defer' => $defer),
4663
	));
4664
}
4665
4666
/**
4667
 * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date
4668
 */
4669
function deleteAllMinified()
4670
{
4671
	global $smcFunc, $txt, $modSettings;
4672
4673
	$not_deleted = array();
4674
	$most_recent = 0;
4675
4676
	// Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie.
4677
	$request = $smcFunc['db_query']('', '
4678
		SELECT id_theme AS id, value AS dir
4679
		FROM {db_prefix}themes
4680
		WHERE variable = {string:var}',
4681
		array(
4682
			'var' => 'theme_dir',
4683
		)
4684
	);
4685
	while ($theme = $smcFunc['db_fetch_assoc']($request))
4686
	{
4687
		foreach (array('css', 'js') as $type)
4688
		{
4689
			foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename)
4690
			{
4691
				// We want to find the most recent mtime of non-minified files
4692
				if (strpos(pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
0 ignored issues
show
Bug introduced by
It seems like pathinfo($filename, PATHINFO_BASENAME) can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, 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

4692
				if (strpos(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
Loading history...
4693
					$most_recent = max($modSettings['browser_cache'], (int) @filemtime($filename));
4694
4695
				// Try to delete minified files. Add them to our error list if that fails.
4696
				elseif (!@unlink($filename))
4697
					$not_deleted[] = $filename;
4698
			}
4699
		}
4700
	}
4701
	$smcFunc['db_free_result']($request);
4702
4703
	// This setting tracks the most recent modification time of any of our CSS and JS files
4704
	if ($most_recent > $modSettings['browser_cache'])
4705
		updateSettings(array('browser_cache' => $most_recent));
4706
4707
	// If any of the files could not be deleted, log an error about it.
4708
	if (!empty($not_deleted))
4709
	{
4710
		loadLanguage('Errors');
4711
		log_error(sprintf($txt['unlink_minimized_fail'], implode('<br>', $not_deleted)), 'general');
4712
	}
4713
}
4714
4715
/**
4716
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
4717
 *
4718
 * @todo this currently returns the hash if new, and the full filename otherwise.
4719
 * Something messy like that.
4720
 * @todo and of course everything relies on this behavior and work around it. :P.
4721
 * Converters included.
4722
 *
4723
 * @param string $filename The name of the file
4724
 * @param int $attachment_id The ID of the attachment
4725
 * @param string|null $dir Which directory it should be in (null to use current one)
4726
 * @param bool $new Whether this is a new attachment
4727
 * @param string $file_hash The file hash
4728
 * @return string The path to the file
4729
 */
4730
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
4731
{
4732
	global $modSettings, $smcFunc;
4733
4734
	// Just make up a nice hash...
4735
	if ($new)
4736
		return sha1(md5($filename . time()) . mt_rand());
4737
4738
	// Just make sure that attachment id is only a int
4739
	$attachment_id = (int) $attachment_id;
4740
4741
	// Grab the file hash if it wasn't added.
4742
	// Left this for legacy.
4743
	if ($file_hash === '')
4744
	{
4745
		$request = $smcFunc['db_query']('', '
4746
			SELECT file_hash
4747
			FROM {db_prefix}attachments
4748
			WHERE id_attach = {int:id_attach}',
4749
			array(
4750
				'id_attach' => $attachment_id,
4751
			)
4752
		);
4753
4754
		if ($smcFunc['db_num_rows']($request) === 0)
4755
			return false;
4756
4757
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
4758
		$smcFunc['db_free_result']($request);
4759
	}
4760
4761
	// Still no hash? mmm...
4762
	if (empty($file_hash))
4763
		$file_hash = sha1(md5($filename . time()) . mt_rand());
4764
4765
	// Are we using multiple directories?
4766
	if (is_array($modSettings['attachmentUploadDir']))
4767
		$path = $modSettings['attachmentUploadDir'][$dir];
4768
4769
	else
4770
		$path = $modSettings['attachmentUploadDir'];
4771
4772
	return $path . '/' . $attachment_id . '_' . $file_hash . '.dat';
4773
}
4774
4775
/**
4776
 * Convert a single IP to a ranged IP.
4777
 * internal function used to convert a user-readable format to a format suitable for the database.
4778
 *
4779
 * @param string $fullip The full IP
4780
 * @return array An array of IP parts
4781
 */
4782
function ip2range($fullip)
4783
{
4784
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
4785
	if ($fullip == 'unknown')
4786
		$fullip = '255.255.255.255';
4787
4788
	$ip_parts = explode('-', $fullip);
4789
	$ip_array = array();
4790
4791
	// if ip 22.12.31.21
4792
	if (count($ip_parts) == 1 && isValidIP($fullip))
4793
	{
4794
		$ip_array['low'] = $fullip;
4795
		$ip_array['high'] = $fullip;
4796
		return $ip_array;
4797
	} // if ip 22.12.* -> 22.12.* - 22.12.*
4798
	elseif (count($ip_parts) == 1)
4799
	{
4800
		$ip_parts[0] = $fullip;
4801
		$ip_parts[1] = $fullip;
4802
	}
4803
4804
	// if ip 22.12.31.21-12.21.31.21
4805
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
4806
	{
4807
		$ip_array['low'] = $ip_parts[0];
4808
		$ip_array['high'] = $ip_parts[1];
4809
		return $ip_array;
4810
	}
4811
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
4812
	{
4813
		$valid_low = isValidIP($ip_parts[0]);
4814
		$valid_high = isValidIP($ip_parts[1]);
4815
		$count = 0;
4816
		$mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.');
4817
		$max = ($mode == ':' ? 'ffff' : '255');
4818
		$min = 0;
4819
		if (!$valid_low)
4820
		{
4821
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
4822
			$valid_low = isValidIP($ip_parts[0]);
4823
			while (!$valid_low)
4824
			{
4825
				$ip_parts[0] .= $mode . $min;
4826
				$valid_low = isValidIP($ip_parts[0]);
4827
				$count++;
4828
				if ($count > 9) break;
4829
			}
4830
		}
4831
4832
		$count = 0;
4833
		if (!$valid_high)
4834
		{
4835
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
4836
			$valid_high = isValidIP($ip_parts[1]);
4837
			while (!$valid_high)
4838
			{
4839
				$ip_parts[1] .= $mode . $max;
4840
				$valid_high = isValidIP($ip_parts[1]);
4841
				$count++;
4842
				if ($count > 9) break;
4843
			}
4844
		}
4845
4846
		if ($valid_high && $valid_low)
4847
		{
4848
			$ip_array['low'] = $ip_parts[0];
4849
			$ip_array['high'] = $ip_parts[1];
4850
		}
4851
	}
4852
4853
	return $ip_array;
4854
}
4855
4856
/**
4857
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
4858
 *
4859
 * @param string $ip The IP to get the hostname from
4860
 * @return string The hostname
4861
 */
4862
function host_from_ip($ip)
4863
{
4864
	global $modSettings;
4865
4866
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
4867
		return $host;
4868
	$t = microtime(true);
4869
4870
	// Try the Linux host command, perhaps?
4871
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
4872
	{
4873
		if (!isset($modSettings['host_to_dis']))
4874
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
4875
		else
4876
			$test = @shell_exec('host ' . @escapeshellarg($ip));
4877
4878
		// Did host say it didn't find anything?
4879
		if (strpos($test, 'not found') !== false)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false; however, parameter $haystack of strpos() does only seem to accept string, 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

4879
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
4880
			$host = '';
4881
		// Invalid server option?
4882
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
4883
			updateSettings(array('host_to_dis' => 1));
4884
		// Maybe it found something, after all?
4885
		elseif (preg_match('~\s([^\s]+?)\.\s~', $test, $match) == 1)
0 ignored issues
show
Bug introduced by
It seems like $test can also be of type false; however, parameter $subject of preg_match() does only seem to accept string, 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

4885
		elseif (preg_match('~\s([^\s]+?)\.\s~', /** @scrutinizer ignore-type */ $test, $match) == 1)
Loading history...
4886
			$host = $match[1];
4887
	}
4888
4889
	// This is nslookup; usually only Windows, but possibly some Unix?
4890
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
4891
	{
4892
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
4893
		if (strpos($test, 'Non-existent domain') !== false)
4894
			$host = '';
4895
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
4896
			$host = $match[1];
4897
	}
4898
4899
	// This is the last try :/.
4900
	if (!isset($host) || $host === false)
4901
		$host = @gethostbyaddr($ip);
4902
4903
	// It took a long time, so let's cache it!
4904
	if (microtime(true) - $t > 0.5)
4905
		cache_put_data('hostlookup-' . $ip, $host, 600);
4906
4907
	return $host;
4908
}
4909
4910
/**
4911
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
4912
 *
4913
 * @param string $text The text to split into words
4914
 * @param int $max_chars The maximum number of characters per word
4915
 * @param bool $encrypt Whether to encrypt the results
4916
 * @return array An array of ints or words depending on $encrypt
4917
 */
4918
function text2words($text, $max_chars = 20, $encrypt = false)
4919
{
4920
	global $smcFunc, $context;
4921
4922
	// Upgrader may be working on old DBs...
4923
	if (!isset($context['utf8']))
4924
		$context['utf8'] = false;
4925
4926
	// Step 1: Remove entities/things we don't consider words:
4927
	$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>' => ' ')));
4928
4929
	// Step 2: Entities we left to letters, where applicable, lowercase.
4930
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
4931
4932
	// Step 3: Ready to split apart and index!
4933
	$words = explode(' ', $words);
4934
4935
	if ($encrypt)
4936
	{
4937
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
4938
		$returned_ints = array();
4939
		foreach ($words as $word)
4940
		{
4941
			if (($word = trim($word, '-_\'')) !== '')
4942
			{
4943
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
4944
				$total = 0;
4945
				for ($i = 0; $i < $max_chars; $i++)
4946
					$total += $possible_chars[ord($encrypted[$i])] * pow(63, $i);
4947
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
4948
			}
4949
		}
4950
		return array_unique($returned_ints);
4951
	}
4952
	else
4953
	{
4954
		// Trim characters before and after and add slashes for database insertion.
4955
		$returned_words = array();
4956
		foreach ($words as $word)
4957
			if (($word = trim($word, '-_\'')) !== '')
4958
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
4959
4960
		// Filter out all words that occur more than once.
4961
		return array_unique($returned_words);
4962
	}
4963
}
4964
4965
/**
4966
 * Creates an image/text button
4967
 *
4968
 * @deprecated since 2.1
4969
 * @param string $name The name of the button (should be a main_icons class or the name of an image)
4970
 * @param string $alt The alt text
4971
 * @param string $label The $txt string to use as the label
4972
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
4973
 * @param boolean $force_use Whether to force use of this when template_create_button is available
4974
 * @return string The HTML to display the button
4975
 */
4976
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
4977
{
4978
	global $settings, $txt;
4979
4980
	// Does the current loaded theme have this and we are not forcing the usage of this function?
4981
	if (function_exists('template_create_button') && !$force_use)
4982
		return template_create_button($name, $alt, $label = '', $custom = '');
4983
4984
	if (!$settings['use_image_buttons'])
4985
		return $txt[$alt];
4986
	elseif (!empty($settings['use_buttons']))
4987
		return '<span class="main_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
4988
	else
4989
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
4990
}
4991
4992
/**
4993
 * Sets up all of the top menu buttons
4994
 * Saves them in the cache if it is available and on
4995
 * Places the results in $context
4996
 */
4997
function setupMenuContext()
4998
{
4999
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc, $cache_enable;
5000
5001
	// Set up the menu privileges.
5002
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
5003
	$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'));
5004
5005
	$context['allow_memberlist'] = allowedTo('view_mlist');
5006
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
5007
	$context['allow_moderation_center'] = $context['user']['can_mod'];
5008
	$context['allow_pm'] = allowedTo('pm_read');
5009
5010
	$cacheTime = $modSettings['lastActive'] * 60;
5011
5012
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
5013
	if (!isset($context['allow_calendar_event']))
5014
	{
5015
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
5016
5017
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
5018
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
5019
		{
5020
			$boards_can_post = boardsAllowedTo('post_new');
5021
			$context['allow_calendar_event'] &= !empty($boards_can_post);
5022
		}
5023
	}
5024
5025
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
5026
	if (!$context['user']['is_guest'])
5027
	{
5028
		addInlineJavaScript('
5029
	var user_menus = new smc_PopupMenu();
5030
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
5031
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true);
5032
		if ($context['allow_pm'])
5033
			addInlineJavaScript('
5034
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
5035
5036
		if (!empty($modSettings['enable_ajax_alerts']))
5037
		{
5038
			require_once($sourcedir . '/Subs-Notify.php');
5039
5040
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
5041
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
5042
5043
			addInlineJavaScript('
5044
	var new_alert_title = "' . $context['forum_name_html_safe'] . '";
5045
	var alert_timeout = ' . $timeout . ';');
5046
			loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts');
5047
		}
5048
	}
5049
5050
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
5051
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
5052
	{
5053
		$buttons = array(
5054
			'home' => array(
5055
				'title' => $txt['home'],
5056
				'href' => $scripturl,
5057
				'show' => true,
5058
				'sub_buttons' => array(
5059
				),
5060
				'is_last' => $context['right_to_left'],
5061
			),
5062
			'search' => array(
5063
				'title' => $txt['search'],
5064
				'href' => $scripturl . '?action=search',
5065
				'show' => $context['allow_search'],
5066
				'sub_buttons' => array(
5067
				),
5068
			),
5069
			'admin' => array(
5070
				'title' => $txt['admin'],
5071
				'href' => $scripturl . '?action=admin',
5072
				'show' => $context['allow_admin'],
5073
				'sub_buttons' => array(
5074
					'featuresettings' => array(
5075
						'title' => $txt['modSettings_title'],
5076
						'href' => $scripturl . '?action=admin;area=featuresettings',
5077
						'show' => allowedTo('admin_forum'),
5078
					),
5079
					'packages' => array(
5080
						'title' => $txt['package'],
5081
						'href' => $scripturl . '?action=admin;area=packages',
5082
						'show' => allowedTo('admin_forum'),
5083
					),
5084
					'errorlog' => array(
5085
						'title' => $txt['errorlog'],
5086
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
5087
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
5088
					),
5089
					'permissions' => array(
5090
						'title' => $txt['edit_permissions'],
5091
						'href' => $scripturl . '?action=admin;area=permissions',
5092
						'show' => allowedTo('manage_permissions'),
5093
					),
5094
					'memberapprove' => array(
5095
						'title' => $txt['approve_members_waiting'],
5096
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
5097
						'show' => !empty($context['unapproved_members']),
5098
						'is_last' => true,
5099
					),
5100
				),
5101
			),
5102
			'moderate' => array(
5103
				'title' => $txt['moderate'],
5104
				'href' => $scripturl . '?action=moderate',
5105
				'show' => $context['allow_moderation_center'],
5106
				'sub_buttons' => array(
5107
					'modlog' => array(
5108
						'title' => $txt['modlog_view'],
5109
						'href' => $scripturl . '?action=moderate;area=modlog',
5110
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
5111
					),
5112
					'poststopics' => array(
5113
						'title' => $txt['mc_unapproved_poststopics'],
5114
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
5115
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
5116
					),
5117
					'attachments' => array(
5118
						'title' => $txt['mc_unapproved_attachments'],
5119
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
5120
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
5121
					),
5122
					'reports' => array(
5123
						'title' => $txt['mc_reported_posts'],
5124
						'href' => $scripturl . '?action=moderate;area=reportedposts',
5125
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
5126
					),
5127
					'reported_members' => array(
5128
						'title' => $txt['mc_reported_members'],
5129
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
5130
						'show' => allowedTo('moderate_forum'),
5131
						'is_last' => true,
5132
					)
5133
				),
5134
			),
5135
			'calendar' => array(
5136
				'title' => $txt['calendar'],
5137
				'href' => $scripturl . '?action=calendar',
5138
				'show' => $context['allow_calendar'],
5139
				'sub_buttons' => array(
5140
					'view' => array(
5141
						'title' => $txt['calendar_menu'],
5142
						'href' => $scripturl . '?action=calendar',
5143
						'show' => $context['allow_calendar_event'],
5144
					),
5145
					'post' => array(
5146
						'title' => $txt['calendar_post_event'],
5147
						'href' => $scripturl . '?action=calendar;sa=post',
5148
						'show' => $context['allow_calendar_event'],
5149
						'is_last' => true,
5150
					),
5151
				),
5152
			),
5153
			'mlist' => array(
5154
				'title' => $txt['members_title'],
5155
				'href' => $scripturl . '?action=mlist',
5156
				'show' => $context['allow_memberlist'],
5157
				'sub_buttons' => array(
5158
					'mlist_view' => array(
5159
						'title' => $txt['mlist_menu_view'],
5160
						'href' => $scripturl . '?action=mlist',
5161
						'show' => true,
5162
					),
5163
					'mlist_search' => array(
5164
						'title' => $txt['mlist_search'],
5165
						'href' => $scripturl . '?action=mlist;sa=search',
5166
						'show' => true,
5167
						'is_last' => true,
5168
					),
5169
				),
5170
				'is_last' => !$context['right_to_left'] && (!$user_info['is_guest'] || !$context['can_register']),
5171
			),
5172
			'signup' => array(
5173
				'title' => $txt['register'],
5174
				'href' => $scripturl . '?action=signup',
5175
				'show' => $user_info['is_guest'] && $context['can_register'],
5176
				'sub_buttons' => array(
5177
				),
5178
				'is_last' => !$context['right_to_left'],
5179
			),
5180
		);
5181
5182
		// Allow editing menu buttons easily.
5183
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
5184
5185
		// Now we put the buttons in the context so the theme can use them.
5186
		$menu_buttons = array();
5187
		foreach ($buttons as $act => $button)
5188
			if (!empty($button['show']))
5189
			{
5190
				$button['active_button'] = false;
5191
5192
				// This button needs some action.
5193
				if (isset($button['action_hook']))
5194
					$needs_action_hook = true;
5195
5196
				// Make sure the last button truly is the last button.
5197
				if (!empty($button['is_last']))
5198
				{
5199
					if (isset($last_button))
5200
						unset($menu_buttons[$last_button]['is_last']);
5201
					$last_button = $act;
5202
				}
5203
5204
				// Go through the sub buttons if there are any.
5205
				if (!empty($button['sub_buttons']))
5206
					foreach ($button['sub_buttons'] as $key => $subbutton)
5207
					{
5208
						if (empty($subbutton['show']))
5209
							unset($button['sub_buttons'][$key]);
5210
5211
						// 2nd level sub buttons next...
5212
						if (!empty($subbutton['sub_buttons']))
5213
						{
5214
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
5215
							{
5216
								if (empty($sub_button2['show']))
5217
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
5218
							}
5219
						}
5220
					}
5221
5222
				// Does this button have its own icon?
5223
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
5224
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
5225
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
5226
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
5227
				elseif (isset($button['icon']))
5228
					$button['icon'] = '<span class="main_icons ' . $button['icon'] . '"></span>';
5229
				else
5230
					$button['icon'] = '<span class="main_icons ' . $act . '"></span>';
5231
5232
				$menu_buttons[$act] = $button;
5233
			}
5234
5235
		if (!empty($cache_enable) && $cache_enable >= 2)
5236
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
5237
	}
5238
5239
	$context['menu_buttons'] = $menu_buttons;
5240
5241
	// Logging out requires the session id in the url.
5242
	if (isset($context['menu_buttons']['logout']))
5243
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
5244
5245
	// Figure out which action we are doing so we can set the active tab.
5246
	// Default to home.
5247
	$current_action = 'home';
5248
5249
	if (isset($context['menu_buttons'][$context['current_action']]))
5250
		$current_action = $context['current_action'];
5251
	elseif ($context['current_action'] == 'search2')
5252
		$current_action = 'search';
5253
	elseif ($context['current_action'] == 'theme')
5254
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
5255
	elseif ($context['current_action'] == 'register2')
5256
		$current_action = 'register';
5257
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
5258
		$current_action = 'login';
5259
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
5260
		$current_action = 'moderate';
5261
5262
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
5263
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
5264
	{
5265
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
5266
		$context[$current_action] = true;
5267
	}
5268
	elseif ($context['current_action'] == 'pm')
5269
	{
5270
		$current_action = 'self_pm';
5271
		$context['self_pm'] = true;
5272
	}
5273
5274
	$context['total_mod_reports'] = 0;
5275
	$context['total_admin_reports'] = 0;
5276
5277
	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']))
5278
	{
5279
		$context['total_mod_reports'] = $context['open_mod_reports'];
5280
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports'];
5281
	}
5282
5283
	// Show how many errors there are
5284
	if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog']))
5285
	{
5286
		// Get an error count, if necessary
5287
		if (!isset($context['num_errors']))
5288
		{
5289
			$query = $smcFunc['db_query']('', '
5290
				SELECT COUNT(*)
5291
				FROM {db_prefix}log_errors',
5292
				array()
5293
			);
5294
5295
			list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
5296
			$smcFunc['db_free_result']($query);
5297
		}
5298
5299
		if (!empty($context['num_errors']))
5300
		{
5301
			$context['total_admin_reports'] += $context['num_errors'];
5302
			$context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors'];
5303
		}
5304
	}
5305
5306
	// Show number of reported members
5307
	if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members']))
5308
	{
5309
		$context['total_mod_reports'] += $context['open_member_reports'];
5310
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports'];
5311
	}
5312
5313
	if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin']))
5314
	{
5315
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members'];
5316
		$context['total_admin_reports'] += $context['unapproved_members'];
5317
	}
5318
5319
	if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin']))
5320
	{
5321
		$context['menu_buttons']['admin']['amt'] = $context['total_admin_reports'];
5322
	}
5323
5324
	// Do we have any open reports?
5325
	if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate']))
5326
	{
5327
		$context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports'];
5328
	}
5329
5330
	// Not all actions are simple.
5331
	if (!empty($needs_action_hook))
5332
		call_integration_hook('integrate_current_action', array(&$current_action));
5333
5334
	if (isset($context['menu_buttons'][$current_action]))
5335
		$context['menu_buttons'][$current_action]['active_button'] = true;
5336
}
5337
5338
/**
5339
 * Generate a random seed and ensure it's stored in settings.
5340
 */
5341
function smf_seed_generator()
5342
{
5343
	updateSettings(array('rand_seed' => microtime(true)));
5344
}
5345
5346
/**
5347
 * Process functions of an integration hook.
5348
 * calls all functions of the given hook.
5349
 * supports static class method calls.
5350
 *
5351
 * @param string $hook The hook name
5352
 * @param array $parameters An array of parameters this hook implements
5353
 * @return array The results of the functions
5354
 */
5355
function call_integration_hook($hook, $parameters = array())
5356
{
5357
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
5358
	global $context, $txt;
5359
5360
	if ($db_show_debug === true)
5361
		$context['debug']['hooks'][] = $hook;
5362
5363
	// Need to have some control.
5364
	if (!isset($context['instances']))
5365
		$context['instances'] = array();
5366
5367
	$results = array();
5368
	if (empty($modSettings[$hook]))
5369
		return $results;
5370
5371
	$functions = explode(',', $modSettings[$hook]);
5372
	// Loop through each function.
5373
	foreach ($functions as $function)
5374
	{
5375
		// Hook has been marked as "disabled". Skip it!
5376
		if (strpos($function, '!') !== false)
5377
			continue;
5378
5379
		$call = call_helper($function, true);
5380
5381
		// Is it valid?
5382
		if (!empty($call))
5383
			$results[$function] = call_user_func_array($call, $parameters);
0 ignored issues
show
Bug introduced by
It seems like $call can also be of type boolean; however, parameter $callback of call_user_func_array() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

5383
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
5384
		// This failed, but we want to do so silently.
5385
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
5386
			return $results;
5387
		// Whatever it was suppose to call, it failed :(
5388
		elseif (!empty($function))
5389
		{
5390
			loadLanguage('Errors');
5391
5392
			// Get a full path to show on error.
5393
			if (strpos($function, '|') !== false)
5394
			{
5395
				list ($file, $string) = explode('|', $function);
5396
				$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'])));
5397
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
5398
			}
5399
			// "Assume" the file resides on $boarddir somewhere...
5400
			else
5401
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
5402
		}
5403
	}
5404
5405
	return $results;
5406
}
5407
5408
/**
5409
 * Add a function for integration hook.
5410
 * does nothing if the function is already added.
5411
 *
5412
 * @param string $hook The complete hook name.
5413
 * @param string $function The function name. Can be a call to a method via Class::method.
5414
 * @param bool $permanent If true, updates the value in settings table.
5415
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5416
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5417
 */
5418
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5419
{
5420
	global $smcFunc, $modSettings;
5421
5422
	// Any objects?
5423
	if ($object)
5424
		$function = $function . '#';
5425
5426
	// Any files  to load?
5427
	if (!empty($file) && is_string($file))
5428
		$function = $file . (!empty($function) ? '|' . $function : '');
5429
5430
	// Get the correct string.
5431
	$integration_call = $function;
5432
5433
	// Is it going to be permanent?
5434
	if ($permanent)
5435
	{
5436
		$request = $smcFunc['db_query']('', '
5437
			SELECT value
5438
			FROM {db_prefix}settings
5439
			WHERE variable = {string:variable}',
5440
			array(
5441
				'variable' => $hook,
5442
			)
5443
		);
5444
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
5445
		$smcFunc['db_free_result']($request);
5446
5447
		if (!empty($current_functions))
5448
		{
5449
			$current_functions = explode(',', $current_functions);
5450
			if (in_array($integration_call, $current_functions))
5451
				return;
5452
5453
			$permanent_functions = array_merge($current_functions, array($integration_call));
5454
		}
5455
		else
5456
			$permanent_functions = array($integration_call);
5457
5458
		updateSettings(array($hook => implode(',', $permanent_functions)));
5459
	}
5460
5461
	// Make current function list usable.
5462
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5463
5464
	// Do nothing, if it's already there.
5465
	if (in_array($integration_call, $functions))
5466
		return;
5467
5468
	$functions[] = $integration_call;
5469
	$modSettings[$hook] = implode(',', $functions);
5470
}
5471
5472
/**
5473
 * Remove an integration hook function.
5474
 * Removes the given function from the given hook.
5475
 * Does nothing if the function is not available.
5476
 *
5477
 * @param string $hook The complete hook name.
5478
 * @param string $function The function name. Can be a call to a method via Class::method.
5479
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
5480
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5481
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5482
 * @see add_integration_function
5483
 */
5484
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5485
{
5486
	global $smcFunc, $modSettings;
5487
5488
	// Any objects?
5489
	if ($object)
5490
		$function = $function . '#';
5491
5492
	// Any files  to load?
5493
	if (!empty($file) && is_string($file))
5494
		$function = $file . '|' . $function;
5495
5496
	// Get the correct string.
5497
	$integration_call = $function;
5498
5499
	// Get the permanent functions.
5500
	$request = $smcFunc['db_query']('', '
5501
		SELECT value
5502
		FROM {db_prefix}settings
5503
		WHERE variable = {string:variable}',
5504
		array(
5505
			'variable' => $hook,
5506
		)
5507
	);
5508
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
5509
	$smcFunc['db_free_result']($request);
5510
5511
	if (!empty($current_functions))
5512
	{
5513
		$current_functions = explode(',', $current_functions);
5514
5515
		if (in_array($integration_call, $current_functions))
5516
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
5517
	}
5518
5519
	// Turn the function list into something usable.
5520
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5521
5522
	// You can only remove it if it's available.
5523
	if (!in_array($integration_call, $functions))
5524
		return;
5525
5526
	$functions = array_diff($functions, array($integration_call));
5527
	$modSettings[$hook] = implode(',', $functions);
5528
}
5529
5530
/**
5531
 * Receives a string and tries to figure it out if its a method or a function.
5532
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
5533
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
5534
 * Prepare and returns a callable depending on the type of method/function found.
5535
 *
5536
 * @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)
5537
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
5538
 * @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.
5539
 */
5540
function call_helper($string, $return = false)
5541
{
5542
	global $context, $smcFunc, $txt, $db_show_debug;
5543
5544
	// Really?
5545
	if (empty($string))
5546
		return false;
5547
5548
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5549
	// A closure? should be a callable one.
5550
	if (is_array($string) || $string instanceof Closure)
5551
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5552
5553
	// No full objects, sorry! pass a method or a property instead!
5554
	if (is_object($string))
5555
		return false;
5556
5557
	// Stay vitaminized my friends...
5558
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5559
5560
	// Is there a file to load?
5561
	$string = load_file($string);
5562
5563
	// Loaded file failed
5564
	if (empty($string))
5565
		return false;
5566
5567
	// Found a method.
5568
	if (strpos($string, '::') !== false)
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type boolean; however, parameter $haystack of strpos() does only seem to accept string, 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

5568
	if (strpos(/** @scrutinizer ignore-type */ $string, '::') !== false)
Loading history...
5569
	{
5570
		list ($class, $method) = explode('::', $string);
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type boolean; however, parameter $string of explode() does only seem to accept string, 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

5570
		list ($class, $method) = explode('::', /** @scrutinizer ignore-type */ $string);
Loading history...
5571
5572
		// Check if a new object will be created.
5573
		if (strpos($method, '#') !== false)
5574
		{
5575
			// Need to remove the # thing.
5576
			$method = str_replace('#', '', $method);
5577
5578
			// Don't need to create a new instance for every method.
5579
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
5580
			{
5581
				$context['instances'][$class] = new $class;
5582
5583
				// Add another one to the list.
5584
				if ($db_show_debug === true)
5585
				{
5586
					if (!isset($context['debug']['instances']))
5587
						$context['debug']['instances'] = array();
5588
5589
					$context['debug']['instances'][$class] = $class;
5590
				}
5591
			}
5592
5593
			$func = array($context['instances'][$class], $method);
5594
		}
5595
5596
		// Right then. This is a call to a static method.
5597
		else
5598
			$func = array($class, $method);
5599
	}
5600
5601
	// Nope! just a plain regular function.
5602
	else
5603
		$func = $string;
5604
5605
	// We can't call this helper, but we want to silently ignore this.
5606
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
5607
		return false;
5608
5609
	// Right, we got what we need, time to do some checks.
5610
	elseif (!is_callable($func, false, $callable_name))
5611
	{
5612
		loadLanguage('Errors');
5613
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
5614
5615
		// Gotta tell everybody.
5616
		return false;
5617
	}
5618
5619
	// Everything went better than expected.
5620
	else
5621
	{
5622
		// What are we gonna do about it?
5623
		if ($return)
5624
			return $func;
5625
5626
		// If this is a plain function, avoid the heat of calling call_user_func().
5627
		else
5628
		{
5629
			if (is_array($func))
5630
				call_user_func($func);
5631
5632
			else
5633
				$func();
5634
		}
5635
	}
5636
}
5637
5638
/**
5639
 * Receives a string and tries to figure it out if it contains info to load a file.
5640
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
5641
 * 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.
5642
 *
5643
 * @param string $string The string containing a valid format.
5644
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
5645
 */
5646
function load_file($string)
5647
{
5648
	global $sourcedir, $txt, $boarddir, $settings, $context;
5649
5650
	if (empty($string))
5651
		return false;
5652
5653
	if (strpos($string, '|') !== false)
5654
	{
5655
		list ($file, $string) = explode('|', $string);
5656
5657
		// Match the wildcards to their regular vars.
5658
		if (empty($settings['theme_dir']))
5659
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
5660
5661
		else
5662
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
5663
5664
		// Load the file if it can be loaded.
5665
		if (file_exists($absPath))
5666
			require_once($absPath);
5667
5668
		// No? try a fallback to $sourcedir
5669
		else
5670
		{
5671
			$absPath = $sourcedir . '/' . $file;
5672
5673
			if (file_exists($absPath))
5674
				require_once($absPath);
5675
5676
			// Sorry, can't do much for you at this point.
5677
			elseif (empty($context['uninstalling']))
5678
			{
5679
				loadLanguage('Errors');
5680
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
5681
5682
				// File couldn't be loaded.
5683
				return false;
5684
			}
5685
		}
5686
	}
5687
5688
	return $string;
5689
}
5690
5691
/**
5692
 * Get the contents of a URL, irrespective of allow_url_fopen.
5693
 *
5694
 * - reads the contents of an http or ftp address and returns the page in a string
5695
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
5696
 * - if post_data is supplied, the value and length is posted to the given url as form data
5697
 * - URL must be supplied in lowercase
5698
 *
5699
 * @param string $url The URL
5700
 * @param string $post_data The data to post to the given URL
5701
 * @param bool $keep_alive Whether to send keepalive info
5702
 * @param int $redirection_level How many levels of redirection
5703
 * @return string|false The fetched data or false on failure
5704
 */
5705
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
5706
{
5707
	global $webmaster_email, $sourcedir, $txt;
5708
	static $keep_alive_dom = null, $keep_alive_fp = null;
5709
5710
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', $url, $match);
5711
5712
	// No scheme? No data for you!
5713
	if (empty($match[1]))
5714
		return false;
5715
5716
	// An FTP url. We should try connecting and RETRieving it...
5717
	elseif ($match[1] == 'ftp')
5718
	{
5719
		// Include the file containing the ftp_connection class.
5720
		require_once($sourcedir . '/Class-Package.php');
5721
5722
		// Establish a connection and attempt to enable passive mode.
5723
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
5724
		if ($ftp->error !== false || !$ftp->passive())
0 ignored issues
show
introduced by
The condition $ftp->error !== false is always true.
Loading history...
5725
			return false;
5726
5727
		// I want that one *points*!
5728
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
5729
5730
		// Since passive mode worked (or we would have returned already!) open the connection.
5731
		$fp = @fsockopen($ftp->pasv['ip'], $ftp->pasv['port'], $err, $err, 5);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $err seems to be never defined.
Loading history...
5732
		if (!$fp)
5733
			return false;
5734
5735
		// The server should now say something in acknowledgement.
5736
		$ftp->check_response(150);
5737
5738
		$data = '';
5739
		while (!feof($fp))
5740
			$data .= fread($fp, 4096);
5741
		fclose($fp);
5742
5743
		// All done, right?  Good.
5744
		$ftp->check_response(226);
5745
		$ftp->close();
5746
	}
5747
5748
	// This is more likely; a standard HTTP URL.
5749
	elseif (isset($match[1]) && $match[1] == 'http')
5750
	{
5751
		// First try to use fsockopen, because it is fastest.
5752
		if ($keep_alive && $match[3] == $keep_alive_dom)
5753
			$fp = $keep_alive_fp;
5754
		if (empty($fp))
5755
		{
5756
			// Open the socket on the port we want...
5757
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
5758
		}
5759
		if (!empty($fp))
5760
		{
5761
			if ($keep_alive)
5762
			{
5763
				$keep_alive_dom = $match[3];
5764
				$keep_alive_fp = $fp;
5765
			}
5766
5767
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
5768
			if (empty($post_data))
5769
			{
5770
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
5771
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5772
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5773
				if ($keep_alive)
5774
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
5775
				else
5776
					fwrite($fp, 'connection: close' . "\r\n\r\n");
5777
			}
5778
			else
5779
			{
5780
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
5781
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5782
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5783
				if ($keep_alive)
5784
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
5785
				else
5786
					fwrite($fp, 'connection: close' . "\r\n");
5787
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
5788
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
5789
				fwrite($fp, $post_data);
5790
			}
5791
5792
			$response = fgets($fp, 768);
5793
5794
			// Redirect in case this location is permanently or temporarily moved.
5795
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
5796
			{
5797
				$header = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $header is dead and can be removed.
Loading history...
5798
				$location = '';
5799
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5800
					if (stripos($header, 'location:') !== false)
5801
						$location = trim(substr($header, strpos($header, ':') + 1));
5802
5803
				if (empty($location))
5804
					return false;
5805
				else
5806
				{
5807
					if (!$keep_alive)
5808
						fclose($fp);
5809
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
5810
				}
5811
			}
5812
5813
			// Make sure we get a 200 OK.
5814
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
5815
				return false;
5816
5817
			// Skip the headers...
5818
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5819
			{
5820
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
5821
					$content_length = $match[1];
5822
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
5823
				{
5824
					$keep_alive_dom = null;
5825
					$keep_alive = false;
5826
				}
5827
5828
				continue;
5829
			}
5830
5831
			$data = '';
5832
			if (isset($content_length))
5833
			{
5834
				while (!feof($fp) && strlen($data) < $content_length)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $content_length does not seem to be defined for all execution paths leading up to this point.
Loading history...
5835
					$data .= fread($fp, $content_length - strlen($data));
5836
			}
5837
			else
5838
			{
5839
				while (!feof($fp))
5840
					$data .= fread($fp, 4096);
5841
			}
5842
5843
			if (!$keep_alive)
5844
				fclose($fp);
5845
		}
5846
5847
		// If using fsockopen didn't work, try to use cURL if available.
5848
		elseif (function_exists('curl_init'))
5849
		{
5850
			// Include the file containing the curl_fetch_web_data class.
5851
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
5852
5853
			$fetch_data = new curl_fetch_web_data();
5854
			$fetch_data->get_url_data($url, $post_data);
0 ignored issues
show
Bug introduced by
$post_data of type string is incompatible with the type array expected by parameter $post_data of curl_fetch_web_data::get_url_data(). ( Ignorable by Annotation )

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

5854
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
5855
5856
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
5857
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
5858
				$data = $fetch_data->result('body');
5859
			else
5860
				return false;
5861
		}
5862
5863
		// Neither fsockopen nor curl are available. Well, phooey.
5864
		else
5865
			return false;
5866
	}
5867
	else
5868
	{
5869
		// Umm, this shouldn't happen?
5870
		loadLanguage('Errors');
5871
		trigger_error($txt['fetch_web_data_bad_url'], E_USER_NOTICE);
5872
		$data = false;
5873
	}
5874
5875
	return $data;
5876
}
5877
5878
/**
5879
 * Attempts to determine the MIME type of some data or a file.
5880
 *
5881
 * @param string $data The data to check, or the path or URL of a file to check.
5882
 * @param string $is_path If true, $data is a path or URL to a file.
5883
 * @return string|bool A MIME type, or false if we cannot determine it.
5884
 */
5885
function get_mime_type($data, $is_path = false)
5886
{
5887
	global $cachedir;
5888
5889
	$finfo_loaded = extension_loaded('fileinfo');
5890
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
5891
5892
	// Oh well. We tried.
5893
	if (!$finfo_loaded && !$exif_loaded)
5894
		return false;
5895
5896
	// Start with the 'empty' MIME type.
5897
	$mime_type = 'application/x-empty';
5898
5899
	if ($finfo_loaded)
5900
	{
5901
		// Just some nice, simple data to analyze.
5902
		if (empty($is_path))
5903
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5904
5905
		// A file, or maybe a URL?
5906
		else
5907
		{
5908
			// Local file.
5909
			if (file_exists($data))
5910
				$mime_type = mime_content_type($data);
5911
5912
			// URL.
5913
			elseif ($data = fetch_web_data($data))
5914
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
5915
		}
5916
	}
5917
	// Workaround using Exif requires a local file.
5918
	else
5919
	{
5920
		// If $data is a URL to fetch, do so.
5921
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
5922
		{
5923
			$data = fetch_web_data($data);
5924
			$is_path = false;
5925
		}
5926
5927
		// If we don't have a local file, create one and use it.
5928
		if (empty($is_path))
5929
		{
5930
			$temp_file = tempnam($cachedir, md5($data));
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type false; however, parameter $string of md5() does only seem to accept string, 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

5930
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
5931
			file_put_contents($temp_file, $data);
5932
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
5933
			$data = $temp_file;
5934
		}
5935
5936
		$imagetype = @exif_imagetype($data);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type false; however, parameter $filename of exif_imagetype() does only seem to accept string, 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

5936
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
5937
5938
		if (isset($temp_file))
5939
			unlink($temp_file);
5940
5941
		// Unfortunately, this workaround only works for image files.
5942
		if ($imagetype !== false)
5943
			$mime_type = image_type_to_mime_type($imagetype);
5944
	}
5945
5946
	return $mime_type;
5947
}
5948
5949
/**
5950
 * Checks whether a file or data has the expected MIME type.
5951
 *
5952
 * @param string $data The data to check, or the path or URL of a file to check.
5953
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
5954
 * @param string $is_path If true, $data is a path or URL to a file.
5955
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
5956
 */
5957
function check_mime_type($data, $type_pattern, $is_path = false)
5958
{
5959
	// Get the MIME type.
5960
	$mime_type = get_mime_type($data, $is_path);
0 ignored issues
show
Bug introduced by
It seems like $is_path can also be of type false; however, parameter $is_path of get_mime_type() does only seem to accept string, 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

5960
	$mime_type = get_mime_type($data, /** @scrutinizer ignore-type */ $is_path);
Loading history...
5961
5962
	// Couldn't determine it.
5963
	if ($mime_type === false)
5964
		return 2;
5965
5966
	// Check whether the MIME type matches expectations.
5967
	return (int) @preg_match('~' . $type_pattern . '~', $mime_type);
5968
}
5969
5970
/**
5971
 * Prepares an array of "likes" info for the topic specified by $topic
5972
 *
5973
 * @param integer $topic The topic ID to fetch the info from.
5974
 * @return array An array of IDs of messages in the specified topic that the current user likes
5975
 */
5976
function prepareLikesContext($topic)
5977
{
5978
	global $user_info, $smcFunc;
5979
5980
	// Make sure we have something to work with.
5981
	if (empty($topic))
5982
		return array();
5983
5984
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
5985
	$user = $user_info['id'];
5986
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
5987
	$ttl = 180;
5988
5989
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
5990
	{
5991
		$temp = array();
5992
		$request = $smcFunc['db_query']('', '
5993
			SELECT content_id
5994
			FROM {db_prefix}user_likes AS l
5995
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
5996
			WHERE l.id_member = {int:current_user}
5997
				AND l.content_type = {literal:msg}
5998
				AND m.id_topic = {int:topic}',
5999
			array(
6000
				'current_user' => $user,
6001
				'topic' => $topic,
6002
			)
6003
		);
6004
		while ($row = $smcFunc['db_fetch_assoc']($request))
6005
			$temp[] = (int) $row['content_id'];
6006
6007
		cache_put_data($cache_key, $temp, $ttl);
6008
	}
6009
6010
	return $temp;
6011
}
6012
6013
/**
6014
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
6015
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
6016
 * that are not normally displayable.  This converts the popular ones that
6017
 * appear from a cut and paste from windows.
6018
 *
6019
 * @param string $string The string
6020
 * @return string The sanitized string
6021
 */
6022
function sanitizeMSCutPaste($string)
6023
{
6024
	global $context;
6025
6026
	if (empty($string))
6027
		return $string;
6028
6029
	// UTF-8 occurences of MS special characters
6030
	$findchars_utf8 = array(
6031
		"\xe2\x80\x9a",	// single low-9 quotation mark
6032
		"\xe2\x80\x9e",	// double low-9 quotation mark
6033
		"\xe2\x80\xa6",	// horizontal ellipsis
6034
		"\xe2\x80\x98",	// left single curly quote
6035
		"\xe2\x80\x99",	// right single curly quote
6036
		"\xe2\x80\x9c",	// left double curly quote
6037
		"\xe2\x80\x9d",	// right double curly quote
6038
	);
6039
6040
	// windows 1252 / iso equivalents
6041
	$findchars_iso = array(
6042
		chr(130),
6043
		chr(132),
6044
		chr(133),
6045
		chr(145),
6046
		chr(146),
6047
		chr(147),
6048
		chr(148),
6049
	);
6050
6051
	// safe replacements
6052
	$replacechars = array(
6053
		',',	// &sbquo;
6054
		',,',	// &bdquo;
6055
		'...',	// &hellip;
6056
		"'",	// &lsquo;
6057
		"'",	// &rsquo;
6058
		'"',	// &ldquo;
6059
		'"',	// &rdquo;
6060
	);
6061
6062
	if ($context['utf8'])
6063
		$string = str_replace($findchars_utf8, $replacechars, $string);
6064
	else
6065
		$string = str_replace($findchars_iso, $replacechars, $string);
6066
6067
	return $string;
6068
}
6069
6070
/**
6071
 * Return a Gravatar URL based on
6072
 * - the supplied email address,
6073
 * - the global maximum rating,
6074
 * - the global default fallback,
6075
 * - maximum sizes as set in the admin panel.
6076
 *
6077
 * It is SSL aware, and caches most of the parameters.
6078
 *
6079
 * @param string $email_address The user's email address
6080
 * @return string The gravatar URL
6081
 */
6082
function get_gravatar_url($email_address)
6083
{
6084
	global $modSettings, $smcFunc;
6085
	static $url_params = null;
6086
6087
	if ($url_params === null)
6088
	{
6089
		$ratings = array('G', 'PG', 'R', 'X');
6090
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
6091
		$url_params = array();
6092
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
6093
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
6094
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
6095
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
6096
		if (!empty($modSettings['avatar_max_width_external']))
6097
			$size_string = (int) $modSettings['avatar_max_width_external'];
6098
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
6099
			if ((int) $modSettings['avatar_max_height_external'] < $size_string)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $size_string does not seem to be defined for all execution paths leading up to this point.
Loading history...
6100
				$size_string = $modSettings['avatar_max_height_external'];
6101
6102
		if (!empty($size_string))
6103
			$url_params[] = 's=' . $size_string;
6104
	}
6105
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
6106
6107
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
6108
}
6109
6110
/**
6111
 * Get a list of time zones.
6112
 *
6113
 * @param string $when The date/time for which to calculate the time zone values.
6114
 *		May be a Unix timestamp or any string that strtotime() can understand.
6115
 *		Defaults to 'now'.
6116
 * @return array An array of time zone identifiers and label text.
6117
 */
6118
function smf_list_timezones($when = 'now')
6119
{
6120
	global $modSettings, $tztxt, $txt, $context, $cur_profile, $sourcedir;
6121
	static $timezones_when = array();
6122
6123
	require_once($sourcedir . '/Subs-Timezones.php');
6124
6125
	// Parseable datetime string?
6126
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
6127
		$when = $timestamp;
6128
6129
	// A Unix timestamp?
6130
	elseif (is_numeric($when))
6131
		$when = intval($when);
6132
6133
	// Invalid value? Just get current Unix timestamp.
6134
	else
6135
		$when = time();
6136
6137
	// No point doing this over if we already did it once
6138
	if (isset($timezones_when[$when]))
6139
		return $timezones_when[$when];
6140
6141
	// We'll need these too
6142
	$date_when = date_create('@' . $when);
6143
	$later = strtotime('@' . $when . ' + 1 year');
6144
6145
	// Load up any custom time zone descriptions we might have
6146
	loadLanguage('Timezones');
6147
6148
	$tzid_metazones = get_tzid_metazones($later);
6149
6150
	// Should we put time zones from certain countries at the top of the list?
6151
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
6152
6153
	$priority_tzids = array();
6154
	foreach ($priority_countries as $country)
6155
	{
6156
		$country_tzids = get_sorted_tzids_for_country($country);
6157
6158
		if (!empty($country_tzids))
6159
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
6160
	}
6161
6162
	// Antarctic research stations should be listed last, unless you're running a penguin forum
6163
	$low_priority_tzids = !in_array('AQ', $priority_countries) ? timezone_identifiers_list(DateTimeZone::ANTARCTICA) : array();
0 ignored issues
show
Bug introduced by
Are you sure the usage of timezone_identifiers_lis...teTimeZone::ANTARCTICA) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
6164
6165
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), timezone_identifiers_list())), $priority_tzids, $low_priority_tzids);
0 ignored issues
show
Bug introduced by
Are you sure the usage of timezone_identifiers_list() is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
timezone_identifiers_list() of type void is incompatible with the type array expected by parameter $arrays of array_merge(). ( Ignorable by Annotation )

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

6165
	$normal_priority_tzids = array_diff(array_unique(array_merge(array_keys($tzid_metazones), /** @scrutinizer ignore-type */ timezone_identifiers_list())), $priority_tzids, $low_priority_tzids);
Loading history...
6166
6167
	// Process them in order of importance.
6168
	$tzids = array_merge($priority_tzids, $normal_priority_tzids, $low_priority_tzids);
6169
6170
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
6171
	$dst_types = array();
6172
	$labels = array();
6173
	$offsets = array();
6174
	foreach ($tzids as $tzid)
6175
	{
6176
		// We don't want UTC right now
6177
		if ($tzid == 'UTC')
6178
			continue;
6179
6180
		$tz = @timezone_open($tzid);
6181
6182
		if ($tz == null)
6183
			continue;
6184
6185
		// First, get the set of transition rules for this tzid
6186
		$tzinfo = timezone_transitions_get($tz, $when, $later);
6187
6188
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
6189
		$tzkey = serialize($tzinfo);
6190
6191
		// ...But make sure to include all explicitly defined meta-zones.
6192
		if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6193
			$tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid])));
6194
6195
		// Don't overwrite our preferred tzids
6196
		if (empty($zones[$tzkey]['tzid']))
6197
		{
6198
			$zones[$tzkey]['tzid'] = $tzid;
6199
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6200
6201
			foreach ($tzinfo as $transition) {
6202
				$zones[$tzkey]['abbrs'][] = $transition['abbr'];
6203
			}
6204
6205
			if (isset($tzid_metazones[$tzid]))
6206
				$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6207
			else
6208
			{
6209
				$tzgeo = timezone_location_get($tz);
6210
				$country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']);
6211
6212
				if (count($country_tzids) === 1)
6213
					$zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']];
6214
			}
6215
		}
6216
6217
		// A time zone from a prioritized country?
6218
		if (in_array($tzid, $priority_tzids))
6219
			$priority_zones[$tzkey] = true;
6220
6221
		// Keep track of the location and offset for this tzid
6222
		if (!empty($txt[$tzid]))
6223
			$zones[$tzkey]['locations'][] = $txt[$tzid];
6224
		else
6225
		{
6226
			$tzid_parts = explode('/', $tzid);
6227
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6228
		}
6229
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6230
6231
		// Figure out the "meta-zone" info for the label
6232
		if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6233
		{
6234
			$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6235
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6236
		}
6237
		$dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f');
6238
		$labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : '';
6239
6240
		// Remember this for later
6241
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6242
			$member_tzkey = $tzkey;
6243
		if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid)
6244
			$event_tzkey = $tzkey;
6245
	}
6246
6247
	// Sort by offset, then label, then DST type.
6248
	array_multisort($offsets, SORT_ASC, SORT_NUMERIC, $labels, SORT_ASC, $dst_types, SORT_ASC, $zones);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
Bug introduced by
SORT_NUMERIC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

6248
	array_multisort($offsets, SORT_ASC, /** @scrutinizer ignore-type */ SORT_NUMERIC, $labels, SORT_ASC, $dst_types, SORT_ASC, $zones);
Loading history...
Bug introduced by
SORT_ASC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

6248
	array_multisort($offsets, /** @scrutinizer ignore-type */ SORT_ASC, SORT_NUMERIC, $labels, SORT_ASC, $dst_types, SORT_ASC, $zones);
Loading history...
6249
6250
	// Build the final array of formatted values
6251
	$priority_timezones = array();
6252
	$timezones = array();
6253
	foreach ($zones as $tzkey => $tzvalue)
6254
	{
6255
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
6256
6257
		// Use the human friendly time zone name, if there is one.
6258
		$desc = '';
6259
		if (!empty($tzvalue['metazone']))
6260
		{
6261
			if (!empty($tztxt[$tzvalue['metazone']]))
6262
				$metazone = $tztxt[$tzvalue['metazone']];
6263
			else
6264
				$metazone = sprintf($tztxt['generic_timezone'], $tzvalue['metazone'], '%1$s');
6265
6266
			switch ($tzvalue['dst_type'])
6267
			{
6268
				case 0:
6269
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_false']);
6270
					break;
6271
6272
				case 1:
6273
					$desc = sprintf($metazone, '');
6274
					break;
6275
6276
				case 2:
6277
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_true']);
6278
					break;
6279
			}
6280
		}
6281
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6282
		else
6283
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6284
6285
		// We don't want abbreviations like '+03' or '-11'.
6286
		$abbrs = array_filter($tzvalue['abbrs'], function ($abbr) {
6287
			return !strspn($abbr, '+-');
6288
		});
6289
		$abbrs = count($abbrs) == count($tzvalue['abbrs']) ? array_unique($abbrs) : array();
6290
6291
		// Show the UTC offset and abbreviation(s).
6292
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . str_replace('  ', ' ', $desc) . (!empty($abbrs) ? ' (' . implode('/', $abbrs) . ')' : '');
6293
6294
		if (isset($priority_zones[$tzkey]))
6295
			$priority_timezones[$tzvalue['tzid']] = $desc;
6296
		else
6297
			$timezones[$tzvalue['tzid']] = $desc;
6298
6299
		// Automatically fix orphaned time zones.
6300
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6301
			$cur_profile['timezone'] = $tzvalue['tzid'];
6302
		if (isset($event_tzkey) && $event_tzkey == $tzkey)
6303
			$context['event']['tz'] = $tzvalue['tzid'];
6304
	}
6305
6306
	if (!empty($priority_timezones))
6307
		$priority_timezones[] = '-----';
6308
6309
	$timezones = array_merge(
6310
		$priority_timezones,
6311
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6312
		$timezones
6313
	);
6314
6315
	$timezones_when[$when] = $timezones;
6316
6317
	return $timezones_when[$when];
6318
}
6319
6320
/**
6321
 * Gets a member's selected time zone identifier
6322
 *
6323
 * @param int $id_member The member id to look up. If not provided, the current user's id will be used.
6324
 * @return string The time zone identifier string for the user's time zone.
6325
 */
6326
function getUserTimezone($id_member = null)
6327
{
6328
	global $smcFunc, $context, $user_info, $modSettings, $user_settings;
6329
	static $member_cache = array();
6330
6331
	if (is_null($id_member) && $user_info['is_guest'] == false)
6332
		$id_member = $context['user']['id'];
6333
6334
	// Did we already look this up?
6335
	if (isset($id_member) && isset($member_cache[$id_member]))
6336
	{
6337
		return $member_cache[$id_member];
6338
	}
6339
6340
	// Check if we already have this in $user_settings.
6341
	if (isset($user_settings['id_member']) && $user_settings['id_member'] == $id_member && !empty($user_settings['timezone']))
6342
	{
6343
		$member_cache[$id_member] = $user_settings['timezone'];
6344
		return $user_settings['timezone'];
6345
	}
6346
6347
	// Look it up in the database.
6348
	if (isset($id_member))
6349
	{
6350
		$request = $smcFunc['db_query']('', '
6351
			SELECT timezone
6352
			FROM {db_prefix}members
6353
			WHERE id_member = {int:id_member}',
6354
			array(
6355
				'id_member' => $id_member,
6356
			)
6357
		);
6358
		list($timezone) = $smcFunc['db_fetch_row']($request);
6359
		$smcFunc['db_free_result']($request);
6360
	}
6361
6362
	// If it is invalid, fall back to the default.
6363
	if (empty($timezone) || !in_array($timezone, timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
0 ignored issues
show
Bug introduced by
Are you sure the usage of timezone_identifiers_lis...eTimeZone::ALL_WITH_BC) is correct as it seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
timezone_identifiers_lis...eTimeZone::ALL_WITH_BC) of type void is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

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

6363
	if (empty($timezone) || !in_array($timezone, /** @scrutinizer ignore-type */ timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
Loading history...
6364
		$timezone = isset($modSettings['default_timezone']) ? $modSettings['default_timezone'] : date_default_timezone_get();
6365
6366
	// Save for later.
6367
	if (isset($id_member))
6368
		$member_cache[$id_member] = $timezone;
6369
6370
	return $timezone;
6371
}
6372
6373
/**
6374
 * Converts an IP address into binary
6375
 *
6376
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
6377
 * @return string|false The IP address in binary or false
6378
 */
6379
function inet_ptod($ip_address)
6380
{
6381
	if (!isValidIP($ip_address))
6382
		return $ip_address;
6383
6384
	$bin = inet_pton($ip_address);
6385
	return $bin;
6386
}
6387
6388
/**
6389
 * Converts a binary version of an IP address into a readable format
6390
 *
6391
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
6392
 * @return string|false The IP address in presentation format or false on error
6393
 */
6394
function inet_dtop($bin)
6395
{
6396
	global $db_type;
6397
6398
	if (empty($bin))
6399
		return '';
6400
	elseif ($db_type == 'postgresql')
6401
		return $bin;
6402
	// Already a String?
6403
	elseif (isValidIP($bin))
6404
		return $bin;
6405
	return inet_ntop($bin);
6406
}
6407
6408
/**
6409
 * Safe serialize() and unserialize() replacements
6410
 *
6411
 * @license Public Domain
6412
 *
6413
 * @author anthon (dot) pang (at) gmail (dot) com
6414
 */
6415
6416
/**
6417
 * Safe serialize() replacement. Recursive
6418
 * - output a strict subset of PHP's native serialized representation
6419
 * - does not serialize objects
6420
 *
6421
 * @param mixed $value
6422
 * @return string
6423
 */
6424
function _safe_serialize($value)
6425
{
6426
	if (is_null($value))
6427
		return 'N;';
6428
6429
	if (is_bool($value))
6430
		return 'b:' . (int) $value . ';';
6431
6432
	if (is_int($value))
6433
		return 'i:' . $value . ';';
6434
6435
	if (is_float($value))
6436
		return 'd:' . str_replace(',', '.', $value) . ';';
6437
6438
	if (is_string($value))
6439
		return 's:' . strlen($value) . ':"' . $value . '";';
6440
6441
	if (is_array($value))
6442
	{
6443
		$out = '';
6444
		foreach ($value as $k => $v)
6445
			$out .= _safe_serialize($k) . _safe_serialize($v);
6446
6447
		return 'a:' . count($value) . ':{' . $out . '}';
6448
	}
6449
6450
	// safe_serialize cannot serialize resources or objects.
6451
	return false;
6452
}
6453
6454
/**
6455
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
6456
 *
6457
 * @param mixed $value
6458
 * @return string
6459
 */
6460
function safe_serialize($value)
6461
{
6462
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6463
	if (function_exists('mb_internal_encoding') &&
6464
		(((int) ini_get('mbstring.func_overload')) & 2))
6465
	{
6466
		$mbIntEnc = mb_internal_encoding();
6467
		mb_internal_encoding('ASCII');
6468
	}
6469
6470
	$out = _safe_serialize($value);
6471
6472
	if (isset($mbIntEnc))
6473
		mb_internal_encoding($mbIntEnc);
6474
6475
	return $out;
6476
}
6477
6478
/**
6479
 * Safe unserialize() replacement
6480
 * - accepts a strict subset of PHP's native serialized representation
6481
 * - does not unserialize objects
6482
 *
6483
 * @param string $str
6484
 * @return mixed
6485
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
6486
 */
6487
function _safe_unserialize($str)
6488
{
6489
	// Input  is not a string.
6490
	if (empty($str) || !is_string($str))
6491
		return false;
6492
6493
	$stack = array();
6494
	$expected = array();
6495
6496
	/*
6497
	 * states:
6498
	 *   0 - initial state, expecting a single value or array
6499
	 *   1 - terminal state
6500
	 *   2 - in array, expecting end of array or a key
6501
	 *   3 - in array, expecting value or another array
6502
	 */
6503
	$state = 0;
6504
	while ($state != 1)
6505
	{
6506
		$type = isset($str[0]) ? $str[0] : '';
6507
		if ($type == '}')
6508
			$str = substr($str, 1);
6509
6510
		elseif ($type == 'N' && $str[1] == ';')
6511
		{
6512
			$value = null;
6513
			$str = substr($str, 2);
6514
		}
6515
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
6516
		{
6517
			$value = $matches[1] == '1' ? true : false;
6518
			$str = substr($str, 4);
6519
		}
6520
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
6521
		{
6522
			$value = (int) $matches[1];
6523
			$str = $matches[2];
6524
		}
6525
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
6526
		{
6527
			$value = (float) $matches[1];
6528
			$str = $matches[3];
6529
		}
6530
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
6531
		{
6532
			$value = substr($matches[2], 0, (int) $matches[1]);
6533
			$str = substr($matches[2], (int) $matches[1] + 2);
6534
		}
6535
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
6536
		{
6537
			$expectedLength = (int) $matches[1];
6538
			$str = $matches[2];
6539
		}
6540
6541
		// Object or unknown/malformed type.
6542
		else
6543
			return false;
6544
6545
		switch ($state)
6546
		{
6547
			case 3: // In array, expecting value or another array.
6548
				if ($type == 'a')
6549
				{
6550
					$stack[] = &$list;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $list does not seem to be defined for all execution paths leading up to this point.
Loading history...
6551
					$list[$key] = array();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $key does not seem to be defined for all execution paths leading up to this point.
Loading history...
6552
					$list = &$list[$key];
6553
					$expected[] = $expectedLength;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $expectedLength does not seem to be defined for all execution paths leading up to this point.
Loading history...
6554
					$state = 2;
6555
					break;
6556
				}
6557
				if ($type != '}')
6558
				{
6559
					$list[$key] = $value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.
Loading history...
6560
					$state = 2;
6561
					break;
6562
				}
6563
6564
				// Missing array value.
6565
				return false;
6566
6567
			case 2: // in array, expecting end of array or a key
6568
				if ($type == '}')
6569
				{
6570
					// Array size is less than expected.
6571
					if (count($list) < end($expected))
6572
						return false;
6573
6574
					unset($list);
6575
					$list = &$stack[count($stack) - 1];
6576
					array_pop($stack);
6577
6578
					// Go to terminal state if we're at the end of the root array.
6579
					array_pop($expected);
6580
6581
					if (count($expected) == 0)
6582
						$state = 1;
6583
6584
					break;
6585
				}
6586
6587
				if ($type == 'i' || $type == 's')
6588
				{
6589
					// Array size exceeds expected length.
6590
					if (count($list) >= end($expected))
6591
						return false;
6592
6593
					$key = $value;
6594
					$state = 3;
6595
					break;
6596
				}
6597
6598
				// Illegal array index type.
6599
				return false;
6600
6601
			// Expecting array or value.
6602
			case 0:
6603
				if ($type == 'a')
6604
				{
6605
					$data = array();
6606
					$list = &$data;
6607
					$expected[] = $expectedLength;
6608
					$state = 2;
6609
					break;
6610
				}
6611
6612
				if ($type != '}')
6613
				{
6614
					$data = $value;
6615
					$state = 1;
6616
					break;
6617
				}
6618
6619
				// Not in array.
6620
				return false;
6621
		}
6622
	}
6623
6624
	// Trailing data in input.
6625
	if (!empty($str))
6626
		return false;
6627
6628
	return $data;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data does not seem to be defined for all execution paths leading up to this point.
Loading history...
6629
}
6630
6631
/**
6632
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
6633
 *
6634
 * @param string $str
6635
 * @return mixed
6636
 */
6637
function safe_unserialize($str)
6638
{
6639
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6640
	if (function_exists('mb_internal_encoding') &&
6641
		(((int) ini_get('mbstring.func_overload')) & 0x02))
6642
	{
6643
		$mbIntEnc = mb_internal_encoding();
6644
		mb_internal_encoding('ASCII');
6645
	}
6646
6647
	$out = _safe_unserialize($str);
6648
6649
	if (isset($mbIntEnc))
6650
		mb_internal_encoding($mbIntEnc);
6651
6652
	return $out;
6653
}
6654
6655
/**
6656
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
6657
 *
6658
 * @param string $file The file/dir full path.
6659
 * @param int $value Not needed, added for legacy reasons.
6660
 * @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.
6661
 */
6662
function smf_chmod($file, $value = 0)
6663
{
6664
	// No file? no checks!
6665
	if (empty($file))
6666
		return false;
6667
6668
	// Already writable?
6669
	if (is_writable($file))
6670
		return true;
6671
6672
	// Do we have a file or a dir?
6673
	$isDir = is_dir($file);
6674
	$isWritable = false;
6675
6676
	// Set different modes.
6677
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
6678
6679
	foreach ($chmodValues as $val)
6680
	{
6681
		// If it's writable, break out of the loop.
6682
		if (is_writable($file))
6683
		{
6684
			$isWritable = true;
6685
			break;
6686
		}
6687
6688
		else
6689
			@chmod($file, $val);
6690
	}
6691
6692
	return $isWritable;
6693
}
6694
6695
/**
6696
 * Wrapper function for json_decode() with error handling.
6697
 *
6698
 * @param string $json The string to decode.
6699
 * @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.
6700
 * @param bool $logIt To specify if the error will be logged if theres any.
6701
 * @return array Either an empty array or the decoded data as an array.
6702
 */
6703
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
6704
{
6705
	global $txt;
6706
6707
	// Come on...
6708
	if (empty($json) || !is_string($json))
6709
		return array();
6710
6711
	$returnArray = @json_decode($json, $returnAsArray);
6712
6713
	// PHP 5.3 so no json_last_error_msg()
6714
	switch (json_last_error())
6715
	{
6716
		case JSON_ERROR_NONE:
6717
			$jsonError = false;
6718
			break;
6719
		case JSON_ERROR_DEPTH:
6720
			$jsonError = 'JSON_ERROR_DEPTH';
6721
			break;
6722
		case JSON_ERROR_STATE_MISMATCH:
6723
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
6724
			break;
6725
		case JSON_ERROR_CTRL_CHAR:
6726
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
6727
			break;
6728
		case JSON_ERROR_SYNTAX:
6729
			$jsonError = 'JSON_ERROR_SYNTAX';
6730
			break;
6731
		case JSON_ERROR_UTF8:
6732
			$jsonError = 'JSON_ERROR_UTF8';
6733
			break;
6734
		default:
6735
			$jsonError = 'unknown';
6736
			break;
6737
	}
6738
6739
	// Something went wrong!
6740
	if (!empty($jsonError) && $logIt)
6741
	{
6742
		// Being a wrapper means we lost our smf_error_handler() privileges :(
6743
		$jsonDebug = debug_backtrace();
6744
		$jsonDebug = $jsonDebug[0];
6745
		loadLanguage('Errors');
6746
6747
		if (!empty($jsonDebug))
6748
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
6749
6750
		else
6751
			log_error($txt['json_' . $jsonError], 'critical');
6752
6753
		// Everyone expects an array.
6754
		return array();
6755
	}
6756
6757
	return $returnArray;
6758
}
6759
6760
/**
6761
 * Check the given String if he is a valid IPv4 or IPv6
6762
 * return true or false
6763
 *
6764
 * @param string $IPString
6765
 *
6766
 * @return bool
6767
 */
6768
function isValidIP($IPString)
6769
{
6770
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
6771
}
6772
6773
/**
6774
 * Outputs a response.
6775
 * It assumes the data is already a string.
6776
 *
6777
 * @param string $data The data to print
6778
 * @param string $type The content type. Defaults to Json.
6779
 * @return void
6780
 */
6781
function smf_serverResponse($data = '', $type = 'content-type: application/json')
6782
{
6783
	global $db_show_debug, $modSettings;
6784
6785
	// Defensive programming anyone?
6786
	if (empty($data))
6787
		return false;
6788
6789
	// Don't need extra stuff...
6790
	$db_show_debug = false;
6791
6792
	// Kill anything else.
6793
	ob_end_clean();
6794
6795
	if (!empty($modSettings['CompressedOutput']))
6796
		@ob_start('ob_gzhandler');
6797
6798
	else
6799
		ob_start();
6800
6801
	// Set the header.
6802
	header($type);
6803
6804
	// Echo!
6805
	echo $data;
6806
6807
	// Done.
6808
	obExit(false);
6809
}
6810
6811
/**
6812
 * Creates an optimized regex to match all known top level domains.
6813
 *
6814
 * The optimized regex is stored in $modSettings['tld_regex'].
6815
 *
6816
 * To update the stored version of the regex to use the latest list of valid
6817
 * TLDs from iana.org, set the $update parameter to true. Updating can take some
6818
 * time, based on network connectivity, so it should normally only be done by
6819
 * calling this function from a background or scheduled task.
6820
 *
6821
 * If $update is not true, but the regex is missing or invalid, the regex will
6822
 * be regenerated from a hard-coded list of TLDs. This regenerated regex will be
6823
 * overwritten on the next scheduled update.
6824
 *
6825
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
6826
 */
6827
function set_tld_regex($update = false)
6828
{
6829
	global $sourcedir, $smcFunc, $modSettings;
6830
	static $done = false;
6831
6832
	// If we don't need to do anything, don't
6833
	if (!$update && $done)
6834
		return;
6835
6836
	// Should we get a new copy of the official list of TLDs?
6837
	if ($update)
6838
	{
6839
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
6840
		$tlds_md5 = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt.md5');
6841
6842
		/**
6843
		 * If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
6844
		 * We're probably running on a server hidden in a bunker deep underground to protect
6845
		 * it from marauding bandits roaming on the surface. We don't want to waste precious
6846
		 * electricity on pointlessly repeating background tasks, so we'll wait until the next
6847
		 * regularly scheduled update to see if civilization has been restored.
6848
		 */
6849
		if ($tlds === false || $tlds_md5 === false)
6850
			$postapocalypticNightmare = true;
6851
6852
		// Make sure nothing went horribly wrong along the way.
6853
		if (md5($tlds) != substr($tlds_md5, 0, 32))
0 ignored issues
show
Bug introduced by
It seems like $tlds can also be of type false; however, parameter $string of md5() does only seem to accept string, 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

6853
		if (md5(/** @scrutinizer ignore-type */ $tlds) != substr($tlds_md5, 0, 32))
Loading history...
Bug introduced by
It seems like $tlds_md5 can also be of type false; however, parameter $string of substr() does only seem to accept string, 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

6853
		if (md5($tlds) != substr(/** @scrutinizer ignore-type */ $tlds_md5, 0, 32))
Loading history...
6854
			$tlds = array();
6855
	}
6856
	// If we aren't updating and the regex is valid, we're done
6857
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', null) !== false)
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type string expected by parameter $subject of preg_match(). ( Ignorable by Annotation )

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

6857
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', /** @scrutinizer ignore-type */ null) !== false)
Loading history...
6858
	{
6859
		$done = true;
6860
		return;
6861
	}
6862
6863
	// If we successfully got an update, process the list into an array
6864
	if (!empty($tlds))
6865
	{
6866
		// Clean $tlds and convert it to an array
6867
		$tlds = array_filter(explode("\n", strtolower($tlds)), function($line)
6868
		{
6869
			$line = trim($line);
6870
			if (empty($line) || strlen($line) != strspn($line, 'abcdefghijklmnopqrstuvwxyz0123456789-'))
6871
				return false;
6872
			else
6873
				return true;
6874
		});
6875
6876
		// Convert Punycode to Unicode
6877
		require_once($sourcedir . '/Class-Punycode.php');
6878
		$Punycode = new Punycode();
6879
		$tlds = array_map(function($input) use ($Punycode)
6880
		{
6881
			return $Punycode->decode($input);
6882
		}, $tlds);
6883
	}
6884
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
6885
	else
6886
	{
6887
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz',
6888
			'cat', 'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post',
6889
			'pro', 'tel', 'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al',
6890
			'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',
6891
			'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
6892
			'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm',
6893
			'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do',
6894
			'dz', 'ec', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo',
6895
			'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp',
6896
			'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',
6897
			'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo',
6898
			'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la',
6899
			'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md',
6900
			'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt',
6901
			'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl',
6902
			'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl',
6903
			'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw',
6904
			'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
6905
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg',
6906
			'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',
6907
			'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
6908
			'ws', 'ye', 'yt', 'za', 'zm', 'zw',
6909
		);
6910
6911
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
6912
		if (empty($postapocalypticNightmare))
6913
		{
6914
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
6915
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
6916
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
6917
			);
6918
		}
6919
	}
6920
6921
	// Tack on some "special use domain names" that aren't in DNS but may possibly resolve.
6922
	// See https://www.iana.org/assignments/special-use-domain-names/ for more info.
6923
	$tlds = array_merge($tlds, array('local', 'onion', 'test'));
6924
6925
	// Get an optimized regex to match all the TLDs
6926
	$tld_regex = build_regex($tlds);
6927
6928
	// Remember the new regex in $modSettings
6929
	updateSettings(array('tld_regex' => $tld_regex));
6930
6931
	// Redundant repetition is redundant
6932
	$done = true;
6933
}
6934
6935
/**
6936
 * Creates optimized regular expressions from an array of strings.
6937
 *
6938
 * An optimized regex built using this function will be much faster than a
6939
 * simple regex built using `implode('|', $strings)` --- anywhere from several
6940
 * times to several orders of magnitude faster.
6941
 *
6942
 * However, the time required to build the optimized regex is approximately
6943
 * equal to the time it takes to execute the simple regex. Therefore, it is only
6944
 * worth calling this function if the resulting regex will be used more than
6945
 * once.
6946
 *
6947
 * Because PHP places an upper limit on the allowed length of a regex, very
6948
 * large arrays of $strings may not fit in a single regex. Normally, the excess
6949
 * strings will simply be dropped. However, if the $returnArray parameter is set
6950
 * to true, this function will build as many regexes as necessary to accommodate
6951
 * everything in $strings and return them in an array. You will need to iterate
6952
 * through all elements of the returned array in order to test all possible
6953
 * matches.
6954
 *
6955
 * @param array $strings An array of strings to make a regex for.
6956
 * @param string $delim An optional delimiter character to pass to preg_quote().
6957
 * @param bool $returnArray If true, returns an array of regexes.
6958
 * @return string|array One or more regular expressions to match any of the input strings.
6959
 */
6960
function build_regex($strings, $delim = null, $returnArray = false)
6961
{
6962
	global $smcFunc;
6963
	static $regexes = array();
6964
6965
	// If it's not an array, there's not much to do. ;)
6966
	if (!is_array($strings))
0 ignored issues
show
introduced by
The condition is_array($strings) is always true.
Loading history...
6967
		return preg_quote(@strval($strings), $delim);
6968
6969
	$regex_key = md5(json_encode(array($strings, $delim, $returnArray)));
6970
6971
	if (isset($regexes[$regex_key]))
6972
		return $regexes[$regex_key];
6973
6974
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
6975
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
6976
	{
6977
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
6978
		{
6979
			$current_encoding = mb_internal_encoding();
6980
			mb_internal_encoding($string_encoding);
6981
		}
6982
6983
		$strlen = 'mb_strlen';
6984
		$substr = 'mb_substr';
6985
	}
6986
	else
6987
	{
6988
		$strlen = $smcFunc['strlen'];
6989
		$substr = $smcFunc['substr'];
6990
	}
6991
6992
	// This recursive function creates the index array from the strings
6993
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
6994
	{
6995
		static $depth = 0;
6996
		$depth++;
6997
6998
		$first = (string) @$substr($string, 0, 1);
6999
7000
		// No first character? That's no good.
7001
		if ($first === '')
7002
		{
7003
			// A nested array? Really? Ugh. Fine.
7004
			if (is_array($string) && $depth < 20)
7005
			{
7006
				foreach ($string as $str)
7007
					$index = $add_string_to_index($str, $index);
7008
			}
7009
7010
			$depth--;
7011
			return $index;
7012
		}
7013
7014
		if (empty($index[$first]))
7015
			$index[$first] = array();
7016
7017
		if ($strlen($string) > 1)
7018
		{
7019
			// Sanity check on recursion
7020
			if ($depth > 99)
7021
				$index[$first][$substr($string, 1)] = '';
7022
7023
			else
7024
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
7025
		}
7026
		else
7027
			$index[$first][''] = '';
7028
7029
		$depth--;
7030
		return $index;
7031
	};
7032
7033
	// This recursive function turns the index array into a regular expression
7034
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
7035
	{
7036
		static $depth = 0;
7037
		$depth++;
7038
7039
		// Absolute max length for a regex is 32768, but we might need wiggle room
7040
		$max_length = 30000;
7041
7042
		$regex = array();
7043
		$length = 0;
7044
7045
		foreach ($index as $key => $value)
7046
		{
7047
			$key_regex = preg_quote($key, $delim);
7048
			$new_key = $key;
7049
7050
			if (empty($value))
7051
				$sub_regex = '';
7052
			else
7053
			{
7054
				$sub_regex = $index_to_regex($value, $delim);
7055
7056
				if (count(array_keys($value)) == 1)
7057
				{
7058
					$new_key_array = explode('(?' . '>', $sub_regex);
7059
					$new_key .= $new_key_array[0];
7060
				}
7061
				else
7062
					$sub_regex = '(?' . '>' . $sub_regex . ')';
7063
			}
7064
7065
			if ($depth > 1)
7066
				$regex[$new_key] = $key_regex . $sub_regex;
7067
			else
7068
			{
7069
				if (($length += strlen($key_regex . $sub_regex) + 1) < $max_length || empty($regex))
7070
				{
7071
					$regex[$new_key] = $key_regex . $sub_regex;
7072
					unset($index[$key]);
7073
				}
7074
				else
7075
					break;
7076
			}
7077
		}
7078
7079
		// Sort by key length and then alphabetically
7080
		uksort($regex, function($k1, $k2) use (&$strlen)
7081
		{
7082
			$l1 = $strlen($k1);
7083
			$l2 = $strlen($k2);
7084
7085
			if ($l1 == $l2)
7086
				return strcmp($k1, $k2) > 0 ? 1 : -1;
7087
			else
7088
				return $l1 > $l2 ? -1 : 1;
7089
		});
7090
7091
		$depth--;
7092
		return implode('|', $regex);
7093
	};
7094
7095
	// Now that the functions are defined, let's do this thing
7096
	$index = array();
7097
	$regex = '';
7098
7099
	foreach ($strings as $string)
7100
		$index = $add_string_to_index($string, $index);
7101
7102
	if ($returnArray === true)
7103
	{
7104
		$regex = array();
7105
		while (!empty($index))
7106
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
7107
	}
7108
	else
7109
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
7110
7111
	// Restore PHP's internal character encoding to whatever it was originally
7112
	if (!empty($current_encoding))
7113
		mb_internal_encoding($current_encoding);
7114
7115
	$regexes[$regex_key] = $regex;
7116
	return $regex;
7117
}
7118
7119
/**
7120
 * Check if the passed url has an SSL certificate.
7121
 *
7122
 * Returns true if a cert was found & false if not.
7123
 *
7124
 * @param string $url to check, in $boardurl format (no trailing slash).
7125
 */
7126
function ssl_cert_found($url)
7127
{
7128
	// This check won't work without OpenSSL
7129
	if (!extension_loaded('openssl'))
7130
		return true;
7131
7132
	// First, strip the subfolder from the passed url, if any
7133
	$parsedurl = parse_url($url);
7134
	$url = 'ssl://' . $parsedurl['host'] . ':443';
7135
7136
	// Next, check the ssl stream context for certificate info
7137
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
7138
		$ssloptions = array("capture_peer_cert" => true);
7139
	else
7140
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
7141
7142
	$result = false;
7143
	$context = stream_context_create(array("ssl" => $ssloptions));
7144
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
7145
	if ($stream !== false)
7146
	{
7147
		$params = stream_context_get_params($stream);
7148
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
7149
	}
7150
	return $result;
7151
}
7152
7153
/**
7154
 * Check if the passed url has a redirect to https:// by querying headers.
7155
 *
7156
 * Returns true if a redirect was found & false if not.
7157
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
7158
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
7159
 *
7160
 * @param string $url to check, in $boardurl format (no trailing slash).
7161
 */
7162
function https_redirect_active($url)
7163
{
7164
	// Ask for the headers for the passed url, but via http...
7165
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
7166
	$url = str_ireplace('https://', 'http://', $url) . '/';
0 ignored issues
show
Bug introduced by
Are you sure str_ireplace('https://', 'http://', $url) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

7166
	$url = /** @scrutinizer ignore-type */ str_ireplace('https://', 'http://', $url) . '/';
Loading history...
7167
	$headers = @get_headers($url);
7168
	if ($headers === false)
7169
		return false;
7170
7171
	// Now to see if it came back https...
7172
	// First check for a redirect status code in first row (301, 302, 307)
7173
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
7174
		return false;
7175
7176
	// Search for the location entry to confirm https
7177
	$result = false;
7178
	foreach ($headers as $header)
7179
	{
7180
		if (stristr($header, 'Location: https://') !== false)
7181
		{
7182
			$result = true;
7183
			break;
7184
		}
7185
	}
7186
	return $result;
7187
}
7188
7189
/**
7190
 * Build query_wanna_see_board and query_see_board for a userid
7191
 *
7192
 * Returns array with keys query_wanna_see_board and query_see_board
7193
 *
7194
 * @param int $userid of the user
7195
 */
7196
function build_query_board($userid)
7197
{
7198
	global $user_info, $modSettings, $smcFunc, $db_prefix;
7199
7200
	$query_part = array();
7201
7202
	// If we come from cron, we can't have a $user_info.
7203
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
7204
	{
7205
		$groups = $user_info['groups'];
7206
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
7207
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
7208
	}
7209
	else
7210
	{
7211
		$request = $smcFunc['db_query']('', '
7212
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
7213
			FROM {db_prefix}members AS mem
7214
			WHERE mem.id_member = {int:id_member}
7215
			LIMIT 1',
7216
			array(
7217
				'id_member' => $userid,
7218
			)
7219
		);
7220
7221
		$row = $smcFunc['db_fetch_assoc']($request);
7222
7223
		if (empty($row['additional_groups']))
7224
			$groups = array($row['id_group'], $row['id_post_group']);
7225
		else
7226
			$groups = array_merge(
7227
				array($row['id_group'], $row['id_post_group']),
7228
				explode(',', $row['additional_groups'])
7229
			);
7230
7231
		// Because history has proven that it is possible for groups to go bad - clean up in case.
7232
		foreach ($groups as $k => $v)
7233
			$groups[$k] = (int) $v;
7234
7235
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
7236
7237
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
7238
	}
7239
7240
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
7241
	if ($can_see_all_boards)
7242
		$query_part['query_see_board'] = '1=1';
7243
	// Otherwise just the groups in $user_info['groups'].
7244
	else
7245
	{
7246
		$query_part['query_see_board'] = '
7247
			EXISTS (
7248
				SELECT bpv.id_board
7249
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7250
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
7251
					AND bpv.deny = 0
7252
					AND bpv.id_board = b.id_board
7253
			)';
7254
7255
		if (!empty($modSettings['deny_boards_access']))
7256
			$query_part['query_see_board'] .= '
7257
			AND NOT EXISTS (
7258
				SELECT bpv.id_board
7259
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7260
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
7261
					AND bpv.deny = 1
7262
					AND bpv.id_board = b.id_board
7263
			)';
7264
	}
7265
7266
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
7267
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
7268
7269
	// Build the list of boards they WANT to see.
7270
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
7271
7272
	// If they aren't ignoring any boards then they want to see all the boards they can see
7273
	if (empty($ignoreboards))
7274
	{
7275
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
7276
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
7277
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
7278
	}
7279
	// Ok I guess they don't want to see all the boards
7280
	else
7281
	{
7282
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7283
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7284
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7285
	}
7286
7287
	return $query_part;
7288
}
7289
7290
/**
7291
 * Check if the connection is using https.
7292
 *
7293
 * @return boolean true if connection used https
7294
 */
7295
function httpsOn()
7296
{
7297
	$secure = false;
7298
7299
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
7300
		$secure = true;
7301
	elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on')
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (! empty($_SERVER['HTTP_...FORWARDED_SSL'] == 'on', Probably Intended Meaning: ! empty($_SERVER['HTTP_X...ORWARDED_SSL'] == 'on')
Loading history...
7302
		$secure = true;
7303
7304
	return $secure;
7305
}
7306
7307
/**
7308
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
7309
 * with international characters (a.k.a. IRIs)
7310
 *
7311
 * @param string $iri The IRI to test.
7312
 * @param int $flags Optional flags to pass to filter_var()
7313
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
7314
 */
7315
function validate_iri($iri, $flags = null)
7316
{
7317
	$url = iri_to_url($iri);
7318
7319
	if (filter_var($url, FILTER_VALIDATE_URL, $flags) !== false)
0 ignored issues
show
Bug introduced by
It seems like $flags can also be of type null; however, parameter $options of filter_var() does only seem to accept array|integer, maybe add an additional type check? ( Ignorable by Annotation )

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

7319
	if (filter_var($url, FILTER_VALIDATE_URL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
7320
		return $iri;
7321
	else
7322
		return false;
7323
}
7324
7325
/**
7326
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
7327
 * with international characters (a.k.a. IRIs)
7328
 *
7329
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
7330
 * feed the result of this function to iri_to_url()
7331
 *
7332
 * @param string $iri The IRI to sanitize.
7333
 * @return string|bool The sanitized version of the IRI
7334
 */
7335
function sanitize_iri($iri)
7336
{
7337
	// Encode any non-ASCII characters (but not space or control characters of any sort)
7338
	$iri = preg_replace_callback('~[^\x00-\x7F\pZ\pC]~u', function($matches)
7339
	{
7340
		return rawurlencode($matches[0]);
7341
	}, $iri);
7342
7343
	// Perform normal sanitization
7344
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
7345
7346
	// Decode the non-ASCII characters
7347
	$iri = rawurldecode($iri);
7348
7349
	return $iri;
7350
}
7351
7352
/**
7353
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
7354
 *
7355
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
7356
 * standard URL encoding on the rest.
7357
 *
7358
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
7359
 * @return string|bool The URL version of the IRI.
7360
 */
7361
function iri_to_url($iri)
7362
{
7363
	global $sourcedir;
7364
7365
	$host = parse_url((strpos($iri, '://') === false ? 'http://' : '') . ltrim($iri, ':/'), PHP_URL_HOST);
7366
7367
	if (empty($host))
7368
		return $iri;
7369
7370
	// Convert the domain using the Punycode algorithm
7371
	require_once($sourcedir . '/Class-Punycode.php');
7372
	$Punycode = new Punycode();
7373
	$encoded_host = $Punycode->encode($host);
7374
	$pos = strpos($iri, $host);
7375
	$iri = substr_replace($iri, $encoded_host, $pos, strlen($host));
7376
7377
	// Encode any disallowed characters in the rest of the URL
7378
	$unescaped = array(
7379
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
7380
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
7381
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
7382
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
7383
		'%25' => '%',
7384
	);
7385
	$iri = strtr(rawurlencode($iri), $unescaped);
0 ignored issues
show
Bug introduced by
It seems like $iri can also be of type array; however, parameter $string of rawurlencode() does only seem to accept string, 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

7385
	$iri = strtr(rawurlencode(/** @scrutinizer ignore-type */ $iri), $unescaped);
Loading history...
7386
7387
	return $iri;
7388
}
7389
7390
/**
7391
 * Decodes a URL containing encoded international characters to UTF-8
7392
 *
7393
 * Decodes any Punycode encoded characters in the domain name, then uses
7394
 * standard URL decoding on the rest.
7395
 *
7396
 * @param string $url The pure ASCII version of a URL.
7397
 * @return string|bool The UTF-8 version of the URL.
7398
 */
7399
function url_to_iri($url)
7400
{
7401
	global $sourcedir;
7402
7403
	$host = parse_url((strpos($url, '://') === false ? 'http://' : '') . ltrim($url, ':/'), PHP_URL_HOST);
7404
7405
	if (empty($host))
7406
		return $url;
7407
7408
	// Decode the domain from Punycode
7409
	require_once($sourcedir . '/Class-Punycode.php');
7410
	$Punycode = new Punycode();
7411
	$decoded_host = $Punycode->decode($host);
7412
	$pos = strpos($url, $host);
7413
	$url = substr_replace($url, $decoded_host, $pos, strlen($host));
7414
7415
	// Decode the rest of the URL
7416
	$url = rawurldecode($url);
0 ignored issues
show
Bug introduced by
It seems like $url can also be of type array; however, parameter $string of rawurldecode() does only seem to accept string, 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

7416
	$url = rawurldecode(/** @scrutinizer ignore-type */ $url);
Loading history...
7417
7418
	return $url;
7419
}
7420
7421
/**
7422
 * Ensures SMF's scheduled tasks are being run as intended
7423
 *
7424
 * If the admin activated the cron_is_real_cron setting, but the cron job is
7425
 * not running things at least once per day, we need to go back to SMF's default
7426
 * behaviour using "web cron" JavaScript calls.
7427
 */
7428
function check_cron()
7429
{
7430
	global $modSettings, $smcFunc, $txt;
7431
7432
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
7433
	{
7434
		$request = $smcFunc['db_query']('', '
7435
			SELECT COUNT(*)
7436
			FROM {db_prefix}scheduled_tasks
7437
			WHERE disabled = {int:not_disabled}
7438
				AND next_time < {int:yesterday}',
7439
			array(
7440
				'not_disabled' => 0,
7441
				'yesterday' => time() - 84600,
7442
			)
7443
		);
7444
		list($overdue) = $smcFunc['db_fetch_row']($request);
7445
		$smcFunc['db_free_result']($request);
7446
7447
		// If we have tasks more than a day overdue, cron isn't doing its job.
7448
		if (!empty($overdue))
7449
		{
7450
			loadLanguage('ManageScheduledTasks');
7451
			log_error($txt['cron_not_working']);
7452
			updateSettings(array('cron_is_real_cron' => 0));
7453
		}
7454
		else
7455
			updateSettings(array('cron_last_checked' => time()));
7456
	}
7457
}
7458
7459
/**
7460
 * Sends an appropriate HTTP status header based on a given status code
7461
 *
7462
 * @param int $code The status code
7463
 * @param string $status The string for the status. Set automatically if not provided.
7464
 */
7465
function send_http_status($code, $status = '')
7466
{
7467
	$statuses = array(
7468
		206 => 'Partial Content',
7469
		304 => 'Not Modified',
7470
		400 => 'Bad Request',
7471
		403 => 'Forbidden',
7472
		404 => 'Not Found',
7473
		410 => 'Gone',
7474
		500 => 'Internal Server Error',
7475
		503 => 'Service Unavailable',
7476
	);
7477
7478
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
7479
7480
	if (!isset($statuses[$code]) && empty($status))
7481
		header($protocol . ' 500 Internal Server Error');
7482
	else
7483
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
7484
}
7485
7486
/**
7487
 * Concatenates an array of strings into a grammatically correct sentence list
7488
 *
7489
 * Uses formats defined in the language files to build the list appropropriately
7490
 * for the currently loaded language.
7491
 *
7492
 * @param array $list An array of strings to concatenate.
7493
 * @return string The localized sentence list.
7494
 */
7495
function sentence_list($list)
7496
{
7497
	global $txt;
7498
7499
	// Make sure the bare necessities are defined
7500
	if (empty($txt['sentence_list_format']['n']))
7501
		$txt['sentence_list_format']['n'] = '{series}';
7502
	if (!isset($txt['sentence_list_separator']))
7503
		$txt['sentence_list_separator'] = ', ';
7504
	if (!isset($txt['sentence_list_separator_alt']))
7505
		$txt['sentence_list_separator_alt'] = '; ';
7506
7507
	// Which format should we use?
7508
	if (isset($txt['sentence_list_format'][count($list)]))
7509
		$format = $txt['sentence_list_format'][count($list)];
7510
	else
7511
		$format = $txt['sentence_list_format']['n'];
7512
7513
	// Do we want the normal separator or the alternate?
7514
	$separator = $txt['sentence_list_separator'];
7515
	foreach ($list as $item)
7516
	{
7517
		if (strpos($item, $separator) !== false)
7518
		{
7519
			$separator = $txt['sentence_list_separator_alt'];
7520
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
7521
			break;
7522
		}
7523
	}
7524
7525
	$replacements = array();
7526
7527
	// Special handling for the last items on the list
7528
	$i = 0;
7529
	while (empty($done))
7530
	{
7531
		if (strpos($format, '{'. --$i . '}') !== false)
7532
			$replacements['{'. $i . '}'] = array_pop($list);
7533
		else
7534
			$done = true;
7535
	}
7536
	unset($done);
7537
7538
	// Special handling for the first items on the list
7539
	$i = 0;
7540
	while (empty($done))
7541
	{
7542
		if (strpos($format, '{'. ++$i . '}') !== false)
7543
			$replacements['{'. $i . '}'] = array_shift($list);
7544
		else
7545
			$done = true;
7546
	}
7547
	unset($done);
7548
7549
	// Whatever is left
7550
	$replacements['{series}'] = implode($separator, $list);
7551
7552
	// Do the deed
7553
	return strtr($format, $replacements);
7554
}
7555
7556
/**
7557
 * Truncate an array to a specified length
7558
 *
7559
 * @param array $array The array to truncate
7560
 * @param int $max_length The upperbound on the length
7561
 * @param int $deep How levels in an multidimensional array should the function take into account.
7562
 * @return array The truncated array
7563
 */
7564
function truncate_array($array, $max_length = 1900, $deep = 3)
7565
{
7566
	$array = (array) $array;
7567
7568
	$curr_length = array_length($array, $deep);
7569
7570
	if ($curr_length <= $max_length)
7571
		return $array;
7572
7573
	else
7574
	{
7575
		// Truncate each element's value to a reasonable length
7576
		$param_max = floor($max_length / count($array));
7577
7578
		$current_deep = $deep - 1;
7579
7580
		foreach ($array as $key => &$value)
7581
		{
7582
			if (is_array($value))
7583
				if ($current_deep > 0)
7584
					$value = truncate_array($value, $current_deep);
7585
7586
			else
7587
				$value = substr($value, 0, $param_max - strlen($key) - 5);
0 ignored issues
show
Bug introduced by
$value of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

7587
				$value = substr(/** @scrutinizer ignore-type */ $value, 0, $param_max - strlen($key) - 5);
Loading history...
Bug introduced by
$param_max - strlen($key) - 5 of type double is incompatible with the type integer|null expected by parameter $length of substr(). ( Ignorable by Annotation )

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

7587
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
7588
		}
7589
7590
		return $array;
7591
	}
7592
}
7593
7594
/**
7595
 * array_length Recursive
7596
 * @param array $array
7597
 * @param int $deep How many levels should the function
7598
 * @return int
7599
 */
7600
function array_length($array, $deep = 3)
7601
{
7602
	// Work with arrays
7603
	$array = (array) $array;
7604
	$length = 0;
7605
7606
	$deep_count = $deep - 1;
7607
7608
	foreach ($array as $value)
7609
	{
7610
		// Recursive?
7611
		if (is_array($value))
7612
		{
7613
			// No can't do
7614
			if ($deep_count <= 0)
7615
				continue;
7616
7617
			$length += array_length($value, $deep_count);
7618
		}
7619
		else
7620
			$length += strlen($value);
7621
	}
7622
7623
	return $length;
7624
}
7625
7626
/**
7627
 * Compares existance request variables against an array.
7628
 *
7629
 * The input array is associative, where keys denote accepted values
7630
 * in a request variable denoted by `$req_val`. Values can be:
7631
 *
7632
 * - another associative array where at least one key must be found
7633
 *   in the request and their values are accepted request values.
7634
 * - A scalar value, in which case no furthur checks are done.
7635
 *
7636
 * @param array $array
7637
 * @param string $req_var request variable
7638
 *
7639
 * @return bool whether any of the criteria was satisfied
7640
 */
7641
function is_filtered_request(array $array, $req_var)
7642
{
7643
	$matched = false;
7644
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
7645
	{
7646
		if (is_array($array[$_REQUEST[$req_var]]))
7647
		{
7648
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
7649
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
7650
		}
7651
		else
7652
			$matched = true;
7653
	}
7654
7655
	return (bool) $matched;
7656
}
7657
7658
/**
7659
 * Clean up the XML to make sure it doesn't contain invalid characters.
7660
 *
7661
 * See https://www.w3.org/TR/xml/#charsets
7662
 *
7663
 * @param string $string The string to clean
7664
 * @return string The cleaned string
7665
 */
7666
function cleanXml($string)
7667
{
7668
	global $context;
7669
7670
	$illegal_chars = array(
7671
		// Remove all ASCII control characters except \t, \n, and \r.
7672
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
7673
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
7674
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
7675
		"\x1E", "\x1F",
7676
		// Remove \xFFFE and \xFFFF
7677
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
7678
	);
7679
7680
	$string = str_replace($illegal_chars, '', $string);
7681
7682
	// The Unicode surrogate pair code points should never be present in our
7683
	// strings to begin with, but if any snuck in, they need to be removed.
7684
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
7685
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
7686
7687
	return $string;
7688
}
7689
7690
/**
7691
 * Escapes (replaces) characters in strings to make them safe for use in javascript
7692
 *
7693
 * @param string $string The string to escape
7694
 * @return string The escaped string
7695
 */
7696
function JavaScriptEscape($string)
7697
{
7698
	global $scripturl;
7699
7700
	return '\'' . strtr($string, array(
7701
		"\r" => '',
7702
		"\n" => '\\n',
7703
		"\t" => '\\t',
7704
		'\\' => '\\\\',
7705
		'\'' => '\\\'',
7706
		'</' => '<\' + \'/',
7707
		'<script' => '<scri\'+\'pt',
7708
		'<body>' => '<bo\'+\'dy>',
7709
		'<a href' => '<a hr\'+\'ef',
7710
		$scripturl => '\' + smf_scripturl + \'',
7711
	)) . '\'';
7712
}
7713
7714
?>