Passed
Push — release-2.1 ( 312769...2f400a )
by Jon
12:24 queued 06:16
created

sanitize_chars()   F

Complexity

Conditions 14
Paths 332

Size

Total Lines 66
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 14
eloc 31
c 2
b 0
f 0
nc 332
nop 3
dl 0
loc 66
rs 3.7833

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file has all the main functions in it that relate to, well, everything.
5
 *
6
 * Simple Machines Forum (SMF)
7
 *
8
 * @package SMF
9
 * @author Simple Machines 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 RC4
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
	// Windows requires a slightly different language code identifier (LCID).
853
	// https://msdn.microsoft.com/en-us/library/cc233982.aspx
854
	$lang_locale = $context['server']['is_windows'] ? strtr($txt['lang_locale'], '_', '-') : $txt['lang_locale'];
855
856
	// Make sure we are using the correct locale.
857
	if (!isset($locale) || ($process_safe === true && setlocale(LC_TIME, '0') != $locale))
858
		$locale = setlocale(LC_TIME, array($lang_locale . '.' . $modSettings['global_character_set'], $lang_locale . '.' . $txt['lang_character_set'], $lang_locale));
859
860
	// If the current locale is unsupported, we'll have to localize the hard way.
861
	if ($locale === false)
862
	{
863
		$timeformat = strtr($timeformat, array(
864
			'%a' => '#txt_days_short_%w#',
865
			'%A' => '#txt_days_%w#',
866
			'%b' => '#txt_months_short_%m#',
867
			'%B' => '#txt_months_%m#',
868
			'%p' => '&#37;p',
869
			'%P' => '&#37;p'
870
		));
871
	}
872
	// Just in case the locale doesn't support '%p' properly.
873
	// @todo Is this even necessary?
874
	else
875
	{
876
		if (!isset($non_twelve_hour) && strpos($timeformat, '%p') !== false)
877
			$non_twelve_hour = trim(strftime('%p')) === '';
878
879
		if (!empty($non_twelve_hour))
880
			$timeformat = strtr($timeformat, array(
881
				'%p' => '&#37;p',
882
				'%P' => '&#37;p'
883
			));
884
	}
885
886
	// And now, the moment we've all be waiting for...
887
	$timestring = strftime($timeformat, $log_time);
888
889
	// Do-it-yourself time localization.  Fun.
890
	if (strpos($timestring, '&#37;p') !== false)
891
		$timestring = str_replace('&#37;p', (strftime('%H', $log_time) < 12 ? $txt['time_am'] : $txt['time_pm']), $timestring);
892
	if (strpos($timestring, '#txt_') !== false)
893
	{
894
		if (strpos($timestring, '#txt_days_short_') !== false)
895
			$timestring = strtr($timestring, array(
896
				'#txt_days_short_0#' => $txt['days_short'][0],
897
				'#txt_days_short_1#' => $txt['days_short'][1],
898
				'#txt_days_short_2#' => $txt['days_short'][2],
899
				'#txt_days_short_3#' => $txt['days_short'][3],
900
				'#txt_days_short_4#' => $txt['days_short'][4],
901
				'#txt_days_short_5#' => $txt['days_short'][5],
902
				'#txt_days_short_6#' => $txt['days_short'][6],
903
			));
904
905
		if (strpos($timestring, '#txt_days_') !== false)
906
			$timestring = strtr($timestring, array(
907
				'#txt_days_0#' => $txt['days'][0],
908
				'#txt_days_1#' => $txt['days'][1],
909
				'#txt_days_2#' => $txt['days'][2],
910
				'#txt_days_3#' => $txt['days'][3],
911
				'#txt_days_4#' => $txt['days'][4],
912
				'#txt_days_5#' => $txt['days'][5],
913
				'#txt_days_6#' => $txt['days'][6],
914
			));
915
916
		if (strpos($timestring, '#txt_months_short_') !== false)
917
			$timestring = strtr($timestring, array(
918
				'#txt_months_short_01#' => $txt['months_short'][1],
919
				'#txt_months_short_02#' => $txt['months_short'][2],
920
				'#txt_months_short_03#' => $txt['months_short'][3],
921
				'#txt_months_short_04#' => $txt['months_short'][4],
922
				'#txt_months_short_05#' => $txt['months_short'][5],
923
				'#txt_months_short_06#' => $txt['months_short'][6],
924
				'#txt_months_short_07#' => $txt['months_short'][7],
925
				'#txt_months_short_08#' => $txt['months_short'][8],
926
				'#txt_months_short_09#' => $txt['months_short'][9],
927
				'#txt_months_short_10#' => $txt['months_short'][10],
928
				'#txt_months_short_11#' => $txt['months_short'][11],
929
				'#txt_months_short_12#' => $txt['months_short'][12],
930
			));
931
932
		if (strpos($timestring, '#txt_months_') !== false)
933
			$timestring = strtr($timestring, array(
934
				'#txt_months_01#' => $txt['months'][1],
935
				'#txt_months_02#' => $txt['months'][2],
936
				'#txt_months_03#' => $txt['months'][3],
937
				'#txt_months_04#' => $txt['months'][4],
938
				'#txt_months_05#' => $txt['months'][5],
939
				'#txt_months_06#' => $txt['months'][6],
940
				'#txt_months_07#' => $txt['months'][7],
941
				'#txt_months_08#' => $txt['months'][8],
942
				'#txt_months_09#' => $txt['months'][9],
943
				'#txt_months_10#' => $txt['months'][10],
944
				'#txt_months_11#' => $txt['months'][11],
945
				'#txt_months_12#' => $txt['months'][12],
946
			));
947
	}
948
949
	// Restore any literal percent characters, add the prefix, and we're done.
950
	return $prefix . str_replace('&#37;', '%', $timestring);
951
}
952
953
/**
954
 * Gets a version of a strftime() format that only shows the date or time components
955
 *
956
 * @param string $type Either 'date' or 'time'.
957
 * @param string $format A strftime() format to process. Defaults to $user_info['time_format'].
958
 * @return string A strftime() format string
959
 */
960
function get_date_or_time_format($type = '', $format = '')
961
{
962
	global $user_info, $modSettings;
963
	static $formats;
964
965
	// If the format is invalid, fall back to defaults.
966
	if (strpos($format, '%') === false)
967
		$format = !empty($user_info['time_format']) ? $user_info['time_format'] : (!empty($modSettings['time_format']) ? $modSettings['time_format'] : '%F %k:%M');
968
969
	$orig_format = $format;
970
971
	// Have we already done this?
972
	if (isset($formats[$orig_format][$type]))
973
		return $formats[$orig_format][$type];
974
975
	if ($type === 'date')
976
	{
977
		$specifications = array(
978
			// Day
979
			'%a' => '%a', '%A' => '%A', '%e' => '%e', '%d' => '%d', '%j' => '%j', '%u' => '%u', '%w' => '%w',
980
			// Week
981
			'%U' => '%U', '%V' => '%V', '%W' => '%W',
982
			// Month
983
			'%b' => '%b', '%B' => '%B', '%h' => '%h', '%m' => '%m',
984
			// Year
985
			'%C' => '%C', '%g' => '%g', '%G' => '%G', '%y' => '%y', '%Y' => '%Y',
986
			// Time
987
			'%H' => '', '%k' => '', '%I' => '', '%l' => '', '%M' => '', '%p' => '', '%P' => '',
988
			'%r' => '', '%R' => '', '%S' => '', '%T' => '', '%X' => '', '%z' => '', '%Z' => '',
989
			// Time and Date Stamps
990
			'%c' => '%x', '%D' => '%D', '%F' => '%F', '%s' => '%s', '%x' => '%x',
991
			// Miscellaneous
992
			'%n' => '', '%t' => '', '%%' => '%%',
993
		);
994
995
		$default_format = '%F';
996
	}
997
	elseif ($type === 'time')
998
	{
999
		$specifications = array(
1000
			// Day
1001
			'%a' => '', '%A' => '', '%e' => '', '%d' => '', '%j' => '', '%u' => '', '%w' => '',
1002
			// Week
1003
			'%U' => '', '%V' => '', '%W' => '',
1004
			// Month
1005
			'%b' => '', '%B' => '', '%h' => '', '%m' => '',
1006
			// Year
1007
			'%C' => '', '%g' => '', '%G' => '', '%y' => '', '%Y' => '',
1008
			// Time
1009
			'%H' => '%H', '%k' => '%k', '%I' => '%I', '%l' => '%l', '%M' => '%M', '%p' => '%p', '%P' => '%P',
1010
			'%r' => '%r', '%R' => '%R', '%S' => '%S', '%T' => '%T', '%X' => '%X', '%z' => '%z', '%Z' => '%Z',
1011
			// Time and Date Stamps
1012
			'%c' => '%X', '%D' => '', '%F' => '', '%s' => '%s', '%x' => '',
1013
			// Miscellaneous
1014
			'%n' => '', '%t' => '', '%%' => '%%',
1015
		);
1016
1017
		$default_format = '%k:%M';
1018
	}
1019
	// Invalid type requests just get the full format string.
1020
	else
1021
		return $format;
1022
1023
	// Separate the specifications we want from the ones we don't.
1024
	$wanted = array_filter($specifications);
1025
	$unwanted = array_diff(array_keys($specifications), $wanted);
1026
1027
	// First, make any necessary substitutions in the format.
1028
	$format = strtr($format, $wanted);
1029
1030
	// Next, strip out any specifications and literal text that we don't want.
1031
	$format_parts = preg_split('~%[' . (strtr(implode('', $unwanted), array('%' => ''))) . ']~u', $format);
1032
1033
	foreach ($format_parts as $p => $f)
1034
	{
1035
		if (strpos($f, '%') === false)
1036
			unset($format_parts[$p]);
1037
	}
1038
1039
	$format = implode('', $format_parts);
1040
1041
	// Finally, strip out any unwanted leftovers.
1042
	// 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
1043
	$format = preg_replace(
1044
		array(
1045
			// Anything that isn't a specification, punctuation mark, or whitespace.
1046
			'~(?<!%)\p{L}|[^\p{L}\p{P}\s]~u',
1047
			// A series of punctuation marks (except %), possibly separated by whitespace.
1048
			'~([^%\P{P}])(\s*)(?'.'>(\1|[^%\P{Po}])\s*(?!$))*~u',
1049
			// Unwanted trailing punctuation and whitespace.
1050
			'~(?'.'>([\p{Pd}\p{Ps}\p{Pi}\p{Pc}]|[^%\P{Po}])\s*)*$~u',
1051
			// Unwanted opening punctuation and whitespace.
1052
			'~^\s*(?'.'>([\p{Pd}\p{Pe}\p{Pf}\p{Pc}]|[^%\P{Po}])\s*)*~u',
1053
		),
1054
		array(
1055
			'',
1056
			'$1$2',
1057
			'',
1058
			'',
1059
		),
1060
		$format
1061
	);
1062
1063
	// Gotta have something...
1064
	if (empty($format))
1065
		$format = $default_format;
1066
1067
	// Remember what we've done.
1068
	$formats[$orig_format][$type] = trim($format);
1069
1070
	return $formats[$orig_format][$type];
1071
}
1072
1073
/**
1074
 * Replaces special entities in strings with the real characters.
1075
 *
1076
 * Functionally equivalent to htmlspecialchars_decode(), except that this also
1077
 * replaces '&nbsp;' with a simple space character.
1078
 *
1079
 * @param string $string A string
1080
 * @return string The string without entities
1081
 */
1082
function un_htmlspecialchars($string)
1083
{
1084
	global $context;
1085
	static $translation = array();
1086
1087
	// Determine the character set... Default to UTF-8
1088
	if (empty($context['character_set']))
1089
		$charset = 'UTF-8';
1090
	// Use ISO-8859-1 in place of non-supported ISO-8859 charsets...
1091
	elseif (strpos($context['character_set'], 'ISO-8859-') !== false && !in_array($context['character_set'], array('ISO-8859-5', 'ISO-8859-15')))
1092
		$charset = 'ISO-8859-1';
1093
	else
1094
		$charset = $context['character_set'];
1095
1096
	if (empty($translation))
1097
		$translation = array_flip(get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES, $charset)) + array('&#039;' => '\'', '&#39;' => '\'', '&nbsp;' => ' ');
1098
1099
	return strtr($string, $translation);
1100
}
1101
1102
/**
1103
 * Replaces invalid characters with a substitute.
1104
 *
1105
 * !!! Warning !!! Setting $substitute to '' in order to delete invalid
1106
 * characters from the string can create unexpected security problems. See
1107
 * https://www.unicode.org/reports/tr36/#Deletion_of_Noncharacters for an
1108
 * explanation.
1109
 *
1110
 * @param string $string The string to sanitize.
1111
 * @param int $level Controls filtering of invisible formatting characters.
1112
 *      0: Allow valid formatting characters. Use for sanitizing text in posts.
1113
 *      1: Allow necessary formatting characters. Use for sanitizing usernames.
1114
 *      2: Disallow all formatting characters. Use for internal comparisions
1115
 *         only, such as in the word censor, search contexts, etc.
1116
 *      Default: 0.
1117
 * @param string|null $substitute Replacement string for the invalid characters.
1118
 *      If not set, the Unicode replacement character (U+FFFD) will be used
1119
 *      (or a fallback like "?" if necessary).
1120
 * @return string The sanitized string.
1121
 */
1122
function sanitize_chars($string, $level = 0, $substitute = null)
1123
{
1124
	global $context, $sourcedir;
1125
1126
	$string = (string) $string;
1127
	$level = min(max((int) $level, 0), 2);
1128
1129
	// What substitute character should we use?
1130
	if (isset($substitute))
1131
	{
1132
		$substitute = strval($substitute);
1133
	}
1134
	elseif (!empty($context['utf8']))
1135
	{
1136
		// Raw UTF-8 bytes for U+FFFD.
1137
		$substitute = "\xEF\xBF\xBD";
1138
	}
1139
	elseif (!empty($context['character_set']) && is_callable('mb_decode_numericentity'))
1140
	{
1141
		// Get whatever the default replacement character is for this encoding.
1142
		$substitute = mb_decode_numericentity('&#xFFFD;', array(0xFFFD,0xFFFD,0,0xFFFF), $context['character_set']);
1143
	}
1144
	else
1145
		$substitute = '?';
1146
1147
	// Fix any invalid byte sequences.
1148
	if (!empty($context['character_set']))
1149
	{
1150
		// For UTF-8, this preg_match test is much faster than mb_check_encoding.
1151
		$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']));
1152
1153
		if ($malformed)
1154
		{
1155
			// mb_convert_encoding will replace invalid byte sequences with our substitute.
1156
			if (is_callable('mb_convert_encoding'))
1157
			{
1158
				if (!is_callable('mb_ord'))
1159
					require_once($sourcedir . '/Subs-Compat.php');
1160
1161
				$substitute_ord = $substitute === '' ? 'none' : mb_ord($substitute, $context['character_set']);
1162
1163
				$mb_substitute_character = mb_substitute_character();
1164
				mb_substitute_character($substitute_ord);
1165
1166
				$string = mb_convert_encoding($string, $context['character_set'], $context['character_set']);
1167
1168
				mb_substitute_character($mb_substitute_character);
0 ignored issues
show
Bug introduced by
It seems like $mb_substitute_character can also be of type true; however, parameter $substitute_character of mb_substitute_character() does only seem to accept integer|null|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

1168
				mb_substitute_character(/** @scrutinizer ignore-type */ $mb_substitute_character);
Loading history...
1169
			}
1170
			else
1171
				return false;
1172
		}
1173
	}
1174
1175
	// Fix any weird vertical space characters.
1176
	$string = normalize_spaces($string, true);
0 ignored issues
show
Bug introduced by
It seems like $string can also be of type array; 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

1176
	$string = normalize_spaces(/** @scrutinizer ignore-type */ $string, true);
Loading history...
1177
1178
	// Deal with unwanted control characters, invisible formatting characters, and other creepy-crawlies.
1179
	if (!empty($context['utf8']))
1180
	{
1181
		require_once($sourcedir . '/Subs-Charset.php');
1182
		$string = utf8_sanitize_invisibles($string, $level, $substitute);
1183
	}
1184
	else
1185
		$string = preg_replace('/[^\P{Cc}\t\r\n]/', $substitute, $string);
1186
1187
	return $string;
1188
}
1189
1190
/**
1191
 * Normalizes space characters and line breaks.
1192
 *
1193
 * @param string $string The string to sanitize.
1194
 * @param bool $vspace If true, replaces all line breaks and vertical space
1195
 *      characters with "\n". Default: true.
1196
 * @param bool $hspace If true, replaces horizontal space characters with a
1197
 *      plain " " character. (Note: tabs are not replaced unless the
1198
 *      'replace_tabs' option is supplied.) Default: false.
1199
 * @param array $options An array of boolean options. Possible values are:
1200
 *      - no_breaks: Vertical spaces are replaced by " " instead of "\n".
1201
 *      - replace_tabs: If true, tabs are are replaced by " " chars.
1202
 *      - collapse_hspace: If true, removes extra horizontal spaces.
1203
 * @return string The sanitized string.
1204
 */
1205
function normalize_spaces($string, $vspace = true, $hspace = false, $options = array())
1206
{
1207
	global $context;
1208
1209
	$string = (string) $string;
1210
	$vspace = !empty($vspace);
1211
	$hspace = !empty($hspace);
1212
1213
	if (!$vspace && !$hspace)
1214
		return $string;
1215
1216
	$options['no_breaks'] = !empty($options['no_breaks']);
1217
	$options['collapse_hspace'] = !empty($options['collapse_hspace']);
1218
	$options['replace_tabs'] = !empty($options['replace_tabs']);
1219
1220
	$patterns = array();
1221
	$replacements = array();
1222
1223
	if ($vspace)
1224
	{
1225
		// \R is like \v, except it handles "\r\n" as a single unit.
1226
		$patterns[] = '/\R/' . ($context['utf8'] ? 'u' : '');
1227
		$replacements[] = $options['no_breaks'] ? ' ' : "\n";
1228
	}
1229
1230
	if ($hspace)
1231
	{
1232
		// Interesting fact: Unicode properties like \p{Zs} work even when not in UTF-8 mode.
1233
		$patterns[] = '/' . ($options['replace_tabs'] ? '\h' : '\p{Zs}') . ($options['collapse_hspace'] ? '+' : '') . '/' . ($context['utf8'] ? 'u' : '');
1234
		$replacements[] = ' ';
1235
	}
1236
1237
	return preg_replace($patterns, $replacements, $string);
1238
}
1239
1240
/**
1241
 * Shorten a subject + internationalization concerns.
1242
 *
1243
 * - shortens a subject so that it is either shorter than length, or that length plus an ellipsis.
1244
 * - respects internationalization characters and entities as one character.
1245
 * - avoids trailing entities.
1246
 * - returns the shortened string.
1247
 *
1248
 * @param string $subject The subject
1249
 * @param int $len How many characters to limit it to
1250
 * @return string The shortened subject - either the entire subject (if it's <= $len) or the subject shortened to $len characters with "..." appended
1251
 */
1252
function shorten_subject($subject, $len)
1253
{
1254
	global $smcFunc;
1255
1256
	// It was already short enough!
1257
	if ($smcFunc['strlen']($subject) <= $len)
1258
		return $subject;
1259
1260
	// Shorten it by the length it was too long, and strip off junk from the end.
1261
	return $smcFunc['substr']($subject, 0, $len) . '...';
1262
}
1263
1264
/**
1265
 * Gets the current time with offset.
1266
 *
1267
 * - always applies the offset in the time_offset setting.
1268
 *
1269
 * @param bool $use_user_offset Whether to apply the user's offset as well
1270
 * @param int $timestamp A timestamp (null to use current time)
1271
 * @return int Seconds since the unix epoch, with forum time offset and (optionally) user time offset applied
1272
 */
1273
function forum_time($use_user_offset = true, $timestamp = null)
1274
{
1275
	global $user_info, $modSettings;
1276
1277
	// Ensure required values are set
1278
	$modSettings['time_offset'] = !empty($modSettings['time_offset']) ? $modSettings['time_offset'] : 0;
1279
	$user_info['time_offset'] = !empty($user_info['time_offset']) ? $user_info['time_offset'] : 0;
1280
1281
	if ($timestamp === null)
1282
		$timestamp = time();
1283
	elseif ($timestamp == 0)
1284
		return 0;
1285
1286
	return $timestamp + ($modSettings['time_offset'] + ($use_user_offset ? $user_info['time_offset'] : 0)) * 3600;
1287
}
1288
1289
/**
1290
 * Calculates all the possible permutations (orders) of array.
1291
 * should not be called on huge arrays (bigger than like 10 elements.)
1292
 * returns an array containing each permutation.
1293
 *
1294
 * @deprecated since 2.1
1295
 * @param array $array An array
1296
 * @return array An array containing each permutation
1297
 */
1298
function permute($array)
1299
{
1300
	$orders = array($array);
1301
1302
	$n = count($array);
1303
	$p = range(0, $n);
1304
	for ($i = 1; $i < $n; null)
1305
	{
1306
		$p[$i]--;
1307
		$j = $i % 2 != 0 ? $p[$i] : 0;
1308
1309
		$temp = $array[$i];
1310
		$array[$i] = $array[$j];
1311
		$array[$j] = $temp;
1312
1313
		for ($i = 1; $p[$i] == 0; $i++)
1314
			$p[$i] = 1;
1315
1316
		$orders[] = $array;
1317
	}
1318
1319
	return $orders;
1320
}
1321
1322
/**
1323
 * Return an array with allowed bbc tags for signatures, that can be passed to parse_bbc().
1324
 *
1325
 * @return array An array containing allowed tags for signatures, or an empty array if all tags are allowed.
1326
 */
1327
function get_signature_allowed_bbc_tags()
1328
{
1329
	global $modSettings;
1330
1331
	list ($sig_limits, $sig_bbc) = explode(':', $modSettings['signature_settings']);
1332
	if (empty($sig_bbc))
1333
		return array();
1334
	$disabledTags = explode(',', $sig_bbc);
1335
1336
	// Get all available bbc tags
1337
	$temp = parse_bbc(false);
1338
	$allowedTags = array();
1339
	foreach ($temp as $tag)
0 ignored issues
show
Bug introduced by
The expression $temp of type string is not traversable.
Loading history...
1340
		if (!in_array($tag['tag'], $disabledTags))
1341
			$allowedTags[] = $tag['tag'];
1342
1343
	$allowedTags = array_unique($allowedTags);
1344
	if (empty($allowedTags))
1345
		// An empty array means that all bbc tags are allowed. So if all tags are disabled we need to add a dummy tag.
1346
		$allowedTags[] = 'nonexisting';
1347
1348
	return $allowedTags;
1349
}
1350
1351
/**
1352
 * Parse bulletin board code in a string, as well as smileys optionally.
1353
 *
1354
 * - only parses bbc tags which are not disabled in disabledBBC.
1355
 * - handles basic HTML, if enablePostHTML is on.
1356
 * - caches the from/to replace regular expressions so as not to reload them every time a string is parsed.
1357
 * - only parses smileys if smileys is true.
1358
 * - does nothing if the enableBBC setting is off.
1359
 * - uses the cache_id as a unique identifier to facilitate any caching it may do.
1360
 * - returns the modified message.
1361
 *
1362
 * @param string|bool $message The message.
1363
 *		When a empty string, nothing is done.
1364
 *		When false we provide a list of BBC codes available.
1365
 *		When a string, the message is parsed and bbc handled.
1366
 * @param bool $smileys Whether to parse smileys as well
1367
 * @param string $cache_id The cache ID
1368
 * @param array $parse_tags If set, only parses these tags rather than all of them
1369
 * @return string The parsed message
1370
 */
1371
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array())
1372
{
1373
	global $smcFunc, $txt, $scripturl, $context, $modSettings, $user_info, $sourcedir, $cache_enable;
1374
	static $bbc_lang_locales = array(), $itemcodes = array(), $no_autolink_tags = array();
1375
	static $disabled, $alltags_regex = '', $param_regexes = array(), $url_regex = '';
1376
1377
	// Don't waste cycles
1378
	if ($message === '')
1379
		return '';
1380
1381
	// Just in case it wasn't determined yet whether UTF-8 is enabled.
1382
	if (!isset($context['utf8']))
1383
		$context['utf8'] = (empty($modSettings['global_character_set']) ? $txt['lang_character_set'] : $modSettings['global_character_set']) === 'UTF-8';
1384
1385
	// Clean up any cut/paste issues we may have
1386
	$message = sanitizeMSCutPaste($message);
1387
1388
	// If the load average is too high, don't parse the BBC.
1389
	if (!empty($context['load_average']) && !empty($modSettings['bbc']) && $context['load_average'] >= $modSettings['bbc'])
1390
	{
1391
		$context['disabled_parse_bbc'] = true;
1392
		return $message;
1393
	}
1394
1395
	if ($smileys !== null && ($smileys == '1' || $smileys == '0'))
1396
		$smileys = (bool) $smileys;
1397
1398
	if (empty($modSettings['enableBBC']) && $message !== false)
1399
	{
1400
		if ($smileys === true)
1401
			parsesmileys($message);
1402
1403
		return $message;
1404
	}
1405
1406
	// If we already have a version of the BBCodes for the current language, use that. Otherwise, make one.
1407
	if (!empty($bbc_lang_locales[$txt['lang_locale']]))
1408
		$bbc_codes = $bbc_lang_locales[$txt['lang_locale']];
1409
	else
1410
		$bbc_codes = array();
1411
1412
	// If we are not doing every tag then we don't cache this run.
1413
	if (!empty($parse_tags))
1414
		$bbc_codes = array();
1415
1416
	// Ensure $modSettings['tld_regex'] contains a valid regex for the autolinker
1417
	if (!empty($modSettings['autoLinkUrls']))
1418
		set_tld_regex();
1419
1420
	// Allow mods access before entering the main parse_bbc loop
1421
	call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
1422
1423
	// Sift out the bbc for a performance improvement.
1424
	if (empty($bbc_codes) || $message === false || !empty($parse_tags))
1425
	{
1426
		if (!empty($modSettings['disabledBBC']))
1427
		{
1428
			$disabled = array();
1429
1430
			$temp = explode(',', strtolower($modSettings['disabledBBC']));
1431
1432
			foreach ($temp as $tag)
1433
				$disabled[trim($tag)] = true;
1434
1435
			if (in_array('color', $disabled))
1436
				$disabled = array_merge($disabled, array(
1437
					'black' => true,
1438
					'white' => true,
1439
					'red' => true,
1440
					'green' => true,
1441
					'blue' => true,
1442
					)
1443
				);
1444
		}
1445
1446
		if (!empty($parse_tags))
1447
		{
1448
			if (!in_array('email', $parse_tags))
1449
				$disabled['email'] = true;
1450
			if (!in_array('url', $parse_tags))
1451
				$disabled['url'] = true;
1452
			if (!in_array('iurl', $parse_tags))
1453
				$disabled['iurl'] = true;
1454
		}
1455
1456
		// The YouTube bbc needs this for its origin parameter
1457
		$scripturl_parts = parse_url($scripturl);
1458
		$hosturl = $scripturl_parts['scheme'] . '://' . $scripturl_parts['host'];
1459
1460
		/* The following bbc are formatted as an array, with keys as follows:
1461
1462
			tag: the tag's name - should be lowercase!
1463
1464
			type: one of...
1465
				- (missing): [tag]parsed content[/tag]
1466
				- unparsed_equals: [tag=xyz]parsed content[/tag]
1467
				- parsed_equals: [tag=parsed data]parsed content[/tag]
1468
				- unparsed_content: [tag]unparsed content[/tag]
1469
				- closed: [tag], [tag/], [tag /]
1470
				- unparsed_commas: [tag=1,2,3]parsed content[/tag]
1471
				- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag]
1472
				- unparsed_equals_content: [tag=...]unparsed content[/tag]
1473
1474
			parameters: an optional array of parameters, for the form
1475
			  [tag abc=123]content[/tag].  The array is an associative array
1476
			  where the keys are the parameter names, and the values are an
1477
			  array which may contain the following:
1478
				- match: a regular expression to validate and match the value.
1479
				- quoted: true if the value should be quoted.
1480
				- validate: callback to evaluate on the data, which is $data.
1481
				- value: a string in which to replace $1 with the data.
1482
					Either value or validate may be used, not both.
1483
				- optional: true if the parameter is optional.
1484
				- default: a default value for missing optional parameters.
1485
1486
			test: a regular expression to test immediately after the tag's
1487
			  '=', ' ' or ']'.  Typically, should have a \] at the end.
1488
			  Optional.
1489
1490
			content: only available for unparsed_content, closed,
1491
			  unparsed_commas_content, and unparsed_equals_content.
1492
			  $1 is replaced with the content of the tag.  Parameters
1493
			  are replaced in the form {param}.  For unparsed_commas_content,
1494
			  $2, $3, ..., $n are replaced.
1495
1496
			before: only when content is not used, to go before any
1497
			  content.  For unparsed_equals, $1 is replaced with the value.
1498
			  For unparsed_commas, $1, $2, ..., $n are replaced.
1499
1500
			after: similar to before in every way, except that it is used
1501
			  when the tag is closed.
1502
1503
			disabled_content: used in place of content when the tag is
1504
			  disabled.  For closed, default is '', otherwise it is '$1' if
1505
			  block_level is false, '<div>$1</div>' elsewise.
1506
1507
			disabled_before: used in place of before when disabled.  Defaults
1508
			  to '<div>' if block_level, '' if not.
1509
1510
			disabled_after: used in place of after when disabled.  Defaults
1511
			  to '</div>' if block_level, '' if not.
1512
1513
			block_level: set to true the tag is a "block level" tag, similar
1514
			  to HTML.  Block level tags cannot be nested inside tags that are
1515
			  not block level, and will not be implicitly closed as easily.
1516
			  One break following a block level tag may also be removed.
1517
1518
			trim: if set, and 'inside' whitespace after the begin tag will be
1519
			  removed.  If set to 'outside', whitespace after the end tag will
1520
			  meet the same fate.
1521
1522
			validate: except when type is missing or 'closed', a callback to
1523
			  validate the data as $data.  Depending on the tag's type, $data
1524
			  may be a string or an array of strings (corresponding to the
1525
			  replacement.)
1526
1527
			quoted: when type is 'unparsed_equals' or 'parsed_equals' only,
1528
			  may be not set, 'optional', or 'required' corresponding to if
1529
			  the content may be quoted.  This allows the parser to read
1530
			  [tag="abc]def[esdf]"] properly.
1531
1532
			require_parents: an array of tag names, or not set.  If set, the
1533
			  enclosing tag *must* be one of the listed tags, or parsing won't
1534
			  occur.
1535
1536
			require_children: similar to require_parents, if set children
1537
			  won't be parsed if they are not in the list.
1538
1539
			disallow_children: similar to, but very different from,
1540
			  require_children, if it is set the listed tags will not be
1541
			  parsed inside the tag.
1542
1543
			parsed_tags_allowed: an array restricting what BBC can be in the
1544
			  parsed_equals parameter, if desired.
1545
		*/
1546
1547
		$codes = array(
1548
			array(
1549
				'tag' => 'abbr',
1550
				'type' => 'unparsed_equals',
1551
				'before' => '<abbr title="$1">',
1552
				'after' => '</abbr>',
1553
				'quoted' => 'optional',
1554
				'disabled_after' => ' ($1)',
1555
			),
1556
			// Legacy (and just an alias for [abbr] even when enabled)
1557
			array(
1558
				'tag' => 'acronym',
1559
				'type' => 'unparsed_equals',
1560
				'before' => '<abbr title="$1">',
1561
				'after' => '</abbr>',
1562
				'quoted' => 'optional',
1563
				'disabled_after' => ' ($1)',
1564
			),
1565
			array(
1566
				'tag' => 'anchor',
1567
				'type' => 'unparsed_equals',
1568
				'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]',
1569
				'before' => '<span id="post_$1">',
1570
				'after' => '</span>',
1571
			),
1572
			array(
1573
				'tag' => 'attach',
1574
				'type' => 'unparsed_content',
1575
				'parameters' => array(
1576
					'id' => array('match' => '(\d+)'),
1577
					'alt' => array('optional' => true),
1578
					'width' => array('optional' => true, 'match' => '(\d+)'),
1579
					'height' => array('optional' => true, 'match' => '(\d+)'),
1580
					'display' => array('optional' => true, 'match' => '(link|embed)'),
1581
				),
1582
				'content' => '$1',
1583
				'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...
1584
				{
1585
					$returnContext = '';
1586
1587
					// BBC or the entire attachments feature is disabled
1588
					if (empty($modSettings['attachmentEnable']) || !empty($disabled['attach']))
1589
						return $data;
1590
1591
					// Save the attach ID.
1592
					$attachID = $params['{id}'];
1593
1594
					// Kinda need this.
1595
					require_once($sourcedir . '/Subs-Attachments.php');
1596
1597
					$currentAttachment = parseAttachBBC($attachID);
1598
1599
					// parseAttachBBC will return a string ($txt key) rather than dying with a fatal_error. Up to you to decide what to do.
1600
					if (is_string($currentAttachment))
1601
						return $data = !empty($txt[$currentAttachment]) ? $txt[$currentAttachment] : $currentAttachment;
1602
1603
					// We need a display mode.
1604
					if (empty($params['{display}']))
1605
					{
1606
						// Images, video, and audio are embedded by default.
1607
						if (!empty($currentAttachment['is_image']) || strpos($currentAttachment['mime_type'], 'video/') === 0 || strpos($currentAttachment['mime_type'], 'audio/') === 0)
1608
							$params['{display}'] = 'embed';
1609
						// Anything else shows a link by default.
1610
						else
1611
							$params['{display}'] = 'link';
1612
					}
1613
1614
					// Embedded file.
1615
					if ($params['{display}'] == 'embed')
1616
					{
1617
						$alt = ' alt="' . (!empty($params['{alt}']) ? $params['{alt}'] : $currentAttachment['name']) . '"';
1618
						$title = !empty($data) ? ' title="' . $smcFunc['htmlspecialchars']($data) . '"' : '';
1619
1620
						// Image.
1621
						if (!empty($currentAttachment['is_image']))
1622
						{
1623
							if (empty($params['{width}']) && empty($params['{height}']))
1624
								$returnContext .= '<img src="' . $currentAttachment['href'] . '"' . $alt . $title . ' class="bbc_img">';
1625
							else
1626
							{
1627
								$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"': '';
1628
								$height = !empty($params['{height}']) ? 'height="' . $params['{height}'] . '"' : '';
1629
								$returnContext .= '<img src="' . $currentAttachment['href'] . ';image"' . $alt . $title . $width . $height . ' class="bbc_img resized"/>';
1630
							}
1631
						}
1632
						// Video.
1633
						elseif (strpos($currentAttachment['mime_type'], 'video/') === 0)
1634
						{
1635
							$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1636
							$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1637
1638
							$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>' : '');
1639
						}
1640
						// Audio.
1641
						elseif (strpos($currentAttachment['mime_type'], 'audio/') === 0)
1642
						{
1643
							$width = 'max-width:100%; width: ' . (!empty($params['{width}']) ? $params['{width}'] : '400') . 'px;';
1644
							$height = !empty($params['{height}']) ? 'height: ' . $params['{height}'] . 'px;' : '';
1645
1646
							$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>';
1647
						}
1648
						// Anything else.
1649
						else
1650
						{
1651
							$width = !empty($params['{width}']) ? ' width="' . $params['{width}'] . '"' : '';
1652
							$height = !empty($params['{height}']) ? ' height="' . $params['{height}'] . '"' : '';
1653
1654
							$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>';
1655
						}
1656
					}
1657
1658
					// No image. Show a link.
1659
					else
1660
						$returnContext .= '<a href="' . $currentAttachment['href'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](!empty($data) ? $data : $currentAttachment['name']) . '</a>';
1661
1662
					// Use this hook to adjust the HTML output of the attach BBCode.
1663
					// If you want to work with the attachment data itself, use one of these:
1664
					// - integrate_pre_parseAttachBBC
1665
					// - integrate_post_parseAttachBBC
1666
					call_integration_hook('integrate_attach_bbc_validate', array(&$returnContext, $currentAttachment, $tag, $data, $disabled, $params));
1667
1668
					// Gotta append what we just did.
1669
					$data = $returnContext;
1670
				},
1671
			),
1672
			array(
1673
				'tag' => 'b',
1674
				'before' => '<b>',
1675
				'after' => '</b>',
1676
			),
1677
			// Legacy (equivalent to [ltr] or [rtl])
1678
			array(
1679
				'tag' => 'bdo',
1680
				'type' => 'unparsed_equals',
1681
				'before' => '<bdo dir="$1">',
1682
				'after' => '</bdo>',
1683
				'test' => '(rtl|ltr)\]',
1684
				'block_level' => true,
1685
			),
1686
			// Legacy (alias of [color=black])
1687
			array(
1688
				'tag' => 'black',
1689
				'before' => '<span style="color: black;" class="bbc_color">',
1690
				'after' => '</span>',
1691
			),
1692
			// Legacy (alias of [color=blue])
1693
			array(
1694
				'tag' => 'blue',
1695
				'before' => '<span style="color: blue;" class="bbc_color">',
1696
				'after' => '</span>',
1697
			),
1698
			array(
1699
				'tag' => 'br',
1700
				'type' => 'closed',
1701
				'content' => '<br>',
1702
			),
1703
			array(
1704
				'tag' => 'center',
1705
				'before' => '<div class="centertext">',
1706
				'after' => '</div>',
1707
				'block_level' => true,
1708
			),
1709
			array(
1710
				'tag' => 'code',
1711
				'type' => 'unparsed_content',
1712
				'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>',
1713
				// @todo Maybe this can be simplified?
1714
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1715
				{
1716
					if (!isset($disabled['code']))
1717
					{
1718
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data, -1, PREG_SPLIT_DELIM_CAPTURE);
1719
1720
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1721
						{
1722
							// Do PHP code coloring?
1723
							if ($php_parts[$php_i] != '&lt;?php')
1724
								continue;
1725
1726
							$php_string = '';
1727
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1728
							{
1729
								$php_string .= $php_parts[$php_i];
1730
								$php_parts[$php_i++] = '';
1731
							}
1732
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1733
						}
1734
1735
						// Fix the PHP code stuff...
1736
						$data = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1737
						$data = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data);
1738
1739
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1740
						if (!empty($context['browser']['is_opera']))
1741
							$data .= '&nbsp;';
1742
					}
1743
				},
1744
				'block_level' => true,
1745
			),
1746
			array(
1747
				'tag' => 'code',
1748
				'type' => 'unparsed_equals_content',
1749
				'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>',
1750
				// @todo Maybe this can be simplified?
1751
				'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) use ($context)
1752
				{
1753
					if (!isset($disabled['code']))
1754
					{
1755
						$php_parts = preg_split('~(&lt;\?php|\?&gt;)~', $data[0], -1, PREG_SPLIT_DELIM_CAPTURE);
1756
1757
						for ($php_i = 0, $php_n = count($php_parts); $php_i < $php_n; $php_i++)
1758
						{
1759
							// Do PHP code coloring?
1760
							if ($php_parts[$php_i] != '&lt;?php')
1761
								continue;
1762
1763
							$php_string = '';
1764
							while ($php_i + 1 < count($php_parts) && $php_parts[$php_i] != '?&gt;')
1765
							{
1766
								$php_string .= $php_parts[$php_i];
1767
								$php_parts[$php_i++] = '';
1768
							}
1769
							$php_parts[$php_i] = highlight_php_code($php_string . $php_parts[$php_i]);
1770
						}
1771
1772
						// Fix the PHP code stuff...
1773
						$data[0] = str_replace("<pre style=\"display: inline;\">\t</pre>", "\t", implode('', $php_parts));
1774
						$data[0] = str_replace("\t", "<span style=\"white-space: pre;\">\t</span>", $data[0]);
1775
1776
						// Recent Opera bug requiring temporary fix. &nsbp; is needed before </code> to avoid broken selection.
1777
						if (!empty($context['browser']['is_opera']))
1778
							$data[0] .= '&nbsp;';
1779
					}
1780
				},
1781
				'block_level' => true,
1782
			),
1783
			array(
1784
				'tag' => 'color',
1785
				'type' => 'unparsed_equals',
1786
				'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]?)\))\]',
1787
				'before' => '<span style="color: $1;" class="bbc_color">',
1788
				'after' => '</span>',
1789
			),
1790
			array(
1791
				'tag' => 'email',
1792
				'type' => 'unparsed_content',
1793
				'content' => '<a href="mailto:$1" class="bbc_email">$1</a>',
1794
				// @todo Should this respect guest_hideContacts?
1795
				'validate' => function(&$tag, &$data, $disabled)
1796
				{
1797
					$data = strtr($data, array('<br>' => ''));
1798
				},
1799
			),
1800
			array(
1801
				'tag' => 'email',
1802
				'type' => 'unparsed_equals',
1803
				'before' => '<a href="mailto:$1" class="bbc_email">',
1804
				'after' => '</a>',
1805
				// @todo Should this respect guest_hideContacts?
1806
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1807
				'disabled_after' => ' ($1)',
1808
			),
1809
			// Legacy (and just a link even when not disabled)
1810
			array(
1811
				'tag' => 'flash',
1812
				'type' => 'unparsed_commas_content',
1813
				'test' => '\d+,\d+\]',
1814
				'content' => '<a href="$1" target="_blank" rel="noopener">$1</a>',
1815
				'validate' => function (&$tag, &$data, $disabled)
1816
				{
1817
					$scheme = parse_url($data[0], PHP_URL_SCHEME);
1818
					if (empty($scheme))
1819
						$data[0] = '//' . ltrim($data[0], ':/');
1820
				},
1821
			),
1822
			array(
1823
				'tag' => 'float',
1824
				'type' => 'unparsed_equals',
1825
				'test' => '(left|right)(\s+max=\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)?\]',
1826
				'before' => '<div $1>',
1827
				'after' => '</div>',
1828
				'validate' => function(&$tag, &$data, $disabled)
1829
				{
1830
					$class = 'class="bbc_float float' . (strpos($data, 'left') === 0 ? 'left' : 'right') . '"';
1831
1832
					if (preg_match('~\bmax=(\d+(?:%|px|em|rem|ex|pt|pc|ch|vw|vh|vmin|vmax|cm|mm|in)?)~', $data, $matches))
1833
						$css = ' style="max-width:' . $matches[1] . (is_numeric($matches[1]) ? 'px' : '') . '"';
1834
					else
1835
						$css = '';
1836
1837
					$data = $class . $css;
1838
				},
1839
				'trim' => 'outside',
1840
				'block_level' => true,
1841
			),
1842
			// Legacy (alias of [url] with an FTP URL)
1843
			array(
1844
				'tag' => 'ftp',
1845
				'type' => 'unparsed_content',
1846
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
1847
				'validate' => function(&$tag, &$data, $disabled)
1848
				{
1849
					$data = strtr($data, array('<br>' => ''));
1850
					$scheme = parse_url($data, PHP_URL_SCHEME);
1851
					if (empty($scheme))
1852
						$data = 'ftp://' . ltrim($data, ':/');
1853
				},
1854
			),
1855
			// Legacy (alias of [url] with an FTP URL)
1856
			array(
1857
				'tag' => 'ftp',
1858
				'type' => 'unparsed_equals',
1859
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
1860
				'after' => '</a>',
1861
				'validate' => function(&$tag, &$data, $disabled)
1862
				{
1863
					$scheme = parse_url($data, PHP_URL_SCHEME);
1864
					if (empty($scheme))
1865
						$data = 'ftp://' . ltrim($data, ':/');
1866
				},
1867
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1868
				'disabled_after' => ' ($1)',
1869
			),
1870
			array(
1871
				'tag' => 'font',
1872
				'type' => 'unparsed_equals',
1873
				'test' => '[A-Za-z0-9_,\-\s]+?\]',
1874
				'before' => '<span style="font-family: $1;" class="bbc_font">',
1875
				'after' => '</span>',
1876
			),
1877
			// Legacy (one of those things that should not be done)
1878
			array(
1879
				'tag' => 'glow',
1880
				'type' => 'unparsed_commas',
1881
				'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]',
1882
				'before' => '<span style="text-shadow: $1 1px 1px 1px">',
1883
				'after' => '</span>',
1884
			),
1885
			// Legacy (alias of [color=green])
1886
			array(
1887
				'tag' => 'green',
1888
				'before' => '<span style="color: green;" class="bbc_color">',
1889
				'after' => '</span>',
1890
			),
1891
			array(
1892
				'tag' => 'html',
1893
				'type' => 'unparsed_content',
1894
				'content' => '<div>$1</div>',
1895
				'block_level' => true,
1896
				'disabled_content' => '$1',
1897
			),
1898
			array(
1899
				'tag' => 'hr',
1900
				'type' => 'closed',
1901
				'content' => '<hr>',
1902
				'block_level' => true,
1903
			),
1904
			array(
1905
				'tag' => 'i',
1906
				'before' => '<i>',
1907
				'after' => '</i>',
1908
			),
1909
			array(
1910
				'tag' => 'img',
1911
				'type' => 'unparsed_content',
1912
				'parameters' => array(
1913
					'alt' => array('optional' => true),
1914
					'title' => array('optional' => true),
1915
					'width' => array('optional' => true, 'value' => ' width="$1"', 'match' => '(\d+)'),
1916
					'height' => array('optional' => true, 'value' => ' height="$1"', 'match' => '(\d+)'),
1917
				),
1918
				'content' => '$1',
1919
				'validate' => function(&$tag, &$data, $disabled, $params)
1920
				{
1921
					$url = strtr($data, array('<br>' => ''));
1922
1923
					if (parse_url($url, PHP_URL_SCHEME) === null)
1924
						$url = '//' . ltrim($url, ':/');
1925
					else
1926
						$url = get_proxied_url($url);
1927
1928
					$alt = !empty($params['{alt}']) ? ' alt="' . $params['{alt}']. '"' : ' alt=""';
1929
					$title = !empty($params['{title}']) ? ' title="' . $params['{title}']. '"' : '';
1930
1931
					$data = isset($disabled[$tag['tag']]) ? $url : '<img src="' . $url . '"' . $alt . $title . $params['{width}'] . $params['{height}'] . ' class="bbc_img' . (!empty($params['{width}']) || !empty($params['{height}']) ? ' resized' : '') . '" loading="lazy">';
1932
				},
1933
				'disabled_content' => '($1)',
1934
			),
1935
			array(
1936
				'tag' => 'iurl',
1937
				'type' => 'unparsed_content',
1938
				'content' => '<a href="$1" class="bbc_link">$1</a>',
1939
				'validate' => function(&$tag, &$data, $disabled)
1940
				{
1941
					$data = strtr($data, array('<br>' => ''));
1942
					$scheme = parse_url($data, PHP_URL_SCHEME);
1943
					if (empty($scheme))
1944
						$data = '//' . ltrim($data, ':/');
1945
				},
1946
			),
1947
			array(
1948
				'tag' => 'iurl',
1949
				'type' => 'unparsed_equals',
1950
				'quoted' => 'optional',
1951
				'before' => '<a href="$1" class="bbc_link">',
1952
				'after' => '</a>',
1953
				'validate' => function(&$tag, &$data, $disabled)
1954
				{
1955
					if (substr($data, 0, 1) == '#')
1956
						$data = '#post_' . substr($data, 1);
1957
					else
1958
					{
1959
						$scheme = parse_url($data, PHP_URL_SCHEME);
1960
						if (empty($scheme))
1961
							$data = '//' . ltrim($data, ':/');
1962
					}
1963
				},
1964
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
1965
				'disabled_after' => ' ($1)',
1966
			),
1967
			array(
1968
				'tag' => 'justify',
1969
				'before' => '<div class="justifytext">',
1970
				'after' => '</div>',
1971
				'block_level' => true,
1972
			),
1973
			array(
1974
				'tag' => 'left',
1975
				'before' => '<div class="lefttext">',
1976
				'after' => '</div>',
1977
				'block_level' => true,
1978
			),
1979
			array(
1980
				'tag' => 'li',
1981
				'before' => '<li>',
1982
				'after' => '</li>',
1983
				'trim' => 'outside',
1984
				'require_parents' => array('list'),
1985
				'block_level' => true,
1986
				'disabled_before' => '',
1987
				'disabled_after' => '<br>',
1988
			),
1989
			array(
1990
				'tag' => 'list',
1991
				'before' => '<ul class="bbc_list">',
1992
				'after' => '</ul>',
1993
				'trim' => 'inside',
1994
				'require_children' => array('li', 'list'),
1995
				'block_level' => true,
1996
			),
1997
			array(
1998
				'tag' => 'list',
1999
				'parameters' => array(
2000
					'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)'),
2001
				),
2002
				'before' => '<ul class="bbc_list" style="list-style-type: {type};">',
2003
				'after' => '</ul>',
2004
				'trim' => 'inside',
2005
				'require_children' => array('li'),
2006
				'block_level' => true,
2007
			),
2008
			array(
2009
				'tag' => 'ltr',
2010
				'before' => '<bdo dir="ltr">',
2011
				'after' => '</bdo>',
2012
				'block_level' => true,
2013
			),
2014
			array(
2015
				'tag' => 'me',
2016
				'type' => 'unparsed_equals',
2017
				'before' => '<div class="meaction">* $1 ',
2018
				'after' => '</div>',
2019
				'quoted' => 'optional',
2020
				'block_level' => true,
2021
				'disabled_before' => '/me ',
2022
				'disabled_after' => '<br>',
2023
			),
2024
			array(
2025
				'tag' => 'member',
2026
				'type' => 'unparsed_equals',
2027
				'before' => '<a href="' . $scripturl . '?action=profile;u=$1" class="mention" data-mention="$1">@',
2028
				'after' => '</a>',
2029
			),
2030
			// Legacy (horrible memories of the 1990s)
2031
			array(
2032
				'tag' => 'move',
2033
				'before' => '<marquee>',
2034
				'after' => '</marquee>',
2035
				'block_level' => true,
2036
				'disallow_children' => array('move'),
2037
			),
2038
			array(
2039
				'tag' => 'nobbc',
2040
				'type' => 'unparsed_content',
2041
				'content' => '$1',
2042
			),
2043
			array(
2044
				'tag' => 'php',
2045
				'type' => 'unparsed_content',
2046
				'content' => '<span class="phpcode">$1</span>',
2047
				'validate' => isset($disabled['php']) ? null : function(&$tag, &$data, $disabled)
2048
				{
2049
					if (!isset($disabled['php']))
2050
					{
2051
						$add_begin = substr(trim($data), 0, 5) != '&lt;?';
2052
						$data = highlight_php_code($add_begin ? '&lt;?php ' . $data . '?&gt;' : $data);
2053
						if ($add_begin)
2054
							$data = preg_replace(array('~^(.+?)&lt;\?.{0,40}?php(?:&nbsp;|\s)~', '~\?&gt;((?:</(font|span)>)*)$~'), '$1', $data, 2);
2055
					}
2056
				},
2057
				'block_level' => false,
2058
				'disabled_content' => '$1',
2059
			),
2060
			array(
2061
				'tag' => 'pre',
2062
				'before' => '<pre>',
2063
				'after' => '</pre>',
2064
			),
2065
			array(
2066
				'tag' => 'quote',
2067
				'before' => '<blockquote><cite>' . $txt['quote'] . '</cite>',
2068
				'after' => '</blockquote>',
2069
				'trim' => 'both',
2070
				'block_level' => true,
2071
			),
2072
			array(
2073
				'tag' => 'quote',
2074
				'parameters' => array(
2075
					'author' => array('match' => '(.{1,192}?)', 'quoted' => true),
2076
				),
2077
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
2078
				'after' => '</blockquote>',
2079
				'trim' => 'both',
2080
				'block_level' => true,
2081
			),
2082
			array(
2083
				'tag' => 'quote',
2084
				'type' => 'parsed_equals',
2085
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': $1</cite>',
2086
				'after' => '</blockquote>',
2087
				'trim' => 'both',
2088
				'quoted' => 'optional',
2089
				// Don't allow everything to be embedded with the author name.
2090
				'parsed_tags_allowed' => array('url', 'iurl', 'ftp'),
2091
				'block_level' => true,
2092
			),
2093
			array(
2094
				'tag' => 'quote',
2095
				'parameters' => array(
2096
					'author' => array('match' => '([^<>]{1,192}?)'),
2097
					'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d+?|action=profile;u=\d+)'),
2098
					'date' => array('match' => '(\d+)', 'validate' => 'timeformat'),
2099
				),
2100
				'before' => '<blockquote><cite><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . $txt['search_on'] . ' {date}</a></cite>',
2101
				'after' => '</blockquote>',
2102
				'trim' => 'both',
2103
				'block_level' => true,
2104
			),
2105
			array(
2106
				'tag' => 'quote',
2107
				'parameters' => array(
2108
					'author' => array('match' => '(.{1,192}?)'),
2109
				),
2110
				'before' => '<blockquote><cite>' . $txt['quote_from'] . ': {author}</cite>',
2111
				'after' => '</blockquote>',
2112
				'trim' => 'both',
2113
				'block_level' => true,
2114
			),
2115
			// Legacy (alias of [color=red])
2116
			array(
2117
				'tag' => 'red',
2118
				'before' => '<span style="color: red;" class="bbc_color">',
2119
				'after' => '</span>',
2120
			),
2121
			array(
2122
				'tag' => 'right',
2123
				'before' => '<div class="righttext">',
2124
				'after' => '</div>',
2125
				'block_level' => true,
2126
			),
2127
			array(
2128
				'tag' => 'rtl',
2129
				'before' => '<bdo dir="rtl">',
2130
				'after' => '</bdo>',
2131
				'block_level' => true,
2132
			),
2133
			array(
2134
				'tag' => 's',
2135
				'before' => '<s>',
2136
				'after' => '</s>',
2137
			),
2138
			// Legacy (never a good idea)
2139
			array(
2140
				'tag' => 'shadow',
2141
				'type' => 'unparsed_commas',
2142
				'test' => '[#0-9a-zA-Z\-]{3,12},(left|right|top|bottom|[0123]\d{0,2})\]',
2143
				'before' => '<span style="text-shadow: $1 $2">',
2144
				'after' => '</span>',
2145
				'validate' => function(&$tag, &$data, $disabled)
2146
				{
2147
2148
					if ($data[1] == 'top' || (is_numeric($data[1]) && $data[1] < 50))
2149
						$data[1] = '0 -2px 1px';
2150
2151
					elseif ($data[1] == 'right' || (is_numeric($data[1]) && $data[1] < 100))
2152
						$data[1] = '2px 0 1px';
2153
2154
					elseif ($data[1] == 'bottom' || (is_numeric($data[1]) && $data[1] < 190))
2155
						$data[1] = '0 2px 1px';
2156
2157
					elseif ($data[1] == 'left' || (is_numeric($data[1]) && $data[1] < 280))
2158
						$data[1] = '-2px 0 1px';
2159
2160
					else
2161
						$data[1] = '1px 1px 1px';
2162
				},
2163
			),
2164
			array(
2165
				'tag' => 'size',
2166
				'type' => 'unparsed_equals',
2167
				'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]',
2168
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2169
				'after' => '</span>',
2170
			),
2171
			array(
2172
				'tag' => 'size',
2173
				'type' => 'unparsed_equals',
2174
				'test' => '[1-7]\]',
2175
				'before' => '<span style="font-size: $1;" class="bbc_size">',
2176
				'after' => '</span>',
2177
				'validate' => function(&$tag, &$data, $disabled)
2178
				{
2179
					$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95);
2180
					$data = $sizes[$data] . 'em';
2181
				},
2182
			),
2183
			array(
2184
				'tag' => 'sub',
2185
				'before' => '<sub>',
2186
				'after' => '</sub>',
2187
			),
2188
			array(
2189
				'tag' => 'sup',
2190
				'before' => '<sup>',
2191
				'after' => '</sup>',
2192
			),
2193
			array(
2194
				'tag' => 'table',
2195
				'before' => '<table class="bbc_table">',
2196
				'after' => '</table>',
2197
				'trim' => 'inside',
2198
				'require_children' => array('tr'),
2199
				'block_level' => true,
2200
			),
2201
			array(
2202
				'tag' => 'td',
2203
				'before' => '<td>',
2204
				'after' => '</td>',
2205
				'require_parents' => array('tr'),
2206
				'trim' => 'outside',
2207
				'block_level' => true,
2208
				'disabled_before' => '',
2209
				'disabled_after' => '',
2210
			),
2211
			array(
2212
				'tag' => 'time',
2213
				'type' => 'unparsed_content',
2214
				'content' => '$1',
2215
				'validate' => function(&$tag, &$data, $disabled)
2216
				{
2217
					if (is_numeric($data))
2218
						$data = timeformat($data);
2219
2220
					$tag['content'] = '<span class="bbc_time">$1</span>';
2221
				},
2222
			),
2223
			array(
2224
				'tag' => 'tr',
2225
				'before' => '<tr>',
2226
				'after' => '</tr>',
2227
				'require_parents' => array('table'),
2228
				'require_children' => array('td'),
2229
				'trim' => 'both',
2230
				'block_level' => true,
2231
				'disabled_before' => '',
2232
				'disabled_after' => '',
2233
			),
2234
			// Legacy (the <tt> element is dead)
2235
			array(
2236
				'tag' => 'tt',
2237
				'before' => '<span class="monospace">',
2238
				'after' => '</span>',
2239
			),
2240
			array(
2241
				'tag' => 'u',
2242
				'before' => '<u>',
2243
				'after' => '</u>',
2244
			),
2245
			array(
2246
				'tag' => 'url',
2247
				'type' => 'unparsed_content',
2248
				'content' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">$1</a>',
2249
				'validate' => function(&$tag, &$data, $disabled)
2250
				{
2251
					$data = strtr($data, array('<br>' => ''));
2252
					$scheme = parse_url($data, PHP_URL_SCHEME);
2253
					if (empty($scheme))
2254
						$data = '//' . ltrim($data, ':/');
2255
				},
2256
			),
2257
			array(
2258
				'tag' => 'url',
2259
				'type' => 'unparsed_equals',
2260
				'quoted' => 'optional',
2261
				'before' => '<a href="$1" class="bbc_link" target="_blank" rel="noopener">',
2262
				'after' => '</a>',
2263
				'validate' => function(&$tag, &$data, $disabled)
2264
				{
2265
					$scheme = parse_url($data, PHP_URL_SCHEME);
2266
					if (empty($scheme))
2267
						$data = '//' . ltrim($data, ':/');
2268
				},
2269
				'disallow_children' => array('email', 'ftp', 'url', 'iurl'),
2270
				'disabled_after' => ' ($1)',
2271
			),
2272
			// Legacy (alias of [color=white])
2273
			array(
2274
				'tag' => 'white',
2275
				'before' => '<span style="color: white;" class="bbc_color">',
2276
				'after' => '</span>',
2277
			),
2278
			array(
2279
				'tag' => 'youtube',
2280
				'type' => 'unparsed_content',
2281
				'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>',
2282
				'disabled_content' => '<a href="https://www.youtube.com/watch?v=$1" target="_blank" rel="noopener">https://www.youtube.com/watch?v=$1</a>',
2283
				'block_level' => true,
2284
			),
2285
		);
2286
2287
		// Inside these tags autolink is not recommendable.
2288
		$no_autolink_tags = array(
2289
			'url',
2290
			'iurl',
2291
			'email',
2292
			'img',
2293
			'html',
2294
		);
2295
2296
		// Let mods add new BBC without hassle.
2297
		call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags));
2298
2299
		// This is mainly for the bbc manager, so it's easy to add tags above.  Custom BBC should be added above this line.
2300
		if ($message === false)
0 ignored issues
show
introduced by
The condition $message === false is always false.
Loading history...
2301
		{
2302
			usort(
2303
				$codes,
2304
				function($a, $b)
2305
				{
2306
					return strcmp($a['tag'], $b['tag']);
2307
				}
2308
			);
2309
			return $codes;
2310
		}
2311
2312
		// So the parser won't skip them.
2313
		$itemcodes = array(
2314
			'*' => 'disc',
2315
			'@' => 'disc',
2316
			'+' => 'square',
2317
			'x' => 'square',
2318
			'#' => 'square',
2319
			'o' => 'circle',
2320
			'O' => 'circle',
2321
			'0' => 'circle',
2322
		);
2323
		if (!isset($disabled['li']) && !isset($disabled['list']))
2324
		{
2325
			foreach ($itemcodes as $c => $dummy)
2326
				$bbc_codes[$c] = array();
2327
		}
2328
2329
		// Shhhh!
2330
		if (!isset($disabled['color']))
2331
		{
2332
			$codes[] = array(
2333
				'tag' => 'chrissy',
2334
				'before' => '<span style="color: #cc0099;">',
2335
				'after' => ' :-*</span>',
2336
			);
2337
			$codes[] = array(
2338
				'tag' => 'kissy',
2339
				'before' => '<span style="color: #cc0099;">',
2340
				'after' => ' :-*</span>',
2341
			);
2342
		}
2343
		$codes[] = array(
2344
			'tag' => 'cowsay',
2345
			'parameters' => array(
2346
				'e' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => 'oo', 'validate' => function ($eyes) use ($smcFunc)
2347
					{
2348
						return $smcFunc['substr']($eyes . 'oo', 0, 2);
2349
					},
2350
				),
2351
				't' => array('optional' => true, 'quoted' => true, 'match' => '(.*?)', 'default' => '  ', 'validate' => function ($tongue) use ($smcFunc)
2352
					{
2353
						return $smcFunc['substr']($tongue . '  ', 0, 2);
2354
					},
2355
				),
2356
			),
2357
			'before' => '<pre data-e="{e}" data-t="{t}"><div>',
2358
			'after' => '</div></pre>',
2359
			'block_level' => true,
2360
			'validate' => function(&$tag, &$data, $disabled, $params)
2361
			{
2362
				static $moo = true;
2363
2364
				if ($moo)
2365
				{
2366
					addInlineJavaScript("\n\t" . base64_decode(
2367
						'aWYoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImJvdmluZV9vcmFjbGU
2368
						iKT09PW51bGwpe2xldCBzdHlsZU5vZGU9ZG9jdW1lbnQuY3JlYXRlRWx
2369
						lbWVudCgic3R5bGUiKTtzdHlsZU5vZGUuaWQ9ImJvdmluZV9vcmFjbGU
2370
						iO3N0eWxlTm9kZS5pbm5lckhUTUw9J3ByZVtkYXRhLWVdW2RhdGEtdF1
2371
						7d2hpdGUtc3BhY2U6cHJlLXdyYXA7bGluZS1oZWlnaHQ6aW5pdGlhbDt
2372
						9cHJlW2RhdGEtZV1bZGF0YS10XSA+IGRpdntkaXNwbGF5OnRhYmxlO2J
2373
						vcmRlcjoxcHggc29saWQ7Ym9yZGVyLXJhZGl1czowLjVlbTtwYWRkaW5
2374
						nOjFjaDttYXgtd2lkdGg6ODBjaDttaW4td2lkdGg6MTJjaDt9cHJlW2R
2375
						hdGEtZV1bZGF0YS10XTo6YWZ0ZXJ7ZGlzcGxheTppbmxpbmUtYmxvY2s
2376
						7bWFyZ2luLWxlZnQ6OGNoO21pbi13aWR0aDoyMGNoO2RpcmVjdGlvbjp
2377
						sdHI7Y29udGVudDpcJ1xcNUMgXCdcJyBcJ1wnIF5fX15cXEEgXCdcJyB
2378
						cXDVDIFwnXCcgKFwnIGF0dHIoZGF0YS1lKSBcJylcXDVDX19fX19fX1x
2379
						cQSBcJ1wnIFwnXCcgXCdcJyAoX18pXFw1QyBcJ1wnIFwnXCcgXCdcJyB
2380
						cJ1wnIFwnXCcgXCdcJyBcJ1wnIClcXDVDL1xcNUNcXEEgXCdcJyBcJ1w
2381
						nIFwnXCcgXCdcJyBcJyBhdHRyKGRhdGEtdCkgXCcgfHwtLS0tdyB8XFx
2382
						BIFwnXCcgXCdcJyBcJ1wnIFwnXCcgXCdcJyBcJ1wnIFwnXCcgfHwgXCd
2383
						cJyBcJ1wnIFwnXCcgXCdcJyB8fFwnO30nO2RvY3VtZW50LmdldEVsZW1
2384
						lbnRzQnlUYWdOYW1lKCJoZWFkIilbMF0uYXBwZW5kQ2hpbGQoc3R5bGV
2385
						Ob2RlKTt9'
2386
					), true);
2387
2388
					$moo = false;
2389
				}
2390
			}
2391
		);
2392
2393
		foreach ($codes as $code)
2394
		{
2395
			// Make it easier to process parameters later
2396
			if (!empty($code['parameters']))
2397
				ksort($code['parameters'], SORT_STRING);
2398
2399
			// If we are not doing every tag only do ones we are interested in.
2400
			if (empty($parse_tags) || in_array($code['tag'], $parse_tags))
2401
				$bbc_codes[substr($code['tag'], 0, 1)][] = $code;
2402
		}
2403
		$codes = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $codes is dead and can be removed.
Loading history...
2404
	}
2405
2406
	// Shall we take the time to cache this?
2407
	if ($cache_id != '' && !empty($cache_enable) && (($cache_enable >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags))
2408
	{
2409
		// It's likely this will change if the message is modified.
2410
		$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']);
2411
2412
		if (($temp = cache_get_data($cache_key, 240)) != null)
2413
			return $temp;
2414
2415
		$cache_t = microtime(true);
2416
	}
2417
2418
	if ($smileys === 'print')
0 ignored issues
show
introduced by
The condition $smileys === 'print' is always false.
Loading history...
2419
	{
2420
		// [glow], [shadow], and [move] can't really be printed.
2421
		$disabled['glow'] = true;
2422
		$disabled['shadow'] = true;
2423
		$disabled['move'] = true;
2424
2425
		// Colors can't well be displayed... supposed to be black and white.
2426
		$disabled['color'] = true;
2427
		$disabled['black'] = true;
2428
		$disabled['blue'] = true;
2429
		$disabled['white'] = true;
2430
		$disabled['red'] = true;
2431
		$disabled['green'] = true;
2432
		$disabled['me'] = true;
2433
2434
		// Color coding doesn't make sense.
2435
		$disabled['php'] = true;
2436
2437
		// Links are useless on paper... just show the link.
2438
		$disabled['ftp'] = true;
2439
		$disabled['url'] = true;
2440
		$disabled['iurl'] = true;
2441
		$disabled['email'] = true;
2442
		$disabled['flash'] = true;
2443
2444
		// @todo Change maybe?
2445
		if (!isset($_GET['images']))
2446
		{
2447
			$disabled['img'] = true;
2448
			$disabled['attach'] = true;
2449
		}
2450
2451
		// Maybe some custom BBC need to be disabled for printing.
2452
		call_integration_hook('integrate_bbc_print', array(&$disabled));
2453
	}
2454
2455
	$open_tags = array();
2456
	$message = strtr($message, array("\n" => '<br>'));
2457
2458
	if (!empty($parse_tags))
2459
	{
2460
		$real_alltags_regex = $alltags_regex;
2461
		$alltags_regex = '';
2462
	}
2463
	if (empty($alltags_regex))
2464
	{
2465
		$alltags = array();
2466
		foreach ($bbc_codes as $section)
2467
		{
2468
			foreach ($section as $code)
2469
				$alltags[] = $code['tag'];
2470
		}
2471
		$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

2471
		$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

2471
		$alltags_regex = '(?' . '>\b' . /** @scrutinizer ignore-type */ build_regex(array_unique($alltags)) . '\b|' . build_regex(array_keys($itemcodes)) . ')';
Loading history...
2472
	}
2473
2474
	$pos = -1;
2475
	while ($pos !== false)
2476
	{
2477
		$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos;
2478
		preg_match('~\[/?(?=' . $alltags_regex . ')~i', $message, $matches, PREG_OFFSET_CAPTURE, $pos + 1);
2479
		$pos = isset($matches[0][1]) ? $matches[0][1] : false;
2480
2481
		// Failsafe.
2482
		if ($pos === false || $last_pos > $pos)
2483
			$pos = strlen($message) + 1;
2484
2485
		// Can't have a one letter smiley, URL, or email! (Sorry.)
2486
		if ($last_pos < $pos - 1)
2487
		{
2488
			// Make sure the $last_pos is not negative.
2489
			$last_pos = max($last_pos, 0);
2490
2491
			// Pick a block of data to do some raw fixing on.
2492
			$data = substr($message, $last_pos, $pos - $last_pos);
2493
2494
			$placeholders = array();
2495
			$placeholders_counter = 0;
2496
			// Wrap in "private use" Unicode characters to ensure there will be no conflicts.
2497
			$placeholder_template = html_entity_decode('&#xE03C;') . '%1$s' . html_entity_decode('&#xE03E;');
2498
2499
			// Take care of some HTML!
2500
			if (!empty($modSettings['enablePostHTML']) && strpos($data, '&lt;') !== false)
2501
			{
2502
				$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);
2503
2504
				// <br> should be empty.
2505
				$empty_tags = array('br', 'hr');
2506
				foreach ($empty_tags as $tag)
2507
					$data = str_replace(array('&lt;' . $tag . '&gt;', '&lt;' . $tag . '/&gt;', '&lt;' . $tag . ' /&gt;'), '<' . $tag . '>', $data);
2508
2509
				// b, u, i, s, pre... basic tags.
2510
				$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote', 'strong');
2511
				foreach ($closable_tags as $tag)
2512
				{
2513
					$diff = substr_count($data, '&lt;' . $tag . '&gt;') - substr_count($data, '&lt;/' . $tag . '&gt;');
2514
					$data = strtr($data, array('&lt;' . $tag . '&gt;' => '<' . $tag . '>', '&lt;/' . $tag . '&gt;' => '</' . $tag . '>'));
2515
2516
					if ($diff > 0)
2517
						$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1);
2518
				}
2519
2520
				// Do <img ...> - with security... action= -> action-.
2521
				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);
2522
				if (!empty($matches[0]))
2523
				{
2524
					$replaces = array();
2525
					foreach ($matches[2] as $match => $imgtag)
2526
					{
2527
						$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^&quot;|&quot;$~', '', $matches[3][$match]);
2528
2529
						// Remove action= from the URL - no funny business, now.
2530
						// @todo Testing this preg_match seems pointless
2531
						if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) != 0)
2532
							$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag);
2533
2534
						$placeholder = sprintf($placeholder_template, ++$placeholders_counter);
2535
						$placeholders[$placeholder] = '[img' . $alt . ']' . $imgtag . '[/img]';
2536
2537
						$replaces[$matches[0][$match]] = $placeholder;
2538
					}
2539
2540
					$data = strtr($data, $replaces);
2541
				}
2542
			}
2543
2544
			if (!empty($modSettings['autoLinkUrls']))
2545
			{
2546
				if (!function_exists('idn_to_ascii'))
2547
					require_once($sourcedir . '/Subs-Compat.php');
2548
2549
				// Are we inside tags that should be auto linked?
2550
				$no_autolink_area = false;
2551
				if (!empty($open_tags))
2552
				{
2553
					foreach ($open_tags as $open_tag)
2554
						if (in_array($open_tag['tag'], $no_autolink_tags))
2555
							$no_autolink_area = true;
2556
				}
2557
2558
				// Don't go backwards.
2559
				// @todo Don't think is the real solution....
2560
				$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0;
2561
				if ($pos < $lastAutoPos)
2562
					$no_autolink_area = true;
2563
				$lastAutoPos = $pos;
2564
2565
				if (!$no_autolink_area)
2566
				{
2567
					// An &nbsp; right after a URL can break the autolinker
2568
					if (strpos($data, '&nbsp;') !== false)
2569
					{
2570
						$placeholders[html_entity_decode('&nbsp;', null, $context['character_set'])] = '&nbsp;';
0 ignored issues
show
Bug introduced by
null of type null is incompatible with the type integer expected by parameter $flags of html_entity_decode(). ( Ignorable by Annotation )

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

2570
						$placeholders[html_entity_decode('&nbsp;', /** @scrutinizer ignore-type */ null, $context['character_set'])] = '&nbsp;';
Loading history...
2571
						$data = strtr($data, array('&nbsp;' => html_entity_decode('&nbsp;', null, $context['character_set'])));
2572
					}
2573
2574
					// Some reusable character classes
2575
					$excluded_trailing_chars = '!;:.,?';
2576
					$domain_label_chars = '0-9A-Za-z\-' . ($context['utf8'] ? implode('', array(
2577
						'\x{A0}-\x{D7FF}', '\x{F900}-\x{FDCF}', '\x{FDF0}-\x{FFEF}',
2578
						'\x{10000}-\x{1FFFD}', '\x{20000}-\x{2FFFD}', '\x{30000}-\x{3FFFD}',
2579
						'\x{40000}-\x{4FFFD}', '\x{50000}-\x{5FFFD}', '\x{60000}-\x{6FFFD}',
2580
						'\x{70000}-\x{7FFFD}', '\x{80000}-\x{8FFFD}', '\x{90000}-\x{9FFFD}',
2581
						'\x{A0000}-\x{AFFFD}', '\x{B0000}-\x{BFFFD}', '\x{C0000}-\x{CFFFD}',
2582
						'\x{D0000}-\x{DFFFD}', '\x{E1000}-\x{EFFFD}',
2583
					)) : '');
2584
2585
					// Parse any URLs
2586
					if (!isset($disabled['url']) && strpos($data, '[url') === false)
2587
					{
2588
						// URI schemes that require some sort of special handling.
2589
						$schemes = array(
2590
							// Schemes whose URI definitions require a domain name in the
2591
							// authority (or whatever the next part of the URI is).
2592
							'need_domain' => array(
2593
								'aaa', 'aaas', 'acap', 'acct', 'afp', 'cap', 'cid', 'coap',
2594
								'coap+tcp', 'coap+ws', 'coaps', 'coaps+tcp', 'coaps+ws', 'crid',
2595
								'cvs', 'dict', 'dns', 'feed', 'fish', 'ftp', 'git', 'go',
2596
								'gopher', 'h323', 'http', 'https', 'iax', 'icap', 'im', 'imap',
2597
								'ipp', 'ipps', 'irc', 'irc6', 'ircs', 'ldap', 'ldaps', 'mailto',
2598
								'mid', 'mupdate', 'nfs', 'nntp', 'pop', 'pres', 'reload',
2599
								'rsync', 'rtsp', 'sftp', 'sieve', 'sip', 'sips', 'smb', 'snmp',
2600
								'soap.beep', 'soap.beeps', 'ssh', 'svn', 'stun', 'stuns',
2601
								'telnet', 'tftp', 'tip', 'tn3270', 'turn', 'turns', 'tv', 'udp',
2602
								'vemmi', 'vnc', 'webcal', 'ws', 'wss', 'xmlrpc.beep',
2603
								'xmlrpc.beeps', 'xmpp', 'z39.50', 'z39.50r', 'z39.50s',
2604
							),
2605
							// Schemes that allow an empty authority ("://" followed by "/")
2606
							'empty_authority' => array(
2607
								'file', 'ni', 'nih',
2608
							),
2609
							// Schemes that do not use an authority but still have a reasonable
2610
							// chance of working as clickable links.
2611
							'no_authority' => array(
2612
								'about', 'callto', 'geo', 'gg', 'leaptofrogans', 'magnet',
2613
								'mailto', 'maps', 'news', 'ni', 'nih', 'service', 'skype',
2614
								'sms', 'tel', 'tv',
2615
							),
2616
							// Schemes that we should never link.
2617
							'forbidden' => array(
2618
								'javascript', 'data',
2619
							),
2620
						);
2621
2622
						// In case a mod wants to control behaviour for a special URI scheme.
2623
						call_integration_hook('integrate_autolinker_schemes', array(&$schemes));
2624
2625
						// Don't repeat this unnecessarily.
2626
						if (empty($url_regex))
2627
						{
2628
							// PCRE subroutines for efficiency.
2629
							$pcre_subroutines = array(
2630
								'tlds' => $modSettings['tld_regex'],
2631
								'pct' => '%[0-9A-Fa-f]{2}',
2632
								'domain_label_char' => '[' . $domain_label_chars . ']',
2633
								'not_domain_label_char' => '[^' . $domain_label_chars . ']',
2634
								'domain' => '(?:(?P>domain_label_char)+\.)+(?P>tlds)',
2635
								'no_domain' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:@]|(?P>pct))+',
2636
								'scheme_need_domain' => build_regex($schemes['need_domain'], '~'),
2637
								'scheme_empty_authority' => build_regex($schemes['empty_authority'], '~'),
2638
								'scheme_no_authority' => build_regex($schemes['no_authority'], '~'),
2639
								'scheme_any' => '[A-Za-z][0-9A-Za-z+\-.]*',
2640
								'user_info' => '(?:(?P>domain_label_char)|[._\~!$&\'()*+,;=:]|(?P>pct))+',
2641
								'dec_octet' => '(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)',
2642
								'h16' => '[0-9A-Fa-f]{1,4}',
2643
								'ipv4' => '(?:\b(?:(?P>dec_octet)\.){3}(?P>dec_octet)\b)',
2644
								'ipv6' => '\[(?:' . implode('|', array(
2645
									'(?:(?P>h16):){7}(?P>h16)',
2646
									'(?:(?P>h16):){1,7}:',
2647
									'(?:(?P>h16):){1,6}(?::(?P>h16))',
2648
									'(?:(?P>h16):){1,5}(?::(?P>h16)){1,2}',
2649
									'(?:(?P>h16):){1,4}(?::(?P>h16)){1,3}',
2650
									'(?:(?P>h16):){1,3}(?::(?P>h16)){1,4}',
2651
									'(?:(?P>h16):){1,2}(?::(?P>h16)){1,5}',
2652
									'(?P>h16):(?::(?P>h16)){1,6}',
2653
									':(?:(?::(?P>h16)){1,7}|:)',
2654
									'fe80:(?::(?P>h16)){0,4}%[0-9A-Za-z]+',
2655
									'::(ffff(:0{1,4})?:)?(?P>ipv4)',
2656
									'(?:(?P>h16):){1,4}:(?P>ipv4)',
2657
								)) . ')\]',
2658
								'host' => '(?:' . implode('|', array(
2659
									'localhost',
2660
									'(?P>domain)',
2661
									'(?P>ipv4)',
2662
									'(?P>ipv6)',
2663
								)) . ')',
2664
								'authority' => '(?:(?P>user_info)@)?(?P>host)(?::\d+)?',
2665
							);
2666
2667
							// Brackets and quotation marks are problematic at the end of an IRI.
2668
							// E.g.: `http://foo.com/baz(qux)` vs. `(http://foo.com/baz_qux)`
2669
							// In the first case, the user probably intended the `)` as part of the
2670
							// IRI, but not in the second case. To account for this, we test for
2671
							// balanced pairs within the IRI.
2672
							$balanced_pairs = array(
2673
								// Brackets and parentheses
2674
								'(' => ')', '[' => ']', '{' => '}',
2675
								// Double quotation marks
2676
								'"' => '"',
2677
								html_entity_decode('&#x201C;', null, $context['character_set']) => html_entity_decode('&#x201D;', null, $context['character_set']),
2678
								html_entity_decode('&#x201E;', null, $context['character_set']) => html_entity_decode('&#x201D;', null, $context['character_set']),
2679
								html_entity_decode('&#x201F;', null, $context['character_set']) => html_entity_decode('&#x201D;', null, $context['character_set']),
2680
								html_entity_decode('&#x00AB;', null, $context['character_set']) => html_entity_decode('&#x00BB;', null, $context['character_set']),
2681
								// Single quotation marks
2682
								'\'' => '\'',
2683
								html_entity_decode('&#x2018;', null, $context['character_set']) => html_entity_decode('&#x2019;', null, $context['character_set']),
2684
								html_entity_decode('&#x201A;', null, $context['character_set']) => html_entity_decode('&#x2019;', null, $context['character_set']),
2685
								html_entity_decode('&#x201B;', null, $context['character_set']) => html_entity_decode('&#x2019;', null, $context['character_set']),
2686
								html_entity_decode('&#x2039;', null, $context['character_set']) => html_entity_decode('&#x203A;', null, $context['character_set']),
2687
							);
2688
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2689
								$balanced_pairs[$smcFunc['htmlspecialchars']($pair_opener)] = $smcFunc['htmlspecialchars']($pair_closer);
2690
2691
							$bracket_quote_chars = '';
2692
							$bracket_quote_entities = array();
2693
							foreach ($balanced_pairs as $pair_opener => $pair_closer)
2694
							{
2695
								if ($pair_opener == $pair_closer)
2696
									$pair_closer = '';
2697
2698
								foreach (array($pair_opener, $pair_closer) as $bracket_quote)
2699
								{
2700
									if (strpos($bracket_quote, '&') === false)
2701
										$bracket_quote_chars .= $bracket_quote;
2702
									else
2703
										$bracket_quote_entities[] = substr($bracket_quote, 1);
2704
								}
2705
							}
2706
							$bracket_quote_chars = str_replace(array('[', ']'), array('\[', '\]'), $bracket_quote_chars);
2707
2708
							$pcre_subroutines['bracket_quote'] = '[' . $bracket_quote_chars . ']|&' . build_regex($bracket_quote_entities, '~');
0 ignored issues
show
Bug introduced by
Are you sure build_regex($bracket_quote_entities, '~') 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

2708
							$pcre_subroutines['bracket_quote'] = '[' . $bracket_quote_chars . ']|&' . /** @scrutinizer ignore-type */ build_regex($bracket_quote_entities, '~');
Loading history...
2709
							$pcre_subroutines['allowed_entities'] = '&(?!' . build_regex(array_merge($bracket_quote_entities, array('lt;', 'gt;')), '~') . ')';
0 ignored issues
show
Bug introduced by
Are you sure build_regex(array_merge(...ay('lt;', 'gt;')), '~') 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

2709
							$pcre_subroutines['allowed_entities'] = '&(?!' . /** @scrutinizer ignore-type */ build_regex(array_merge($bracket_quote_entities, array('lt;', 'gt;')), '~') . ')';
Loading history...
2710
							$pcre_subroutines['excluded_lookahead'] = '(?![' . $excluded_trailing_chars . ']*(?>[\h\v]|<br>|$))';
2711
2712
							foreach (array('path', 'query', 'fragment') as $part)
2713
							{
2714
								switch ($part) {
2715
									case 'path':
2716
										$part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '/#&';
2717
										$part_excluded_trailing_chars = str_replace('?', '', $excluded_trailing_chars);
2718
										break;
2719
2720
									case 'query':
2721
										$part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '#&';
2722
										$part_excluded_trailing_chars = $excluded_trailing_chars;
2723
										break;
2724
2725
									default:
2726
										$part_disallowed_chars = '\h\v<>' . $bracket_quote_chars . $excluded_trailing_chars . '&';
2727
										$part_excluded_trailing_chars = $excluded_trailing_chars;
2728
										break;
2729
								}
2730
								$pcre_subroutines[$part . '_allowed'] = '[^' . $part_disallowed_chars . ']|(?P>allowed_entities)|[' . $part_excluded_trailing_chars . '](?P>excluded_lookahead)';
2731
2732
								$balanced_construct_regex = array();
2733
2734
								foreach ($balanced_pairs as $pair_opener => $pair_closer)
2735
									$balanced_construct_regex[] = preg_quote($pair_opener) . '(?P>' . $part . '_recursive)*+' . preg_quote($pair_closer);
2736
2737
								$pcre_subroutines[$part . '_balanced'] = '(?:' . implode('|', $balanced_construct_regex) . ')(?P>' . $part . '_allowed)*+';
2738
								$pcre_subroutines[$part . '_recursive'] = '(?' . '>(?P>' . $part . '_allowed)|(?P>' . $part . '_balanced))';
2739
2740
								$pcre_subroutines[$part . '_segment'] =
2741
									// Allowed characters besides brackets and quotation marks
2742
									'(?P>' . $part . '_allowed)*+' .
2743
									// Brackets and quotation marks that are either...
2744
									'(?:' .
2745
										// part of a balanced construct
2746
										'(?P>' . $part . '_balanced)' .
2747
										// or
2748
										'|' .
2749
										// unpaired but not at the end
2750
										'(?P>bracket_quote)(?=(?P>' . $part . '_allowed))' .
2751
									')*+';
2752
							}
2753
2754
							// Time to build this monster!
2755
							$url_regex =
2756
							// 1. IRI scheme and domain components
2757
							'(?:' .
2758
								// 1a. IRIs with a scheme, or at least an opening "//"
2759
								'(?:' .
2760
2761
									// URI scheme (or lack thereof for schemeless URLs)
2762
									'(?:' .
2763
										// URI scheme and colon
2764
										'\b' .
2765
										'(?:' .
2766
											// Either a scheme that need a domain in the authority
2767
											// (Remember for later that we need a domain)
2768
											'(?P<need_domain>(?P>scheme_need_domain)):' .
2769
											// or
2770
											'|' .
2771
											// a scheme that allows an empty authority
2772
											// (Remember for later that the authority can be empty)
2773
											'(?P<empty_authority>(?P>scheme_empty_authority)):' .
2774
											// or
2775
											'|' .
2776
											// a scheme that uses no authority
2777
											'(?P>scheme_no_authority):(?!//)' .
2778
											// or
2779
											'|' .
2780
											// another scheme, but only if it is followed by "://"
2781
											'(?P>scheme_any):(?=//)' .
2782
										')' .
2783
2784
										// or
2785
										'|' .
2786
2787
										// An empty string followed by "//" for schemeless URLs
2788
										'(?P<schemeless>(?=//))' .
2789
									')' .
2790
2791
									// IRI authority chunk (maybe)
2792
									'(?:' .
2793
										// (Keep track of whether we find a valid authority or not)
2794
										'(?P<has_authority>' .
2795
											// 2 slashes before the authority itself
2796
											'//' .
2797
											'(?:' .
2798
												// If there was no scheme...
2799
												'(?(<schemeless>)' .
2800
													// require an authority that contains a domain.
2801
													'(?P>authority)' .
2802
2803
													// Else if a domain is needed...
2804
													'|(?(<need_domain>)' .
2805
														// require an authority with a domain.
2806
														'(?P>authority)' .
2807
2808
														// Else if an empty authority is allowed...
2809
														'|(?(<empty_authority>)' .
2810
															// then require either
2811
															'(?:' .
2812
																// empty string, followed by a "/"
2813
																'(?=/)' .
2814
																// or
2815
																'|' .
2816
																// an authority with a domain.
2817
																'(?P>authority)' .
2818
															')' .
2819
2820
															// Else just a run of IRI characters.
2821
															'|(?P>no_domain)' .
2822
														')' .
2823
													')' .
2824
												')' .
2825
											')' .
2826
											// Followed by a non-domain character or end of line
2827
											'(?=(?P>not_domain_label_char)|$)' .
2828
										')' .
2829
2830
										// or, if there is a scheme but no authority
2831
										// (e.g. "mailto:" URLs)...
2832
										'|' .
2833
2834
										// A run of IRI characters
2835
										'(?P>no_domain)' .
2836
										// If scheme needs a domain, require a dot and a TLD
2837
										'(?(<need_domain>)\.(?P>tlds))' .
2838
										// Followed by a non-domain character or end of line
2839
										'(?=(?P>not_domain_label_char)|$)' .
2840
									')' .
2841
								')' .
2842
2843
								// Or, if there is neither a scheme nor an authority...
2844
								'|' .
2845
2846
								// 1b. Naked domains
2847
								// (e.g. "example.com" in "Go to example.com for an example.")
2848
								'(?P<naked_domain>' .
2849
									// Preceded by start of line or a space
2850
									'(?<=^|<br>|[\h\v])' .
2851
									// A domain name
2852
									'(?P>domain)' .
2853
									// Followed by a non-domain character or end of line
2854
									'(?=(?P>not_domain_label_char)|$)' .
2855
								')' .
2856
							')' .
2857
2858
							// 2. IRI path, query, and fragment components (if present)
2859
							'(?:' .
2860
								// If the IRI has an authority or is a naked domain and any of these
2861
								// components exist, the path must start with a single "/".
2862
								// Note: technically, it is valid to append a query or fragment
2863
								// directly to the authority chunk without a "/", but supporting
2864
								// that in the autolinker would produce a lot of false positives,
2865
								// so we don't.
2866
								'(?=' .
2867
									// If we found an authority above...
2868
									'(?(<has_authority>)' .
2869
										// require a "/"
2870
										'/' .
2871
										// Else if we found a naked domain above...
2872
										'|(?(<naked_domain>)' .
2873
											// require a "/"
2874
											'/' .
2875
										')' .
2876
									')' .
2877
								')' .
2878
2879
								// 2.a. Path component, if any.
2880
								'(?:' .
2881
									// Can have one or more segments
2882
									'(?:' .
2883
										// Not preceded by a "/", except in the special case of an
2884
										// empty authority immediately before the path.
2885
										'(?(<empty_authority>)' .
2886
											'(?:(?<=://)|(?<!/))' .
2887
											'|' .
2888
											'(?<!/)' .
2889
										')' .
2890
										// Initial "/"
2891
										'/' .
2892
										// Then a run of allowed path segement characters
2893
										'(?P>path_segment)*+' .
2894
									')*+' .
2895
								')' .
2896
2897
								// 2.b. Query component, if any.
2898
								'(?:' .
2899
									// Initial "?" that is not last character.
2900
									'\?' . '(?=(?P>bracket_quote)*(?P>query_allowed))' .
2901
									// Then a run of allowed query characters
2902
									'(?P>query_segment)*+' .
2903
								')?' .
2904
2905
								// 2.c. Fragment component, if any.
2906
								'(?:' .
2907
									// Initial "#" that is not last character.
2908
									'#' . '(?=(?P>bracket_quote)*(?P>fragment_allowed))' .
2909
									// Then a run of allowed fragment characters
2910
									'(?P>fragment_segment)*+' .
2911
								')?' .
2912
							')?+';
2913
2914
							// Finally, define the PCRE subroutines in the regex.
2915
							$url_regex .= '(?(DEFINE)';
2916
2917
							foreach ($pcre_subroutines as $name => $subroutine)
2918
								$url_regex .= '(?<' . $name . '>' . $subroutine . ')';
0 ignored issues
show
Bug introduced by
Are you sure $subroutine of type array|mixed|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

2918
								$url_regex .= '(?<' . $name . '>' . /** @scrutinizer ignore-type */ $subroutine . ')';
Loading history...
2919
2920
							$url_regex .= ')';
2921
						}
2922
2923
						$tmp_data = preg_replace_callback(
2924
							'~' . $url_regex . '~i' . ($context['utf8'] ? 'u' : ''),
2925
							function($matches) use ($schemes)
2926
							{
2927
								$url = array_shift($matches);
2928
2929
								// If this isn't a clean URL, bail out
2930
								if ($url != sanitize_iri($url))
2931
									return $url;
2932
2933
								$parsedurl = parse_url($url);
2934
2935
								if (!isset($parsedurl['scheme']))
2936
									$parsedurl['scheme'] = '';
2937
2938
								if ($parsedurl['scheme'] == 'mailto')
2939
								{
2940
									if (isset($disabled['email']))
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...
2941
										return $url;
2942
2943
									// Is this version of PHP capable of validating this email address?
2944
									$can_validate = defined('FILTER_FLAG_EMAIL_UNICODE') || strlen($parsedurl['path']) == strspn(strtolower($parsedurl['path']), 'abcdefghijklmnopqrstuvwxyz0123456789!#$%&\'*+-/=?^_`{|}~.@');
2945
2946
									$flags = defined('FILTER_FLAG_EMAIL_UNICODE') ? FILTER_FLAG_EMAIL_UNICODE : null;
2947
2948
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, $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

2948
									if (!$can_validate || filter_var($parsedurl['path'], FILTER_VALIDATE_EMAIL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
2949
										return '[email=' . str_replace('mailto:', '', $url) . ']' . $url . '[/email]';
2950
									else
2951
										return $url;
2952
								}
2953
2954
								// Are we linking a schemeless URL or naked domain name (e.g. "example.com")?
2955
								if (empty($parsedurl['scheme']))
2956
									$fullUrl = '//' . ltrim($url, ':/');
2957
								else
2958
									$fullUrl = $url;
2959
2960
								// Ensure the host name is in its canonical form.
2961
								$host = !empty($parsedurl['host']) ? $parsedurl['host'] : parse_url($fullUrl, PHP_URL_HOST);
2962
2963
								if (!empty($host))
2964
								{
2965
									$ascii_host = idn_to_ascii($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
2966
2967
									if ($ascii_host !== $host)
2968
									{
2969
										$fullUrl = substr($fullUrl, 0, strpos($fullUrl, $host)) . $ascii_host . substr($fullUrl, strpos($fullUrl, $host) + strlen($host));
2970
2971
										$utf8_host = idn_to_utf8($ascii_host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
2972
2973
										if ($utf8_host !== $host)
2974
										{
2975
											$url = substr($url, 0, strpos($url, $host)) . $utf8_host . substr($url, strpos($url, $host) + strlen($host));
2976
										}
2977
									}
2978
								}
2979
2980
								// Make sure that $fullUrl really is valid
2981
								if (in_array($parsedurl['scheme'], $schemes['forbidden']) || (!in_array($parsedurl['scheme'], $schemes['no_authority']) && validate_iri((strpos($fullUrl, '//') === 0 ? 'http:' : '') . $fullUrl) === false))
2982
									return $url;
2983
2984
								return '[url=&quot;' . str_replace(array('[', ']'), array('&#91;', '&#93;'), $fullUrl) . '&quot;]' . $url . '[/url]';
2985
							},
2986
							$data
2987
						);
2988
2989
						if (!is_null($tmp_data))
2990
							$data = $tmp_data;
2991
					}
2992
2993
					// Next, emails...  Must be careful not to step on enablePostHTML logic above...
2994
					if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false && stripos($data, 'mailto:') === false)
2995
					{
2996
						// Preceded by a space or start of line
2997
						$email_regex = '(?<=^|<br>|[\h\v])' .
2998
2999
						// An email address
3000
						'[' . $domain_label_chars . '_.]{1,80}' .
3001
						'@' .
3002
						'[' . $domain_label_chars . '.]+' .
3003
						'\.' . $modSettings['tld_regex'] .
3004
3005
						// Followed by a non-domain character or end of line
3006
						'(?=[^' . $domain_label_chars . ']|$)';
3007
3008
						$tmp_data = preg_replace('~' . $email_regex . '~i' . ($context['utf8'] ? 'u' : ''), '[email]$0[/email]', $data);
3009
3010
						if (!is_null($tmp_data))
3011
							$data = $tmp_data;
3012
					}
3013
3014
					// Save a little memory.
3015
					unset($tmp_data);
3016
				}
3017
			}
3018
3019
			// Restore any placeholders
3020
			$data = strtr($data, $placeholders);
3021
3022
			$data = strtr($data, array("\t" => '&nbsp;&nbsp;&nbsp;'));
3023
3024
			// If it wasn't changed, no copying or other boring stuff has to happen!
3025
			if ($data != substr($message, $last_pos, $pos - $last_pos))
3026
			{
3027
				$message = substr($message, 0, $last_pos) . $data . substr($message, $pos);
3028
3029
				// Since we changed it, look again in case we added or removed a tag.  But we don't want to skip any.
3030
				$old_pos = strlen($data) + $last_pos;
3031
				$pos = strpos($message, '[', $last_pos);
3032
				$pos = $pos === false ? $old_pos : min($pos, $old_pos);
3033
			}
3034
		}
3035
3036
		// Are we there yet?  Are we there yet?
3037
		if ($pos >= strlen($message) - 1)
3038
			break;
3039
3040
		$tag_character = strtolower($message[$pos + 1]);
3041
3042
		if ($tag_character == '/' && !empty($open_tags))
3043
		{
3044
			$pos2 = strpos($message, ']', $pos + 1);
3045
			if ($pos2 == $pos + 2)
3046
				continue;
3047
3048
			$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2));
3049
3050
			// A closing tag that doesn't match any open tags? Skip it.
3051
			if (!in_array($look_for, array_map(function($code) { return $code['tag']; }, $open_tags)))
3052
				continue;
3053
3054
			$to_close = array();
3055
			$block_level = null;
3056
3057
			do
3058
			{
3059
				$tag = array_pop($open_tags);
3060
				if (!$tag)
3061
					break;
3062
3063
				if (!empty($tag['block_level']))
3064
				{
3065
					// Only find out if we need to.
3066
					if ($block_level === false)
3067
					{
3068
						array_push($open_tags, $tag);
3069
						break;
3070
					}
3071
3072
					// The idea is, if we are LOOKING for a block level tag, we can close them on the way.
3073
					if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]]))
3074
					{
3075
						foreach ($bbc_codes[$look_for[0]] as $temp)
3076
							if ($temp['tag'] == $look_for)
3077
							{
3078
								$block_level = !empty($temp['block_level']);
3079
								break;
3080
							}
3081
					}
3082
3083
					if ($block_level !== true)
3084
					{
3085
						$block_level = false;
3086
						array_push($open_tags, $tag);
3087
						break;
3088
					}
3089
				}
3090
3091
				$to_close[] = $tag;
3092
			}
3093
			while ($tag['tag'] != $look_for);
3094
3095
			// Did we just eat through everything and not find it?
3096
			if ((empty($open_tags) && (empty($tag) || $tag['tag'] != $look_for)))
3097
			{
3098
				$open_tags = $to_close;
3099
				continue;
3100
			}
3101
			elseif (!empty($to_close) && $tag['tag'] != $look_for)
3102
			{
3103
				if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]]))
3104
				{
3105
					foreach ($bbc_codes[$look_for[0]] as $temp)
3106
						if ($temp['tag'] == $look_for)
3107
						{
3108
							$block_level = !empty($temp['block_level']);
3109
							break;
3110
						}
3111
				}
3112
3113
				// We're not looking for a block level tag (or maybe even a tag that exists...)
3114
				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...
3115
				{
3116
					foreach ($to_close as $tag)
3117
						array_push($open_tags, $tag);
3118
					continue;
3119
				}
3120
			}
3121
3122
			foreach ($to_close as $tag)
3123
			{
3124
				$message = substr($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1);
3125
				$pos += strlen($tag['after']) + 2;
3126
				$pos2 = $pos - 1;
3127
3128
				// See the comment at the end of the big loop - just eating whitespace ;).
3129
				$whitespace_regex = '';
3130
				if (!empty($tag['block_level']))
3131
					$whitespace_regex .= '(&nbsp;|\s)*(<br\s*/?' . '>)?';
3132
				// Trim one line of whitespace after unnested tags, but all of it after nested ones
3133
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3134
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3135
3136
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3137
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3138
			}
3139
3140
			if (!empty($to_close))
3141
			{
3142
				$to_close = array();
0 ignored issues
show
Unused Code introduced by
The assignment to $to_close is dead and can be removed.
Loading history...
3143
				$pos--;
3144
			}
3145
3146
			continue;
3147
		}
3148
3149
		// No tags for this character, so just keep going (fastest possible course.)
3150
		if (!isset($bbc_codes[$tag_character]))
3151
			continue;
3152
3153
		$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1];
3154
		$tag = null;
3155
		foreach ($bbc_codes[$tag_character] as $possible)
3156
		{
3157
			$pt_strlen = strlen($possible['tag']);
3158
3159
			// Not a match?
3160
			if (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag'])
3161
				continue;
3162
3163
			$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : '';
3164
3165
			// A tag is the last char maybe
3166
			if ($next_c == '')
3167
				break;
3168
3169
			// A test validation?
3170
			if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0)
3171
				continue;
3172
			// Do we want parameters?
3173
			elseif (!empty($possible['parameters']))
3174
			{
3175
				// Are all the parameters optional?
3176
				$param_required = false;
3177
				foreach ($possible['parameters'] as $param)
3178
				{
3179
					if (empty($param['optional']))
3180
					{
3181
						$param_required = true;
3182
						break;
3183
					}
3184
				}
3185
3186
				if ($param_required && $next_c != ' ')
3187
					continue;
3188
			}
3189
			elseif (isset($possible['type']))
3190
			{
3191
				// Do we need an equal sign?
3192
				if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=')
3193
					continue;
3194
				// Maybe we just want a /...
3195
				if ($possible['type'] == 'closed' && $next_c != ']' && substr($message, $pos + 1 + $pt_strlen, 2) != '/]' && substr($message, $pos + 1 + $pt_strlen, 3) != ' /]')
3196
					continue;
3197
				// An immediate ]?
3198
				if ($possible['type'] == 'unparsed_content' && $next_c != ']')
3199
					continue;
3200
			}
3201
			// No type means 'parsed_content', which demands an immediate ] without parameters!
3202
			elseif ($next_c != ']')
3203
				continue;
3204
3205
			// Check allowed tree?
3206
			if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents'])))
3207
				continue;
3208
			elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children']))
3209
				continue;
3210
			// If this is in the list of disallowed child tags, don't parse it.
3211
			elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children']))
3212
				continue;
3213
3214
			$pos1 = $pos + 1 + $pt_strlen + 1;
3215
3216
			// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes.
3217
			if ($possible['tag'] == 'quote')
3218
			{
3219
				// Start with standard
3220
				$quote_alt = false;
3221
				foreach ($open_tags as $open_quote)
3222
				{
3223
					// Every parent quote this quote has flips the styling
3224
					if ($open_quote['tag'] == 'quote')
3225
						$quote_alt = !$quote_alt;
0 ignored issues
show
introduced by
The condition $quote_alt is always false.
Loading history...
3226
				}
3227
				// Add a class to the quote to style alternating blockquotes
3228
				$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">'));
3229
			}
3230
3231
			// This is long, but it makes things much easier and cleaner.
3232
			if (!empty($possible['parameters']))
3233
			{
3234
				// Build a regular expression for each parameter for the current tag.
3235
				$regex_key = $smcFunc['json_encode']($possible['parameters']);
3236
				if (!isset($params_regexes[$regex_key]))
3237
				{
3238
					$params_regexes[$regex_key] = '';
3239
3240
					foreach ($possible['parameters'] as $p => $info)
3241
						$params_regexes[$regex_key] .= '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '&quot;') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '&quot;') . '\s*)' . (empty($info['optional']) ? '' : '?');
3242
				}
3243
3244
				// Extract the string that potentially holds our parameters.
3245
				$blob = preg_split('~\[/?(?:' . $alltags_regex . ')~i', substr($message, $pos));
3246
				$blobs = preg_split('~\]~i', $blob[1]);
3247
3248
				$splitters = implode('=|', array_keys($possible['parameters'])) . '=';
3249
3250
				// Progressively append more blobs until we find our parameters or run out of blobs
3251
				$blob_counter = 1;
3252
				while ($blob_counter <= count($blobs))
3253
				{
3254
					$given_param_string = implode(']', array_slice($blobs, 0, $blob_counter++));
3255
3256
					$given_params = preg_split('~\s(?=(' . $splitters . '))~i', $given_param_string);
3257
					sort($given_params, SORT_STRING);
3258
3259
					$match = preg_match('~^' . $params_regexes[$regex_key] . '$~i', implode(' ', $given_params), $matches) !== 0;
3260
3261
					if ($match)
3262
						break;
3263
				}
3264
3265
				// Didn't match our parameter list, try the next possible.
3266
				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...
3267
					continue;
3268
3269
				$params = array();
3270
				for ($i = 1, $n = count($matches); $i < $n; $i += 2)
3271
				{
3272
					$key = strtok(ltrim($matches[$i]), '=');
3273
					if ($key === false)
3274
						continue;
3275
					elseif (isset($possible['parameters'][$key]['value']))
3276
						$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1]));
3277
					elseif (isset($possible['parameters'][$key]['validate']))
3278
						$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]);
3279
					else
3280
						$params['{' . $key . '}'] = $matches[$i + 1];
3281
3282
					// Just to make sure: replace any $ or { so they can't interpolate wrongly.
3283
					$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '&#036;', '{' => '&#123;'));
3284
				}
3285
3286
				foreach ($possible['parameters'] as $p => $info)
3287
				{
3288
					if (!isset($params['{' . $p . '}']))
3289
					{
3290
						if (!isset($info['default']))
3291
							$params['{' . $p . '}'] = '';
3292
						elseif (isset($possible['parameters'][$p]['value']))
3293
							$params['{' . $p . '}'] = strtr($possible['parameters'][$p]['value'], array('$1' => $info['default']));
3294
						elseif (isset($possible['parameters'][$p]['validate']))
3295
							$params['{' . $p . '}'] = $possible['parameters'][$p]['validate']($info['default']);
3296
						else
3297
							$params['{' . $p . '}'] = $info['default'];
3298
					}
3299
				}
3300
3301
				$tag = $possible;
3302
3303
				// Put the parameters into the string.
3304
				if (isset($tag['before']))
3305
					$tag['before'] = strtr($tag['before'], $params);
3306
				if (isset($tag['after']))
3307
					$tag['after'] = strtr($tag['after'], $params);
3308
				if (isset($tag['content']))
3309
					$tag['content'] = strtr($tag['content'], $params);
3310
3311
				$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...
3312
			}
3313
			else
3314
			{
3315
				$tag = $possible;
3316
				$params = array();
3317
			}
3318
			break;
3319
		}
3320
3321
		// Item codes are complicated buggers... they are implicit [li]s and can make [list]s!
3322
		if ($smileys !== false && $tag === null && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] == ']' && !isset($disabled['list']) && !isset($disabled['li']))
3323
		{
3324
			if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>')))
3325
				continue;
3326
3327
			$tag = $itemcodes[$message[$pos + 1]];
3328
3329
			// First let's set up the tree: it needs to be in a list, or after an li.
3330
			if ($inside === null || ($inside['tag'] != 'list' && $inside['tag'] != 'li'))
3331
			{
3332
				$open_tags[] = array(
3333
					'tag' => 'list',
3334
					'after' => '</ul>',
3335
					'block_level' => true,
3336
					'require_children' => array('li'),
3337
					'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3338
				);
3339
				$code = '<ul class="bbc_list">';
3340
			}
3341
			// We're in a list item already: another itemcode?  Close it first.
3342
			elseif ($inside['tag'] == 'li')
3343
			{
3344
				array_pop($open_tags);
3345
				$code = '</li>';
3346
			}
3347
			else
3348
				$code = '';
3349
3350
			// Now we open a new tag.
3351
			$open_tags[] = array(
3352
				'tag' => 'li',
3353
				'after' => '</li>',
3354
				'trim' => 'outside',
3355
				'block_level' => true,
3356
				'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null,
3357
			);
3358
3359
			// First, open the tag...
3360
			$code .= '<li' . ($tag == '' ? '' : ' type="' . $tag . '"') . '>';
3361
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3);
3362
			$pos += strlen($code) - 1 + 2;
3363
3364
			// Next, find the next break (if any.)  If there's more itemcode after it, keep it going - otherwise close!
3365
			$pos2 = strpos($message, '<br>', $pos);
3366
			$pos3 = strpos($message, '[/', $pos);
3367
			if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false))
3368
			{
3369
				preg_match('~^(<br>|&nbsp;|\s|\[)+~', substr($message, $pos2 + 4), $matches);
3370
				$message = substr($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2);
3371
3372
				$open_tags[count($open_tags) - 2]['after'] = '</ul>';
3373
			}
3374
			// Tell the [list] that it needs to close specially.
3375
			else
3376
			{
3377
				// Move the li over, because we're not sure what we'll hit.
3378
				$open_tags[count($open_tags) - 1]['after'] = '';
3379
				$open_tags[count($open_tags) - 2]['after'] = '</li></ul>';
3380
			}
3381
3382
			continue;
3383
		}
3384
3385
		// Implicitly close lists and tables if something other than what's required is in them.  This is needed for itemcode.
3386
		if ($tag === null && $inside !== null && !empty($inside['require_children']))
3387
		{
3388
			array_pop($open_tags);
3389
3390
			$message = substr($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos);
3391
			$pos += strlen($inside['after']) - 1 + 2;
3392
		}
3393
3394
		// No tag?  Keep looking, then.  Silly people using brackets without actual tags.
3395
		if ($tag === null)
3396
			continue;
3397
3398
		// Propagate the list to the child (so wrapping the disallowed tag won't work either.)
3399
		if (isset($inside['disallow_children']))
3400
			$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children'];
3401
3402
		// Is this tag disabled?
3403
		if (isset($disabled[$tag['tag']]))
3404
		{
3405
			if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content']))
3406
			{
3407
				$tag['before'] = !empty($tag['block_level']) ? '<div>' : '';
3408
				$tag['after'] = !empty($tag['block_level']) ? '</div>' : '';
3409
				$tag['content'] = isset($tag['type']) && $tag['type'] == 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1');
3410
			}
3411
			elseif (isset($tag['disabled_before']) || isset($tag['disabled_after']))
3412
			{
3413
				$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : '');
3414
				$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : '');
3415
			}
3416
			else
3417
				$tag['content'] = $tag['disabled_content'];
3418
		}
3419
3420
		// we use this a lot
3421
		$tag_strlen = strlen($tag['tag']);
3422
3423
		// The only special case is 'html', which doesn't need to close things.
3424
		if (!empty($tag['block_level']) && $tag['tag'] != 'html' && empty($inside['block_level']))
3425
		{
3426
			$n = count($open_tags) - 1;
3427
			while (empty($open_tags[$n]['block_level']) && $n >= 0)
3428
				$n--;
3429
3430
			// Close all the non block level tags so this tag isn't surrounded by them.
3431
			for ($i = count($open_tags) - 1; $i > $n; $i--)
3432
			{
3433
				$message = substr($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos);
3434
				$ot_strlen = strlen($open_tags[$i]['after']);
3435
				$pos += $ot_strlen + 2;
3436
				$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...
3437
3438
				// Trim or eat trailing stuff... see comment at the end of the big loop.
3439
				$whitespace_regex = '';
3440
				if (!empty($tag['block_level']))
3441
					$whitespace_regex .= '(&nbsp;|\s)*(<br>)?';
3442
				if (!empty($tag['trim']) && $tag['trim'] != 'inside')
3443
					$whitespace_regex .= empty($tag['require_parents']) ? '(&nbsp;|\s)*' : '(<br>|&nbsp;|\s)*';
3444
				if (!empty($whitespace_regex) && preg_match('~' . $whitespace_regex . '~', substr($message, $pos), $matches) != 0)
3445
					$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0]));
3446
3447
				array_pop($open_tags);
3448
			}
3449
		}
3450
3451
		// Can't read past the end of the message
3452
		$pos1 = min(strlen($message), $pos1);
3453
3454
		// No type means 'parsed_content'.
3455
		if (!isset($tag['type']))
3456
		{
3457
			$open_tags[] = $tag;
3458
3459
			// There's no data to change, but maybe do something based on params?
3460
			$data = null;
3461
			if (isset($tag['validate']))
3462
				$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...
3463
3464
			$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1);
3465
			$pos += strlen($tag['before']) - 1 + 2;
3466
		}
3467
		// Don't parse the content, just skip it.
3468
		elseif ($tag['type'] == 'unparsed_content')
3469
		{
3470
			$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1);
3471
			if ($pos2 === false)
3472
				continue;
3473
3474
			$data = substr($message, $pos1, $pos2 - $pos1);
3475
3476
			if (!empty($tag['block_level']) && substr($data, 0, 4) == '<br>')
3477
				$data = substr($data, 4);
3478
3479
			if (isset($tag['validate']))
3480
				$tag['validate']($tag, $data, $disabled, $params);
3481
3482
			$code = strtr($tag['content'], array('$1' => $data));
3483
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen);
3484
3485
			$pos += strlen($code) - 1 + 2;
3486
			$last_pos = $pos + 1;
3487
		}
3488
		// Don't parse the content, just skip it.
3489
		elseif ($tag['type'] == 'unparsed_equals_content')
3490
		{
3491
			// The value may be quoted for some tags - check.
3492
			if (isset($tag['quoted']))
3493
			{
3494
				$quoted = substr($message, $pos1, 6) == '&quot;';
3495
				if ($tag['quoted'] != 'optional' && !$quoted)
3496
					continue;
3497
3498
				if ($quoted)
3499
					$pos1 += 6;
3500
			}
3501
			else
3502
				$quoted = false;
3503
3504
			$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...
3505
			if ($pos2 === false)
3506
				continue;
3507
3508
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3509
			if ($pos3 === false)
3510
				continue;
3511
3512
			$data = array(
3513
				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...
3514
				substr($message, $pos1, $pos2 - $pos1)
3515
			);
3516
3517
			if (!empty($tag['block_level']) && substr($data[0], 0, 4) == '<br>')
3518
				$data[0] = substr($data[0], 4);
3519
3520
			// Validation for my parking, please!
3521
			if (isset($tag['validate']))
3522
				$tag['validate']($tag, $data, $disabled, $params);
3523
3524
			$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1]));
3525
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3526
			$pos += strlen($code) - 1 + 2;
3527
		}
3528
		// A closed tag, with no content or value.
3529
		elseif ($tag['type'] == 'closed')
3530
		{
3531
			$pos2 = strpos($message, ']', $pos);
3532
3533
			// Maybe a custom BBC wants to do something special?
3534
			$data = null;
3535
			if (isset($tag['validate']))
3536
				$tag['validate']($tag, $data, $disabled, $params);
3537
3538
			$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1);
3539
			$pos += strlen($tag['content']) - 1 + 2;
3540
		}
3541
		// This one is sorta ugly... :/.  Unfortunately, it's needed for flash.
3542
		elseif ($tag['type'] == 'unparsed_commas_content')
3543
		{
3544
			$pos2 = strpos($message, ']', $pos1);
3545
			if ($pos2 === false)
3546
				continue;
3547
3548
			$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2);
3549
			if ($pos3 === false)
3550
				continue;
3551
3552
			// We want $1 to be the content, and the rest to be csv.
3553
			$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1));
3554
			$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1);
3555
3556
			if (isset($tag['validate']))
3557
				$tag['validate']($tag, $data, $disabled, $params);
3558
3559
			$code = $tag['content'];
3560
			foreach ($data as $k => $d)
3561
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3562
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen);
3563
			$pos += strlen($code) - 1 + 2;
3564
		}
3565
		// This has parsed content, and a csv value which is unparsed.
3566
		elseif ($tag['type'] == 'unparsed_commas')
3567
		{
3568
			$pos2 = strpos($message, ']', $pos1);
3569
			if ($pos2 === false)
3570
				continue;
3571
3572
			$data = explode(',', substr($message, $pos1, $pos2 - $pos1));
3573
3574
			if (isset($tag['validate']))
3575
				$tag['validate']($tag, $data, $disabled, $params);
3576
3577
			// Fix after, for disabled code mainly.
3578
			foreach ($data as $k => $d)
3579
				$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d)));
3580
3581
			$open_tags[] = $tag;
3582
3583
			// Replace them out, $1, $2, $3, $4, etc.
3584
			$code = $tag['before'];
3585
			foreach ($data as $k => $d)
3586
				$code = strtr($code, array('$' . ($k + 1) => trim($d)));
3587
			$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1);
3588
			$pos += strlen($code) - 1 + 2;
3589
		}
3590
		// A tag set to a value, parsed or not.
3591
		elseif ($tag['type'] == 'unparsed_equals' || $tag['type'] == 'parsed_equals')
3592
		{
3593
			// The value may be quoted for some tags - check.
3594
			if (isset($tag['quoted']))
3595
			{
3596
				$quoted = substr($message, $pos1, 6) == '&quot;';
3597
				if ($tag['quoted'] != 'optional' && !$quoted)
3598
					continue;
3599
3600
				if ($quoted)
3601
					$pos1 += 6;
3602
			}
3603
			else
3604
				$quoted = false;
3605
3606
			if ($quoted)
3607
			{
3608
				$end_of_value = strpos($message, '&quot;]', $pos1);
3609
				$nested_tag = strpos($message, '=&quot;', $pos1);
3610
				// Check so this is not just an quoted url ending with a =
3611
				if ($nested_tag && substr($message, $nested_tag, 8) == '=&quot;]')
3612
					$nested_tag = false;
3613
				if ($nested_tag && $nested_tag < $end_of_value)
0 ignored issues
show
Bug Best Practice introduced by
The expression $nested_tag of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
3614
					// Nested tag with quoted value detected, use next end tag
3615
					$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...
3616
			}
3617
3618
			$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...
3619
			if ($pos2 === false)
3620
				continue;
3621
3622
			$data = substr($message, $pos1, $pos2 - $pos1);
3623
3624
			// Validation for my parking, please!
3625
			if (isset($tag['validate']))
3626
				$tag['validate']($tag, $data, $disabled, $params);
3627
3628
			// For parsed content, we must recurse to avoid security problems.
3629
			if ($tag['type'] != 'unparsed_equals')
3630
				$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array());
3631
3632
			$tag['after'] = strtr($tag['after'], array('$1' => $data));
3633
3634
			$open_tags[] = $tag;
3635
3636
			$code = strtr($tag['before'], array('$1' => $data));
3637
			$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...
3638
			$pos += strlen($code) - 1 + 2;
3639
		}
3640
3641
		// If this is block level, eat any breaks after it.
3642
		if (!empty($tag['block_level']) && substr($message, $pos + 1, 4) == '<br>')
3643
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 5);
3644
3645
		// Are we trimming outside this tag?
3646
		if (!empty($tag['trim']) && $tag['trim'] != 'outside' && preg_match('~(<br>|&nbsp;|\s)*~', substr($message, $pos + 1), $matches) != 0)
3647
			$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0]));
3648
	}
3649
3650
	// Close any remaining tags.
3651
	while ($tag = array_pop($open_tags))
3652
		$message .= "\n" . $tag['after'] . "\n";
3653
3654
	// Parse the smileys within the parts where it can be done safely.
3655
	if ($smileys === true)
3656
	{
3657
		$message_parts = explode("\n", $message);
3658
		for ($i = 0, $n = count($message_parts); $i < $n; $i += 2)
3659
			parsesmileys($message_parts[$i]);
3660
3661
		$message = implode('', $message_parts);
3662
	}
3663
3664
	// No smileys, just get rid of the markers.
3665
	else
3666
		$message = strtr($message, array("\n" => ''));
3667
3668
	if ($message !== '' && $message[0] === ' ')
3669
		$message = '&nbsp;' . substr($message, 1);
3670
3671
	// Cleanup whitespace.
3672
	$message = strtr($message, array('  ' => ' &nbsp;', "\r" => '', "\n" => '<br>', '<br> ' => '<br>&nbsp;', '&#13;' => "\n"));
3673
3674
	// Allow mods access to what parse_bbc created
3675
	call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags));
3676
3677
	// Cache the output if it took some time...
3678
	if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05)
3679
		cache_put_data($cache_key, $message, 240);
3680
3681
	// If this was a force parse revert if needed.
3682
	if (!empty($parse_tags))
3683
	{
3684
		$alltags_regex = empty($real_alltags_regex) ? '' : $real_alltags_regex;
3685
		unset($real_alltags_regex);
3686
	}
3687
	elseif (!empty($bbc_codes))
3688
		$bbc_lang_locales[$txt['lang_locale']] = $bbc_codes;
3689
3690
	return $message;
3691
}
3692
3693
/**
3694
 * Parse smileys in the passed message.
3695
 *
3696
 * The smiley parsing function which makes pretty faces appear :).
3697
 * If custom smiley sets are turned off by smiley_enable, the default set of smileys will be used.
3698
 * These are specifically not parsed in code tags [url=mailto:[email protected]]
3699
 * Caches the smileys from the database or array in memory.
3700
 * Doesn't return anything, but rather modifies message directly.
3701
 *
3702
 * @param string &$message The message to parse smileys in
3703
 */
3704
function parsesmileys(&$message)
3705
{
3706
	global $modSettings, $txt, $user_info, $context, $smcFunc;
3707
	static $smileyPregSearch = null, $smileyPregReplacements = array();
3708
3709
	// No smiley set at all?!
3710
	if ($user_info['smiley_set'] == 'none' || trim($message) == '')
3711
		return;
3712
3713
	// Maybe a mod wants to implement an alternative method (e.g. emojis instead of images)
3714
	call_integration_hook('integrate_smileys', array(&$smileyPregSearch, &$smileyPregReplacements));
3715
3716
	// If smileyPregSearch hasn't been set, do it now.
3717
	if (empty($smileyPregSearch))
3718
	{
3719
		// Cache for longer when customized smiley codes aren't enabled
3720
		$cache_time = empty($modSettings['smiley_enable']) ? 7200 : 480;
3721
3722
		// Load the smileys in reverse order by length so they don't get parsed incorrectly.
3723
		if (($temp = cache_get_data('parsing_smileys_' . $user_info['smiley_set'], $cache_time)) == null)
3724
		{
3725
			$result = $smcFunc['db_query']('', '
3726
				SELECT s.code, f.filename, s.description
3727
				FROM {db_prefix}smileys AS s
3728
					JOIN {db_prefix}smiley_files AS f ON (s.id_smiley = f.id_smiley)
3729
				WHERE f.smiley_set = {string:smiley_set}' . (empty($modSettings['smiley_enable']) ? '
3730
					AND s.code IN ({array_string:default_codes})' : '') . '
3731
				ORDER BY LENGTH(s.code) DESC',
3732
				array(
3733
					'default_codes' => array('>:D', ':D', '::)', '>:(', ':))', ':)', ';)', ';D', ':(', ':o', '8)', ':P', '???', ':-[', ':-X', ':-*', ':\'(', ':-\\', '^-^', 'O0', 'C:-)', 'O:-)'),
3734
					'smiley_set' => $user_info['smiley_set'],
3735
				)
3736
			);
3737
			$smileysfrom = array();
3738
			$smileysto = array();
3739
			$smileysdescs = array();
3740
			while ($row = $smcFunc['db_fetch_assoc']($result))
3741
			{
3742
				$smileysfrom[] = $row['code'];
3743
				$smileysto[] = $smcFunc['htmlspecialchars']($row['filename']);
3744
				$smileysdescs[] = !empty($txt['icon_' . strtolower($row['description'])]) ? $txt['icon_' . strtolower($row['description'])] : $row['description'];
3745
			}
3746
			$smcFunc['db_free_result']($result);
3747
3748
			cache_put_data('parsing_smileys_' . $user_info['smiley_set'], array($smileysfrom, $smileysto, $smileysdescs), $cache_time);
3749
		}
3750
		else
3751
			list ($smileysfrom, $smileysto, $smileysdescs) = $temp;
3752
3753
		// The non-breaking-space is a complex thing...
3754
		$non_breaking_space = $context['utf8'] ? '\x{A0}' : '\xA0';
3755
3756
		// This smiley regex makes sure it doesn't parse smileys within code tags (so [url=mailto:[email protected]] doesn't parse the :D smiley)
3757
		$smileyPregReplacements = array();
3758
		$searchParts = array();
3759
		$smileys_path = $smcFunc['htmlspecialchars']($modSettings['smileys_url'] . '/' . $user_info['smiley_set'] . '/');
3760
3761
		for ($i = 0, $n = count($smileysfrom); $i < $n; $i++)
3762
		{
3763
			$specialChars = $smcFunc['htmlspecialchars']($smileysfrom[$i], ENT_QUOTES);
3764
			$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">';
3765
3766
			$smileyPregReplacements[$smileysfrom[$i]] = $smileyCode;
3767
3768
			$searchParts[] = $smileysfrom[$i];
3769
			if ($smileysfrom[$i] != $specialChars)
3770
			{
3771
				$smileyPregReplacements[$specialChars] = $smileyCode;
3772
				$searchParts[] = $specialChars;
3773
3774
				// Some 2.0 hex htmlchars are in there as 3 digits; allow for finding leading 0 or not
3775
				$specialChars2 = preg_replace('/&#(\d{2});/', '&#0$1;', $specialChars);
3776
				if ($specialChars2 != $specialChars)
3777
				{
3778
					$smileyPregReplacements[$specialChars2] = $smileyCode;
3779
					$searchParts[] = $specialChars2;
3780
				}
3781
			}
3782
		}
3783
3784
		$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

3784
		$smileyPregSearch = '~(?<=[>:\?\.\s' . $non_breaking_space . '[\]()*\\\;]|(?<![a-zA-Z0-9])\(|^)(' . /** @scrutinizer ignore-type */ build_regex($searchParts, '~') . ')(?=[^[:alpha:]0-9]|$)~' . ($context['utf8'] ? 'u' : '');
Loading history...
3785
	}
3786
3787
	// If there are no smileys defined, no need to replace anything
3788
	if (empty($smileyPregReplacements))
3789
		return;
3790
3791
	// Replace away!
3792
	$message = preg_replace_callback(
3793
		$smileyPregSearch,
3794
		function($matches) use ($smileyPregReplacements)
3795
		{
3796
			return $smileyPregReplacements[$matches[1]];
3797
		},
3798
		$message
3799
	);
3800
}
3801
3802
/**
3803
 * Highlight any code.
3804
 *
3805
 * Uses PHP's highlight_string() to highlight PHP syntax
3806
 * does special handling to keep the tabs in the code available.
3807
 * used to parse PHP code from inside [code] and [php] tags.
3808
 *
3809
 * @param string $code The code
3810
 * @return string The code with highlighted HTML.
3811
 */
3812
function highlight_php_code($code)
3813
{
3814
	// Remove special characters.
3815
	$code = un_htmlspecialchars(strtr($code, array('<br />' => "\n", '<br>' => "\n", "\t" => 'SMF_TAB();', '&#91;' => '[')));
3816
3817
	$oldlevel = error_reporting(0);
3818
3819
	$buffer = str_replace(array("\n", "\r"), '', @highlight_string($code, true));
3820
3821
	error_reporting($oldlevel);
3822
3823
	// Yes, I know this is kludging it, but this is the best way to preserve tabs from PHP :P.
3824
	$buffer = preg_replace('~SMF_TAB(?:</(?:font|span)><(?:font color|span style)="[^"]*?">)?\\(\\);~', '<pre style="display: inline;">' . "\t" . '</pre>', $buffer);
3825
3826
	return strtr($buffer, array('\'' => '&#039;', '<code>' => '', '</code>' => ''));
3827
}
3828
3829
/**
3830
 * Gets the appropriate URL to use for images (or whatever) when using SSL
3831
 *
3832
 * The returned URL may or may not be a proxied URL, depending on the situation.
3833
 * Mods can implement alternative proxies using the 'integrate_proxy' hook.
3834
 *
3835
 * @param string $url The original URL of the requested resource
3836
 * @return string The URL to use
3837
 */
3838
function get_proxied_url($url)
3839
{
3840
	global $boardurl, $image_proxy_enabled, $image_proxy_secret, $user_info;
3841
3842
	// Only use the proxy if enabled, and never for robots
3843
	if (empty($image_proxy_enabled) || !empty($user_info['possibly_robot']))
3844
		return $url;
3845
3846
	$parsedurl = parse_url($url);
3847
3848
	// Don't bother with HTTPS URLs, schemeless URLs, or obviously invalid URLs
3849
	if (empty($parsedurl['scheme']) || empty($parsedurl['host']) || empty($parsedurl['path']) || $parsedurl['scheme'] === 'https')
3850
		return $url;
3851
3852
	// We don't need to proxy our own resources
3853
	if ($parsedurl['host'] === parse_url($boardurl, PHP_URL_HOST))
3854
		return strtr($url, array('http://' => 'https://'));
3855
3856
	// By default, use SMF's own image proxy script
3857
	$proxied_url = strtr($boardurl, array('http://' => 'https://')) . '/proxy.php?request=' . urlencode($url) . '&hash=' . hash_hmac('sha1', $url, $image_proxy_secret);
3858
3859
	// Allow mods to easily implement an alternative proxy
3860
	// MOD AUTHORS: To add settings UI for your proxy, use the integrate_general_settings hook.
3861
	call_integration_hook('integrate_proxy', array($url, &$proxied_url));
3862
3863
	return $proxied_url;
3864
}
3865
3866
/**
3867
 * Make sure the browser doesn't come back and repost the form data.
3868
 * Should be used whenever anything is posted.
3869
 *
3870
 * @param string $setLocation The URL to redirect them to
3871
 * @param bool $refresh Whether to use a meta refresh instead
3872
 * @param bool $permanent Whether to send a 301 Moved Permanently instead of a 302 Moved Temporarily
3873
 */
3874
function redirectexit($setLocation = '', $refresh = false, $permanent = false)
3875
{
3876
	global $scripturl, $context, $modSettings, $db_show_debug, $db_cache;
3877
3878
	// In case we have mail to send, better do that - as obExit doesn't always quite make it...
3879
	if (!empty($context['flush_mail']))
3880
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3881
		AddMailQueue(true);
3882
3883
	$add = preg_match('~^(ftp|http)[s]?://~', $setLocation) == 0 && substr($setLocation, 0, 6) != 'about:';
3884
3885
	if ($add)
3886
		$setLocation = $scripturl . ($setLocation != '' ? '?' . $setLocation : '');
3887
3888
	// Put the session ID in.
3889
	if (defined('SID') && SID != '')
3890
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '(?!\?' . preg_quote(SID, '/') . ')\\??/', $scripturl . '?' . SID . ';', $setLocation);
3891
	// Keep that debug in their for template debugging!
3892
	elseif (isset($_GET['debug']))
3893
		$setLocation = preg_replace('/^' . preg_quote($scripturl, '/') . '\\??/', $scripturl . '?debug;', $setLocation);
3894
3895
	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'])))
3896
	{
3897
		if (defined('SID') && SID != '')
3898
			$setLocation = preg_replace_callback(
3899
				'~^' . preg_quote($scripturl, '~') . '\?(?:' . SID . '(?:;|&|&amp;))((?:board|topic)=[^#]+?)(#[^"]*?)?$~',
3900
				function($m) use ($scripturl)
3901
				{
3902
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html?' . SID . (isset($m[2]) ? "$m[2]" : "");
3903
				},
3904
				$setLocation
3905
			);
3906
		else
3907
			$setLocation = preg_replace_callback(
3908
				'~^' . preg_quote($scripturl, '~') . '\?((?:board|topic)=[^#"]+?)(#[^"]*?)?$~',
3909
				function($m) use ($scripturl)
3910
				{
3911
					return $scripturl . '/' . strtr("$m[1]", '&;=', '//,') . '.html' . (isset($m[2]) ? "$m[2]" : "");
3912
				},
3913
				$setLocation
3914
			);
3915
	}
3916
3917
	// Maybe integrations want to change where we are heading?
3918
	call_integration_hook('integrate_redirect', array(&$setLocation, &$refresh, &$permanent));
3919
3920
	// Set the header.
3921
	header('location: ' . str_replace(' ', '%20', $setLocation), true, $permanent ? 301 : 302);
3922
3923
	// Debugging.
3924
	if (isset($db_show_debug) && $db_show_debug === true)
3925
		$_SESSION['debug_redirect'] = $db_cache;
3926
3927
	obExit(false);
3928
}
3929
3930
/**
3931
 * Ends execution.  Takes care of template loading and remembering the previous URL.
3932
 *
3933
 * @param bool $header Whether to do the header
3934
 * @param bool $do_footer Whether to do the footer
3935
 * @param bool $from_index Whether we're coming from the board index
3936
 * @param bool $from_fatal_error Whether we're coming from a fatal error
3937
 */
3938
function obExit($header = null, $do_footer = null, $from_index = false, $from_fatal_error = false)
3939
{
3940
	global $context, $settings, $modSettings, $txt, $smcFunc, $should_log;
3941
	static $header_done = false, $footer_done = false, $level = 0, $has_fatal_error = false;
3942
3943
	// Attempt to prevent a recursive loop.
3944
	++$level;
3945
	if ($level > 1 && !$from_fatal_error && !$has_fatal_error)
3946
		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...
3947
	if ($from_fatal_error)
3948
		$has_fatal_error = true;
3949
3950
	// Clear out the stat cache.
3951
	if (function_exists('trackStats'))
3952
		trackStats();
3953
3954
	// If we have mail to send, send it.
3955
	if (function_exists('AddMailQueue') && !empty($context['flush_mail']))
3956
		// @todo this relies on 'flush_mail' being only set in AddMailQueue itself... :\
3957
		AddMailQueue(true);
3958
3959
	$do_header = $header === null ? !$header_done : $header;
3960
	if ($do_footer === null)
3961
		$do_footer = $do_header;
3962
3963
	// Has the template/header been done yet?
3964
	if ($do_header)
3965
	{
3966
		// Was the page title set last minute? Also update the HTML safe one.
3967
		if (!empty($context['page_title']) && empty($context['page_title_html_safe']))
3968
			$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
3969
3970
		// Start up the session URL fixer.
3971
		ob_start('ob_sessrewrite');
3972
3973
		if (!empty($settings['output_buffers']) && is_string($settings['output_buffers']))
3974
			$buffers = explode(',', $settings['output_buffers']);
3975
		elseif (!empty($settings['output_buffers']))
3976
			$buffers = $settings['output_buffers'];
3977
		else
3978
			$buffers = array();
3979
3980
		if (isset($modSettings['integrate_buffer']))
3981
			$buffers = array_merge(explode(',', $modSettings['integrate_buffer']), $buffers);
3982
3983
		if (!empty($buffers))
3984
			foreach ($buffers as $function)
3985
			{
3986
				$call = call_helper($function, true);
3987
3988
				// Is it valid?
3989
				if (!empty($call))
3990
					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

3990
					ob_start(/** @scrutinizer ignore-type */ $call);
Loading history...
3991
			}
3992
3993
		// Display the screen in the logical order.
3994
		template_header();
3995
		$header_done = true;
3996
	}
3997
	if ($do_footer)
3998
	{
3999
		loadSubTemplate(isset($context['sub_template']) ? $context['sub_template'] : 'main');
4000
4001
		// Anything special to put out?
4002
		if (!empty($context['insert_after_template']) && !isset($_REQUEST['xml']))
4003
			echo $context['insert_after_template'];
4004
4005
		// Just so we don't get caught in an endless loop of errors from the footer...
4006
		if (!$footer_done)
4007
		{
4008
			$footer_done = true;
4009
			template_footer();
4010
4011
			// (since this is just debugging... it's okay that it's after </html>.)
4012
			if (!isset($_REQUEST['xml']))
4013
				displayDebug();
4014
		}
4015
	}
4016
4017
	// Remember this URL in case someone doesn't like sending HTTP_REFERER.
4018
	if ($should_log)
4019
		$_SESSION['old_url'] = $_SERVER['REQUEST_URL'];
4020
4021
	// For session check verification.... don't switch browsers...
4022
	$_SESSION['USER_AGENT'] = empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT'];
4023
4024
	// Hand off the output to the portal, etc. we're integrated with.
4025
	call_integration_hook('integrate_exit', array($do_footer));
4026
4027
	// Don't exit if we're coming from index.php; that will pass through normally.
4028
	if (!$from_index)
4029
		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...
4030
}
4031
4032
/**
4033
 * Get the size of a specified image with better error handling.
4034
 *
4035
 * @todo see if it's better in Subs-Graphics, but one step at the time.
4036
 * Uses getimagesize() to determine the size of a file.
4037
 * Attempts to connect to the server first so it won't time out.
4038
 *
4039
 * @param string $url The URL of the image
4040
 * @return array|false The image size as array (width, height), or false on failure
4041
 */
4042
function url_image_size($url)
4043
{
4044
	global $sourcedir;
4045
4046
	// Make sure it is a proper URL.
4047
	$url = str_replace(' ', '%20', $url);
4048
4049
	// Can we pull this from the cache... please please?
4050
	if (($temp = cache_get_data('url_image_size-' . md5($url), 240)) !== null)
4051
		return $temp;
4052
	$t = microtime(true);
4053
4054
	// Get the host to pester...
4055
	preg_match('~^\w+://(.+?)/(.*)$~', $url, $match);
4056
4057
	// Can't figure it out, just try the image size.
4058
	if ($url == '' || $url == 'http://' || $url == 'https://')
4059
	{
4060
		return false;
4061
	}
4062
	elseif (!isset($match[1]))
4063
	{
4064
		$size = @getimagesize($url);
4065
	}
4066
	else
4067
	{
4068
		// Try to connect to the server... give it half a second.
4069
		$temp = 0;
4070
		$fp = @fsockopen($match[1], 80, $temp, $temp, 0.5);
4071
4072
		// Successful?  Continue...
4073
		if ($fp != false)
4074
		{
4075
			// Send the HEAD request (since we don't have to worry about chunked, HTTP/1.1 is fine here.)
4076
			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");
4077
4078
			// Read in the HTTP/1.1 or whatever.
4079
			$test = substr(fgets($fp, 11), -1);
4080
			fclose($fp);
4081
4082
			// See if it returned a 404/403 or something.
4083
			if ($test < 4)
4084
			{
4085
				$size = @getimagesize($url);
4086
4087
				// This probably means allow_url_fopen is off, let's try GD.
4088
				if ($size === false && function_exists('imagecreatefromstring'))
4089
				{
4090
					// It's going to hate us for doing this, but another request...
4091
					$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

4091
					$image = @imagecreatefromstring(/** @scrutinizer ignore-type */ fetch_web_data($url));
Loading history...
4092
					if ($image !== false)
4093
					{
4094
						$size = array(imagesx($image), imagesy($image));
4095
						imagedestroy($image);
4096
					}
4097
				}
4098
			}
4099
		}
4100
	}
4101
4102
	// If we didn't get it, we failed.
4103
	if (!isset($size))
4104
		$size = false;
4105
4106
	// If this took a long time, we may never have to do it again, but then again we might...
4107
	if (microtime(true) - $t > 0.8)
4108
		cache_put_data('url_image_size-' . md5($url), $size, 240);
4109
4110
	// Didn't work.
4111
	return $size;
4112
}
4113
4114
/**
4115
 * Sets up the basic theme context stuff.
4116
 *
4117
 * @param bool $forceload Whether to load the theme even if it's already loaded
4118
 */
4119
function setupThemeContext($forceload = false)
4120
{
4121
	global $modSettings, $user_info, $scripturl, $context, $settings, $options, $txt, $maintenance;
4122
	global $smcFunc;
4123
	static $loaded = false;
4124
4125
	// Under SSI this function can be called more then once.  That can cause some problems.
4126
	//   So only run the function once unless we are forced to run it again.
4127
	if ($loaded && !$forceload)
4128
		return;
4129
4130
	$loaded = true;
4131
4132
	$context['in_maintenance'] = !empty($maintenance);
4133
	$context['current_time'] = timeformat(time(), false);
4134
	$context['current_action'] = isset($_GET['action']) ? $smcFunc['htmlspecialchars']($_GET['action']) : '';
4135
	$context['random_news_line'] = array();
4136
4137
	// Get some news...
4138
	$context['news_lines'] = array_filter(explode("\n", str_replace("\r", '', trim(addslashes($modSettings['news'])))));
4139
	for ($i = 0, $n = count($context['news_lines']); $i < $n; $i++)
4140
	{
4141
		if (trim($context['news_lines'][$i]) == '')
4142
			continue;
4143
4144
		// Clean it up for presentation ;).
4145
		$context['news_lines'][$i] = parse_bbc(stripslashes(trim($context['news_lines'][$i])), true, 'news' . $i);
4146
	}
4147
4148
	if (!empty($context['news_lines']) && (!empty($modSettings['allow_guestAccess']) || $context['user']['is_logged']))
4149
		$context['random_news_line'] = $context['news_lines'][mt_rand(0, count($context['news_lines']) - 1)];
4150
4151
	if (!$user_info['is_guest'])
4152
	{
4153
		$context['user']['messages'] = &$user_info['messages'];
4154
		$context['user']['unread_messages'] = &$user_info['unread_messages'];
4155
		$context['user']['alerts'] = &$user_info['alerts'];
4156
4157
		// Personal message popup...
4158
		if ($user_info['unread_messages'] > (isset($_SESSION['unread_messages']) ? $_SESSION['unread_messages'] : 0))
4159
			$context['user']['popup_messages'] = true;
4160
		else
4161
			$context['user']['popup_messages'] = false;
4162
		$_SESSION['unread_messages'] = $user_info['unread_messages'];
4163
4164
		if (allowedTo('moderate_forum'))
4165
			$context['unapproved_members'] = !empty($modSettings['unapprovedMembers']) ? $modSettings['unapprovedMembers'] : 0;
4166
4167
		$context['user']['avatar'] = set_avatar_data(array(
4168
			'filename' => $user_info['avatar']['filename'],
4169
			'avatar' => $user_info['avatar']['url'],
4170
			'email' => $user_info['email'],
4171
		));
4172
4173
		// Figure out how long they've been logged in.
4174
		$context['user']['total_time_logged_in'] = array(
4175
			'days' => floor($user_info['total_time_logged_in'] / 86400),
4176
			'hours' => floor(($user_info['total_time_logged_in'] % 86400) / 3600),
4177
			'minutes' => floor(($user_info['total_time_logged_in'] % 3600) / 60)
4178
		);
4179
	}
4180
	else
4181
	{
4182
		$context['user']['messages'] = 0;
4183
		$context['user']['unread_messages'] = 0;
4184
		$context['user']['avatar'] = array();
4185
		$context['user']['total_time_logged_in'] = array('days' => 0, 'hours' => 0, 'minutes' => 0);
4186
		$context['user']['popup_messages'] = false;
4187
4188
		// If we've upgraded recently, go easy on the passwords.
4189
		if (!empty($modSettings['disableHashTime']) && ($modSettings['disableHashTime'] == 1 || time() < $modSettings['disableHashTime']))
4190
			$context['disable_login_hashing'] = true;
4191
	}
4192
4193
	// Setup the main menu items.
4194
	setupMenuContext();
4195
4196
	// This is here because old index templates might still use it.
4197
	$context['show_news'] = !empty($settings['enable_news']);
4198
4199
	// This is done to allow theme authors to customize it as they want.
4200
	$context['show_pm_popup'] = $context['user']['popup_messages'] && !empty($options['popup_messages']) && (!isset($_REQUEST['action']) || $_REQUEST['action'] != 'pm');
4201
4202
	// 2.1+: Add the PM popup here instead. Theme authors can still override it simply by editing/removing the 'fPmPopup' in the array.
4203
	if ($context['show_pm_popup'])
4204
		addInlineJavaScript('
4205
		jQuery(document).ready(function($) {
4206
			new smc_Popup({
4207
				heading: ' . JavaScriptEscape($txt['show_personal_messages_heading']) . ',
4208
				content: ' . JavaScriptEscape(sprintf($txt['show_personal_messages'], $context['user']['unread_messages'], $scripturl . '?action=pm')) . ',
4209
				icon_class: \'main_icons mail_new\'
4210
			});
4211
		});');
4212
4213
	// Add a generic "Are you sure?" confirmation message.
4214
	addInlineJavaScript('
4215
	var smf_you_sure =' . JavaScriptEscape($txt['quickmod_confirm']) . ';');
4216
4217
	// Now add the capping code for avatars.
4218
	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')
4219
		addInlineCss('
4220
	img.avatar { max-width: ' . $modSettings['avatar_max_width_external'] . 'px !important; max-height: ' . $modSettings['avatar_max_height_external'] . 'px !important; }');
4221
4222
	// Add max image limits
4223
	if (!empty($modSettings['max_image_width']))
4224
		addInlineCss('
4225
	.postarea .bbc_img, .list_posts .bbc_img, .post .inner .bbc_img, form#reported_posts .bbc_img, #preview_body .bbc_img { max-width: min(100%,' . $modSettings['max_image_width'] . 'px); }');
4226
4227
	if (!empty($modSettings['max_image_height']))
4228
		addInlineCss('
4229
	.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; }');
4230
4231
	// This looks weird, but it's because BoardIndex.php references the variable.
4232
	$context['common_stats']['latest_member'] = array(
4233
		'id' => $modSettings['latestMember'],
4234
		'name' => $modSettings['latestRealName'],
4235
		'href' => $scripturl . '?action=profile;u=' . $modSettings['latestMember'],
4236
		'link' => '<a href="' . $scripturl . '?action=profile;u=' . $modSettings['latestMember'] . '">' . $modSettings['latestRealName'] . '</a>',
4237
	);
4238
	$context['common_stats'] = array(
4239
		'total_posts' => comma_format($modSettings['totalMessages']),
4240
		'total_topics' => comma_format($modSettings['totalTopics']),
4241
		'total_members' => comma_format($modSettings['totalMembers']),
4242
		'latest_member' => $context['common_stats']['latest_member'],
4243
	);
4244
	$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']);
4245
4246
	if (empty($settings['theme_version']))
4247
		addJavaScriptVar('smf_scripturl', $scripturl);
4248
4249
	if (!isset($context['page_title']))
4250
		$context['page_title'] = '';
4251
4252
	// Set some specific vars.
4253
	$context['page_title_html_safe'] = $smcFunc['htmlspecialchars'](html_entity_decode($context['page_title'])) . (!empty($context['current_page']) ? ' - ' . $txt['page'] . ' ' . ($context['current_page'] + 1) : '');
4254
	$context['meta_keywords'] = !empty($modSettings['meta_keywords']) ? $smcFunc['htmlspecialchars']($modSettings['meta_keywords']) : '';
4255
4256
	// Content related meta tags, including Open Graph
4257
	$context['meta_tags'][] = array('property' => 'og:site_name', 'content' => $context['forum_name']);
4258
	$context['meta_tags'][] = array('property' => 'og:title', 'content' => $context['page_title_html_safe']);
4259
4260
	if (!empty($context['meta_keywords']))
4261
		$context['meta_tags'][] = array('name' => 'keywords', 'content' => $context['meta_keywords']);
4262
4263
	if (!empty($context['canonical_url']))
4264
		$context['meta_tags'][] = array('property' => 'og:url', 'content' => $context['canonical_url']);
4265
4266
	if (!empty($settings['og_image']))
4267
		$context['meta_tags'][] = array('property' => 'og:image', 'content' => $settings['og_image']);
4268
4269
	if (!empty($context['meta_description']))
4270
	{
4271
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['meta_description']);
4272
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['meta_description']);
4273
	}
4274
	else
4275
	{
4276
		$context['meta_tags'][] = array('property' => 'og:description', 'content' => $context['page_title_html_safe']);
4277
		$context['meta_tags'][] = array('name' => 'description', 'content' => $context['page_title_html_safe']);
4278
	}
4279
4280
	call_integration_hook('integrate_theme_context');
4281
}
4282
4283
/**
4284
 * Helper function to set the system memory to a needed value
4285
 * - If the needed memory is greater than current, will attempt to get more
4286
 * - if in_use is set to true, will also try to take the current memory usage in to account
4287
 *
4288
 * @param string $needed The amount of memory to request, if needed, like 256M
4289
 * @param bool $in_use Set to true to account for current memory usage of the script
4290
 * @return boolean True if we have at least the needed memory
4291
 */
4292
function setMemoryLimit($needed, $in_use = false)
4293
{
4294
	// everything in bytes
4295
	$memory_current = memoryReturnBytes(ini_get('memory_limit'));
4296
	$memory_needed = memoryReturnBytes($needed);
4297
4298
	// should we account for how much is currently being used?
4299
	if ($in_use)
4300
		$memory_needed += function_exists('memory_get_usage') ? memory_get_usage() : (2 * 1048576);
4301
4302
	// if more is needed, request it
4303
	if ($memory_current < $memory_needed)
4304
	{
4305
		@ini_set('memory_limit', ceil($memory_needed / 1048576) . 'M');
4306
		$memory_current = memoryReturnBytes(ini_get('memory_limit'));
4307
	}
4308
4309
	$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

4309
	$memory_current = max($memory_current, memoryReturnBytes(/** @scrutinizer ignore-type */ get_cfg_var('memory_limit')));
Loading history...
4310
4311
	// return success or not
4312
	return (bool) ($memory_current >= $memory_needed);
4313
}
4314
4315
/**
4316
 * Helper function to convert memory string settings to bytes
4317
 *
4318
 * @param string $val The byte string, like 256M or 1G
4319
 * @return integer The string converted to a proper integer in bytes
4320
 */
4321
function memoryReturnBytes($val)
4322
{
4323
	if (is_integer($val))
0 ignored issues
show
introduced by
The condition is_integer($val) is always false.
Loading history...
4324
		return $val;
4325
4326
	// Separate the number from the designator
4327
	$val = trim($val);
4328
	$num = intval(substr($val, 0, strlen($val) - 1));
4329
	$last = strtolower(substr($val, -1));
4330
4331
	// convert to bytes
4332
	switch ($last)
4333
	{
4334
		case 'g':
4335
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
4336
		case 'm':
4337
			$num *= 1024;
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment if this fall-through is intended.
Loading history...
4338
		case 'k':
4339
			$num *= 1024;
4340
	}
4341
	return $num;
4342
}
4343
4344
/**
4345
 * The header template
4346
 */
4347
function template_header()
4348
{
4349
	global $txt, $modSettings, $context, $user_info, $boarddir, $cachedir, $cache_enable, $language;
4350
4351
	setupThemeContext();
4352
4353
	// Print stuff to prevent caching of pages (except on attachment errors, etc.)
4354
	if (empty($context['no_last_modified']))
4355
	{
4356
		header('expires: Mon, 26 Jul 1997 05:00:00 GMT');
4357
		header('last-modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
4358
4359
		// Are we debugging the template/html content?
4360
		if (!isset($_REQUEST['xml']) && isset($_GET['debug']) && !isBrowser('ie'))
4361
			header('content-type: application/xhtml+xml');
4362
		elseif (!isset($_REQUEST['xml']))
4363
			header('content-type: text/html; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
4364
	}
4365
4366
	header('content-type: text/' . (isset($_REQUEST['xml']) ? 'xml' : 'html') . '; charset=' . (empty($context['character_set']) ? 'ISO-8859-1' : $context['character_set']));
4367
4368
	// We need to splice this in after the body layer, or after the main layer for older stuff.
4369
	if ($context['in_maintenance'] && $context['user']['is_admin'])
4370
	{
4371
		$position = array_search('body', $context['template_layers']);
4372
		if ($position === false)
4373
			$position = array_search('main', $context['template_layers']);
4374
4375
		if ($position !== false)
4376
		{
4377
			$before = array_slice($context['template_layers'], 0, $position + 1);
4378
			$after = array_slice($context['template_layers'], $position + 1);
4379
			$context['template_layers'] = array_merge($before, array('maint_warning'), $after);
4380
		}
4381
	}
4382
4383
	$checked_securityFiles = false;
4384
	$showed_banned = false;
4385
	foreach ($context['template_layers'] as $layer)
4386
	{
4387
		loadSubTemplate($layer . '_above', true);
4388
4389
		// May seem contrived, but this is done in case the body and main layer aren't there...
4390
		if (in_array($layer, array('body', 'main')) && allowedTo('admin_forum') && !$user_info['is_guest'] && !$checked_securityFiles)
4391
		{
4392
			$checked_securityFiles = true;
4393
4394
			$securityFiles = array('install.php', 'upgrade.php', 'convert.php', 'repair_paths.php', 'repair_settings.php', 'Settings.php~', 'Settings_bak.php~');
4395
4396
			// Add your own files.
4397
			call_integration_hook('integrate_security_files', array(&$securityFiles));
4398
4399
			foreach ($securityFiles as $i => $securityFile)
4400
			{
4401
				if (!file_exists($boarddir . '/' . $securityFile))
4402
					unset($securityFiles[$i]);
4403
			}
4404
4405
			// We are already checking so many files...just few more doesn't make any difference! :P
4406
			if (!empty($modSettings['currentAttachmentUploadDir']))
4407
				$path = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
4408
4409
			else
4410
				$path = $modSettings['attachmentUploadDir'];
4411
4412
			secureDirectory($path, true);
4413
			secureDirectory($cachedir);
4414
4415
			// If agreement is enabled, at least the english version shall exist
4416
			if (!empty($modSettings['requireAgreement']))
4417
				$agreement = !file_exists($boarddir . '/agreement.txt');
4418
4419
			// If privacy policy is enabled, at least the default language version shall exist
4420
			if (!empty($modSettings['requirePolicyAgreement']))
4421
				$policy_agreement = empty($modSettings['policy_' . $language]);
4422
4423
			if (!empty($securityFiles) ||
4424
				(!empty($cache_enable) && !is_writable($cachedir)) ||
4425
				!empty($agreement) ||
4426
				!empty($policy_agreement) ||
4427
				!empty($context['auth_secret_missing']))
4428
			{
4429
				echo '
4430
		<div class="errorbox">
4431
			<p class="alert">!!</p>
4432
			<h3>', empty($securityFiles) && empty($context['auth_secret_missing']) ? $txt['generic_warning'] : $txt['security_risk'], '</h3>
4433
			<p>';
4434
4435
				foreach ($securityFiles as $securityFile)
4436
				{
4437
					echo '
4438
				', $txt['not_removed'], '<strong>', $securityFile, '</strong>!<br>';
4439
4440
					if ($securityFile == 'Settings.php~' || $securityFile == 'Settings_bak.php~')
4441
						echo '
4442
				', sprintf($txt['not_removed_extra'], $securityFile, substr($securityFile, 0, -1)), '<br>';
4443
				}
4444
4445
				if (!empty($cache_enable) && !is_writable($cachedir))
4446
					echo '
4447
				<strong>', $txt['cache_writable'], '</strong><br>';
4448
4449
				if (!empty($agreement))
4450
					echo '
4451
				<strong>', $txt['agreement_missing'], '</strong><br>';
4452
4453
				if (!empty($policy_agreement))
4454
					echo '
4455
				<strong>', $txt['policy_agreement_missing'], '</strong><br>';
4456
4457
				if (!empty($context['auth_secret_missing']))
4458
					echo '
4459
				<strong>', $txt['auth_secret_missing'], '</strong><br>';
4460
4461
				echo '
4462
			</p>
4463
		</div>';
4464
			}
4465
		}
4466
		// If the user is banned from posting inform them of it.
4467
		elseif (in_array($layer, array('main', 'body')) && isset($_SESSION['ban']['cannot_post']) && !$showed_banned)
4468
		{
4469
			$showed_banned = true;
4470
			echo '
4471
				<div class="windowbg alert" style="margin: 2ex; padding: 2ex; border: 2px dashed red;">
4472
					', sprintf($txt['you_are_post_banned'], $user_info['is_guest'] ? $txt['guest_title'] : $user_info['name']);
4473
4474
			if (!empty($_SESSION['ban']['cannot_post']['reason']))
4475
				echo '
4476
					<div style="padding-left: 4ex; padding-top: 1ex;">', $_SESSION['ban']['cannot_post']['reason'], '</div>';
4477
4478
			if (!empty($_SESSION['ban']['expire_time']))
4479
				echo '
4480
					<div>', sprintf($txt['your_ban_expires'], timeformat($_SESSION['ban']['expire_time'], false)), '</div>';
4481
			else
4482
				echo '
4483
					<div>', $txt['your_ban_expires_never'], '</div>';
4484
4485
			echo '
4486
				</div>';
4487
		}
4488
	}
4489
}
4490
4491
/**
4492
 * Show the copyright.
4493
 */
4494
function theme_copyright()
4495
{
4496
	global $forum_copyright, $scripturl;
4497
4498
	// Don't display copyright for things like SSI.
4499
	if (SMF !== 1)
0 ignored issues
show
introduced by
The condition SMF !== 1 is always true.
Loading history...
4500
		return;
4501
4502
	// Put in the version...
4503
	printf($forum_copyright, SMF_FULL_VERSION, SMF_SOFTWARE_YEAR, $scripturl);
4504
}
4505
4506
/**
4507
 * The template footer
4508
 */
4509
function template_footer()
4510
{
4511
	global $context, $modSettings, $db_count;
4512
4513
	// Show the load time?  (only makes sense for the footer.)
4514
	$context['show_load_time'] = !empty($modSettings['timeLoadPageEnable']);
4515
	$context['load_time'] = round(microtime(true) - TIME_START, 3);
4516
	$context['load_queries'] = $db_count;
4517
4518
	if (!empty($context['template_layers']) && is_array($context['template_layers']))
4519
		foreach (array_reverse($context['template_layers']) as $layer)
4520
			loadSubTemplate($layer . '_below', true);
4521
}
4522
4523
/**
4524
 * Output the Javascript files
4525
 * 	- tabbing in this function is to make the HTML source look good and proper
4526
 *  - if deferred is set function will output all JS set to load at page end
4527
 *
4528
 * @param bool $do_deferred If true will only output the deferred JS (the stuff that goes right before the closing body tag)
4529
 */
4530
function template_javascript($do_deferred = false)
4531
{
4532
	global $context, $modSettings, $settings;
4533
4534
	// Use this hook to minify/optimize Javascript files and vars
4535
	call_integration_hook('integrate_pre_javascript_output', array(&$do_deferred));
4536
4537
	$toMinify = array(
4538
		'standard' => array(),
4539
		'defer' => array(),
4540
		'async' => array(),
4541
	);
4542
4543
	// Ouput the declared Javascript variables.
4544
	if (!empty($context['javascript_vars']) && !$do_deferred)
4545
	{
4546
		echo '
4547
	<script>';
4548
4549
		foreach ($context['javascript_vars'] as $key => $value)
4550
		{
4551
			if (!is_string($key) || is_numeric($key))
4552
				continue;
4553
4554
			if (!is_string($value) && !is_numeric($value))
4555
				$value = null;
4556
4557
			echo "\n\t\t", 'var ', $key, isset($value) ? ' = ' . $value : '', ';';
4558
		}
4559
4560
		echo '
4561
	</script>';
4562
	}
4563
4564
	// In the dark days before HTML5, deferred JS files needed to be loaded at the end of the body.
4565
	// Now we load them in the head and use 'async' and/or 'defer' attributes. Much better performance.
4566
	if (!$do_deferred)
4567
	{
4568
		// While we have JavaScript files to place in the template.
4569
		foreach ($context['javascript_files'] as $id => $js_file)
4570
		{
4571
			// Last minute call! allow theme authors to disable single files.
4572
			if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4573
				continue;
4574
4575
			// By default files don't get minimized unless the file explicitly says so!
4576
			if (!empty($js_file['options']['minimize']) && !empty($modSettings['minimize_files']))
4577
			{
4578
				if (!empty($js_file['options']['async']))
4579
					$toMinify['async'][] = $js_file;
4580
4581
				elseif (!empty($js_file['options']['defer']))
4582
					$toMinify['defer'][] = $js_file;
4583
4584
				else
4585
					$toMinify['standard'][] = $js_file;
4586
4587
				// Grab a random seed.
4588
				if (!isset($minSeed) && isset($js_file['options']['seed']))
4589
					$minSeed = $js_file['options']['seed'];
4590
			}
4591
4592
			else
4593
			{
4594
				echo '
4595
	<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' : '';
4596
4597
				if (!empty($js_file['options']['attributes']))
4598
					foreach ($js_file['options']['attributes'] as $key => $value)
4599
					{
4600
						if (is_bool($value))
4601
							echo !empty($value) ? ' ' . $key : '';
4602
4603
						else
4604
							echo ' ', $key, '="', $value, '"';
4605
					}
4606
4607
				echo '></script>';
4608
			}
4609
		}
4610
4611
		foreach ($toMinify as $js_files)
4612
		{
4613
			if (!empty($js_files))
4614
			{
4615
				$result = custMinify($js_files, 'js');
4616
4617
				$minSuccessful = array_keys($result) === array('smf_minified');
4618
4619
				foreach ($result as $minFile)
4620
					echo '
4621
	<script src="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '"', !empty($minFile['options']['async']) ? ' async' : '', !empty($minFile['options']['defer']) ? ' defer' : '', '></script>';
4622
			}
4623
		}
4624
	}
4625
4626
	// Inline JavaScript - Actually useful some times!
4627
	if (!empty($context['javascript_inline']))
4628
	{
4629
		if (!empty($context['javascript_inline']['defer']) && $do_deferred)
4630
		{
4631
			echo '
4632
<script>
4633
window.addEventListener("DOMContentLoaded", function() {';
4634
4635
			foreach ($context['javascript_inline']['defer'] as $js_code)
4636
				echo $js_code;
4637
4638
			echo '
4639
});
4640
</script>';
4641
		}
4642
4643
		if (!empty($context['javascript_inline']['standard']) && !$do_deferred)
4644
		{
4645
			echo '
4646
	<script>';
4647
4648
			foreach ($context['javascript_inline']['standard'] as $js_code)
4649
				echo $js_code;
4650
4651
			echo '
4652
	</script>';
4653
		}
4654
	}
4655
}
4656
4657
/**
4658
 * Output the CSS files
4659
 */
4660
function template_css()
4661
{
4662
	global $context, $db_show_debug, $boardurl, $settings, $modSettings;
4663
4664
	// Use this hook to minify/optimize CSS files
4665
	call_integration_hook('integrate_pre_css_output');
4666
4667
	$toMinify = array();
4668
	$normal = array();
4669
4670
	uasort(
4671
		$context['css_files'],
4672
		function ($a, $b)
4673
		{
4674
			return $a['options']['order_pos'] < $b['options']['order_pos'] ? -1 : ($a['options']['order_pos'] > $b['options']['order_pos'] ? 1 : 0);
4675
		}
4676
	);
4677
4678
	foreach ($context['css_files'] as $id => $file)
4679
	{
4680
		// Last minute call! allow theme authors to disable single files.
4681
		if (!empty($settings['disable_files']) && in_array($id, $settings['disable_files']))
4682
			continue;
4683
4684
		// Files are minimized unless they explicitly opt out.
4685
		if (!isset($file['options']['minimize']))
4686
			$file['options']['minimize'] = true;
4687
4688
		if (!empty($file['options']['minimize']) && !empty($modSettings['minimize_files']) && !isset($_REQUEST['normalcss']))
4689
		{
4690
			$toMinify[] = $file;
4691
4692
			// Grab a random seed.
4693
			if (!isset($minSeed) && isset($file['options']['seed']))
4694
				$minSeed = $file['options']['seed'];
4695
		}
4696
		else
4697
			$normal[] = array(
4698
				'url' => $file['fileUrl'] . (isset($file['options']['seed']) ? $file['options']['seed'] : ''),
4699
				'attributes' => !empty($file['options']['attributes']) ? $file['options']['attributes'] : array()
4700
			);
4701
	}
4702
4703
	if (!empty($toMinify))
4704
	{
4705
		$result = custMinify($toMinify, 'css');
4706
4707
		$minSuccessful = array_keys($result) === array('smf_minified');
4708
4709
		foreach ($result as $minFile)
4710
			echo '
4711
	<link rel="stylesheet" href="', $minFile['fileUrl'], $minSuccessful && isset($minSeed) ? $minSeed : '', '">';
4712
	}
4713
4714
	// Print the rest after the minified files.
4715
	if (!empty($normal))
4716
		foreach ($normal as $nf)
4717
		{
4718
			echo '
4719
	<link rel="stylesheet" href="', $nf['url'], '"';
4720
4721
			if (!empty($nf['attributes']))
4722
				foreach ($nf['attributes'] as $key => $value)
4723
				{
4724
					if (is_bool($value))
4725
						echo !empty($value) ? ' ' . $key : '';
4726
					else
4727
						echo ' ', $key, '="', $value, '"';
4728
				}
4729
4730
			echo '>';
4731
		}
4732
4733
	if ($db_show_debug === true)
4734
	{
4735
		// Try to keep only what's useful.
4736
		$repl = array($boardurl . '/Themes/' => '', $boardurl . '/' => '');
4737
		foreach ($context['css_files'] as $file)
4738
			$context['debug']['sheets'][] = strtr($file['fileName'], $repl);
4739
	}
4740
4741
	if (!empty($context['css_header']))
4742
	{
4743
		echo '
4744
	<style>';
4745
4746
		foreach ($context['css_header'] as $css)
4747
			echo $css . '
4748
	';
4749
4750
		echo '
4751
	</style>';
4752
	}
4753
}
4754
4755
/**
4756
 * Get an array of previously defined files and adds them to our main minified files.
4757
 * Sets a one day cache to avoid re-creating a file on every request.
4758
 *
4759
 * @param array $data The files to minify.
4760
 * @param string $type either css or js.
4761
 * @return array Info about the minified file, or about the original files if the minify process failed.
4762
 */
4763
function custMinify($data, $type)
4764
{
4765
	global $settings, $txt;
4766
4767
	$types = array('css', 'js');
4768
	$type = !empty($type) && in_array($type, $types) ? $type : false;
4769
	$data = is_array($data) ? $data : array();
0 ignored issues
show
introduced by
The condition is_array($data) is always true.
Loading history...
4770
4771
	if (empty($type) || empty($data))
4772
		return $data;
4773
4774
	// Different pages include different files, so we use a hash to label the different combinations
4775
	$hash = md5(implode(' ', array_map(
4776
		function($file)
4777
		{
4778
			return $file['filePath'] . '-' . $file['mtime'];
4779
		},
4780
		$data
4781
	)));
4782
4783
	// Is this a deferred or asynchronous JavaScript file?
4784
	$async = $type === 'js';
4785
	$defer = $type === 'js';
4786
	if ($type === 'js')
4787
	{
4788
		foreach ($data as $id => $file)
4789
		{
4790
			// A minified script should only be loaded asynchronously if all its components wanted to be.
4791
			if (empty($file['options']['async']))
4792
				$async = false;
4793
4794
			// A minified script should only be deferred if all its components wanted to be.
4795
			if (empty($file['options']['defer']))
4796
				$defer = false;
4797
		}
4798
	}
4799
4800
	// Did we already do this?
4801
	$minified_file = $settings['theme_dir'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/minified_' . $hash . '.' . $type;
4802
	$already_exists = file_exists($minified_file);
4803
4804
	// Already done?
4805
	if ($already_exists)
4806
	{
4807
		return array('smf_minified' => array(
4808
			'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4809
			'filePath' => $minified_file,
4810
			'fileName' => basename($minified_file),
4811
			'options' => array('async' => !empty($async), 'defer' => !empty($defer)),
4812
		));
4813
	}
4814
	// File has to exist. If it doesn't, try to create it.
4815
	elseif (@fopen($minified_file, 'w') === false || !smf_chmod($minified_file))
4816
	{
4817
		loadLanguage('Errors');
4818
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4819
4820
		// The process failed, so roll back to print each individual file.
4821
		return $data;
4822
	}
4823
4824
	// No namespaces, sorry!
4825
	$classType = 'MatthiasMullie\\Minify\\' . strtoupper($type);
4826
4827
	$minifier = new $classType();
4828
4829
	foreach ($data as $id => $file)
4830
	{
4831
		$toAdd = !empty($file['filePath']) && file_exists($file['filePath']) ? $file['filePath'] : false;
4832
4833
		// The file couldn't be located so it won't be added. Log this error.
4834
		if (empty($toAdd))
4835
		{
4836
			loadLanguage('Errors');
4837
			log_error(sprintf($txt['file_minimize_fail'], !empty($file['fileName']) ? $file['fileName'] : $id), 'general');
4838
			continue;
4839
		}
4840
4841
		// Add this file to the list.
4842
		$minifier->add($toAdd);
4843
	}
4844
4845
	// Create the file.
4846
	$minifier->minify($minified_file);
4847
	unset($minifier);
4848
	clearstatcache();
4849
4850
	// Minify process failed.
4851
	if (!filesize($minified_file))
4852
	{
4853
		loadLanguage('Errors');
4854
		log_error(sprintf($txt['file_not_created'], $minified_file), 'general');
4855
4856
		// The process failed so roll back to print each individual file.
4857
		return $data;
4858
	}
4859
4860
	return array('smf_minified' => array(
4861
		'fileUrl' => $settings['theme_url'] . '/' . ($type == 'css' ? 'css' : 'scripts') . '/' . basename($minified_file),
4862
		'filePath' => $minified_file,
4863
		'fileName' => basename($minified_file),
4864
		'options' => array('async' => $async, 'defer' => $defer),
4865
	));
4866
}
4867
4868
/**
4869
 * Clears out old minimized CSS and JavaScript files and ensures $modSettings['browser_cache'] is up to date
4870
 */
4871
function deleteAllMinified()
4872
{
4873
	global $smcFunc, $txt, $modSettings;
4874
4875
	$not_deleted = array();
4876
	$most_recent = 0;
4877
4878
	// Kinda sucks that we need to do another query to get all the theme dirs, but c'est la vie.
4879
	$request = $smcFunc['db_query']('', '
4880
		SELECT id_theme AS id, value AS dir
4881
		FROM {db_prefix}themes
4882
		WHERE variable = {string:var}',
4883
		array(
4884
			'var' => 'theme_dir',
4885
		)
4886
	);
4887
	while ($theme = $smcFunc['db_fetch_assoc']($request))
4888
	{
4889
		foreach (array('css', 'js') as $type)
4890
		{
4891
			foreach (glob(rtrim($theme['dir'], '/') . '/' . ($type == 'css' ? 'css' : 'scripts') . '/*.' . $type) as $filename)
4892
			{
4893
				// We want to find the most recent mtime of non-minified files
4894
				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

4894
				if (strpos(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_BASENAME), 'minified') === false)
Loading history...
4895
					$most_recent = max($modSettings['browser_cache'], (int) @filemtime($filename));
4896
4897
				// Try to delete minified files. Add them to our error list if that fails.
4898
				elseif (!@unlink($filename))
4899
					$not_deleted[] = $filename;
4900
			}
4901
		}
4902
	}
4903
	$smcFunc['db_free_result']($request);
4904
4905
	// This setting tracks the most recent modification time of any of our CSS and JS files
4906
	if ($most_recent > $modSettings['browser_cache'])
4907
		updateSettings(array('browser_cache' => $most_recent));
4908
4909
	// If any of the files could not be deleted, log an error about it.
4910
	if (!empty($not_deleted))
4911
	{
4912
		loadLanguage('Errors');
4913
		log_error(sprintf($txt['unlink_minimized_fail'], implode('<br>', $not_deleted)), 'general');
4914
	}
4915
}
4916
4917
/**
4918
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
4919
 *
4920
 * @todo this currently returns the hash if new, and the full filename otherwise.
4921
 * Something messy like that.
4922
 * @todo and of course everything relies on this behavior and work around it. :P.
4923
 * Converters included.
4924
 *
4925
 * @param string $filename The name of the file
4926
 * @param int $attachment_id The ID of the attachment
4927
 * @param string|null $dir Which directory it should be in (null to use current one)
4928
 * @param bool $new Whether this is a new attachment
4929
 * @param string $file_hash The file hash
4930
 * @return string The path to the file
4931
 */
4932
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
4933
{
4934
	global $modSettings, $smcFunc;
4935
4936
	// Just make up a nice hash...
4937
	if ($new)
4938
		return sha1(md5($filename . time()) . mt_rand());
4939
4940
	// Just make sure that attachment id is only a int
4941
	$attachment_id = (int) $attachment_id;
4942
4943
	// Grab the file hash if it wasn't added.
4944
	// Left this for legacy.
4945
	if ($file_hash === '')
4946
	{
4947
		$request = $smcFunc['db_query']('', '
4948
			SELECT file_hash
4949
			FROM {db_prefix}attachments
4950
			WHERE id_attach = {int:id_attach}',
4951
			array(
4952
				'id_attach' => $attachment_id,
4953
			)
4954
		);
4955
4956
		if ($smcFunc['db_num_rows']($request) === 0)
4957
			return false;
4958
4959
		list ($file_hash) = $smcFunc['db_fetch_row']($request);
4960
		$smcFunc['db_free_result']($request);
4961
	}
4962
4963
	// Still no hash? mmm...
4964
	if (empty($file_hash))
4965
		$file_hash = sha1(md5($filename . time()) . mt_rand());
4966
4967
	// Are we using multiple directories?
4968
	if (is_array($modSettings['attachmentUploadDir']))
4969
		$path = $modSettings['attachmentUploadDir'][$dir];
4970
4971
	else
4972
		$path = $modSettings['attachmentUploadDir'];
4973
4974
	return $path . '/' . $attachment_id . '_' . $file_hash . '.dat';
4975
}
4976
4977
/**
4978
 * Convert a single IP to a ranged IP.
4979
 * internal function used to convert a user-readable format to a format suitable for the database.
4980
 *
4981
 * @param string $fullip The full IP
4982
 * @return array An array of IP parts
4983
 */
4984
function ip2range($fullip)
4985
{
4986
	// Pretend that 'unknown' is 255.255.255.255. (since that can't be an IP anyway.)
4987
	if ($fullip == 'unknown')
4988
		$fullip = '255.255.255.255';
4989
4990
	$ip_parts = explode('-', $fullip);
4991
	$ip_array = array();
4992
4993
	// if ip 22.12.31.21
4994
	if (count($ip_parts) == 1 && isValidIP($fullip))
4995
	{
4996
		$ip_array['low'] = $fullip;
4997
		$ip_array['high'] = $fullip;
4998
		return $ip_array;
4999
	} // if ip 22.12.* -> 22.12.* - 22.12.*
5000
	elseif (count($ip_parts) == 1)
5001
	{
5002
		$ip_parts[0] = $fullip;
5003
		$ip_parts[1] = $fullip;
5004
	}
5005
5006
	// if ip 22.12.31.21-12.21.31.21
5007
	if (count($ip_parts) == 2 && isValidIP($ip_parts[0]) && isValidIP($ip_parts[1]))
5008
	{
5009
		$ip_array['low'] = $ip_parts[0];
5010
		$ip_array['high'] = $ip_parts[1];
5011
		return $ip_array;
5012
	}
5013
	elseif (count($ip_parts) == 2) // if ip 22.22.*-22.22.*
5014
	{
5015
		$valid_low = isValidIP($ip_parts[0]);
5016
		$valid_high = isValidIP($ip_parts[1]);
5017
		$count = 0;
5018
		$mode = (preg_match('/:/', $ip_parts[0]) > 0 ? ':' : '.');
5019
		$max = ($mode == ':' ? 'ffff' : '255');
5020
		$min = 0;
5021
		if (!$valid_low)
5022
		{
5023
			$ip_parts[0] = preg_replace('/\*/', '0', $ip_parts[0]);
5024
			$valid_low = isValidIP($ip_parts[0]);
5025
			while (!$valid_low)
5026
			{
5027
				$ip_parts[0] .= $mode . $min;
5028
				$valid_low = isValidIP($ip_parts[0]);
5029
				$count++;
5030
				if ($count > 9) break;
5031
			}
5032
		}
5033
5034
		$count = 0;
5035
		if (!$valid_high)
5036
		{
5037
			$ip_parts[1] = preg_replace('/\*/', $max, $ip_parts[1]);
5038
			$valid_high = isValidIP($ip_parts[1]);
5039
			while (!$valid_high)
5040
			{
5041
				$ip_parts[1] .= $mode . $max;
5042
				$valid_high = isValidIP($ip_parts[1]);
5043
				$count++;
5044
				if ($count > 9) break;
5045
			}
5046
		}
5047
5048
		if ($valid_high && $valid_low)
5049
		{
5050
			$ip_array['low'] = $ip_parts[0];
5051
			$ip_array['high'] = $ip_parts[1];
5052
		}
5053
	}
5054
5055
	return $ip_array;
5056
}
5057
5058
/**
5059
 * Lookup an IP; try shell_exec first because we can do a timeout on it.
5060
 *
5061
 * @param string $ip The IP to get the hostname from
5062
 * @return string The hostname
5063
 */
5064
function host_from_ip($ip)
5065
{
5066
	global $modSettings;
5067
5068
	if (($host = cache_get_data('hostlookup-' . $ip, 600)) !== null)
5069
		return $host;
5070
	$t = microtime(true);
5071
5072
	// Try the Linux host command, perhaps?
5073
	if (!isset($host) && (strpos(strtolower(PHP_OS), 'win') === false || strpos(strtolower(PHP_OS), 'darwin') !== false) && mt_rand(0, 1) == 1)
5074
	{
5075
		if (!isset($modSettings['host_to_dis']))
5076
			$test = @shell_exec('host -W 1 ' . @escapeshellarg($ip));
5077
		else
5078
			$test = @shell_exec('host ' . @escapeshellarg($ip));
5079
5080
		// Did host say it didn't find anything?
5081
		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

5081
		if (strpos(/** @scrutinizer ignore-type */ $test, 'not found') !== false)
Loading history...
5082
			$host = '';
5083
		// Invalid server option?
5084
		elseif ((strpos($test, 'invalid option') || strpos($test, 'Invalid query name 1')) && !isset($modSettings['host_to_dis']))
5085
			updateSettings(array('host_to_dis' => 1));
5086
		// Maybe it found something, after all?
5087
		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

5087
		elseif (preg_match('~\s([^\s]+?)\.\s~', /** @scrutinizer ignore-type */ $test, $match) == 1)
Loading history...
5088
			$host = $match[1];
5089
	}
5090
5091
	// This is nslookup; usually only Windows, but possibly some Unix?
5092
	if (!isset($host) && stripos(PHP_OS, 'win') !== false && strpos(strtolower(PHP_OS), 'darwin') === false && mt_rand(0, 1) == 1)
5093
	{
5094
		$test = @shell_exec('nslookup -timeout=1 ' . @escapeshellarg($ip));
5095
		if (strpos($test, 'Non-existent domain') !== false)
5096
			$host = '';
5097
		elseif (preg_match('~Name:\s+([^\s]+)~', $test, $match) == 1)
5098
			$host = $match[1];
5099
	}
5100
5101
	// This is the last try :/.
5102
	if (!isset($host) || $host === false)
5103
		$host = @gethostbyaddr($ip);
5104
5105
	// It took a long time, so let's cache it!
5106
	if (microtime(true) - $t > 0.5)
5107
		cache_put_data('hostlookup-' . $ip, $host, 600);
5108
5109
	return $host;
5110
}
5111
5112
/**
5113
 * Chops a string into words and prepares them to be inserted into (or searched from) the database.
5114
 *
5115
 * @param string $text The text to split into words
5116
 * @param int $max_chars The maximum number of characters per word
5117
 * @param bool $encrypt Whether to encrypt the results
5118
 * @return array An array of ints or words depending on $encrypt
5119
 */
5120
function text2words($text, $max_chars = 20, $encrypt = false)
5121
{
5122
	global $smcFunc, $context;
5123
5124
	// Upgrader may be working on old DBs...
5125
	if (!isset($context['utf8']))
5126
		$context['utf8'] = false;
5127
5128
	// Step 1: Remove entities/things we don't consider words:
5129
	$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>' => ' ')));
5130
5131
	// Step 2: Entities we left to letters, where applicable, lowercase.
5132
	$words = un_htmlspecialchars($smcFunc['strtolower']($words));
5133
5134
	// Step 3: Ready to split apart and index!
5135
	$words = explode(' ', $words);
5136
5137
	if ($encrypt)
5138
	{
5139
		$possible_chars = array_flip(array_merge(range(46, 57), range(65, 90), range(97, 122)));
5140
		$returned_ints = array();
5141
		foreach ($words as $word)
5142
		{
5143
			if (($word = trim($word, '-_\'')) !== '')
5144
			{
5145
				$encrypted = substr(crypt($word, 'uk'), 2, $max_chars);
5146
				$total = 0;
5147
				for ($i = 0; $i < $max_chars; $i++)
5148
					$total += $possible_chars[ord($encrypted[$i])] * pow(63, $i);
5149
				$returned_ints[] = $max_chars == 4 ? min($total, 16777215) : $total;
5150
			}
5151
		}
5152
		return array_unique($returned_ints);
5153
	}
5154
	else
5155
	{
5156
		// Trim characters before and after and add slashes for database insertion.
5157
		$returned_words = array();
5158
		foreach ($words as $word)
5159
			if (($word = trim($word, '-_\'')) !== '')
5160
				$returned_words[] = $max_chars === null ? $word : substr($word, 0, $max_chars);
5161
5162
		// Filter out all words that occur more than once.
5163
		return array_unique($returned_words);
5164
	}
5165
}
5166
5167
/**
5168
 * Creates an image/text button
5169
 *
5170
 * @deprecated since 2.1
5171
 * @param string $name The name of the button (should be a main_icons class or the name of an image)
5172
 * @param string $alt The alt text
5173
 * @param string $label The $txt string to use as the label
5174
 * @param string $custom Custom text/html to add to the img tag (only when using an actual image)
5175
 * @param boolean $force_use Whether to force use of this when template_create_button is available
5176
 * @return string The HTML to display the button
5177
 */
5178
function create_button($name, $alt, $label = '', $custom = '', $force_use = false)
5179
{
5180
	global $settings, $txt;
5181
5182
	// Does the current loaded theme have this and we are not forcing the usage of this function?
5183
	if (function_exists('template_create_button') && !$force_use)
5184
		return template_create_button($name, $alt, $label = '', $custom = '');
5185
5186
	if (!$settings['use_image_buttons'])
5187
		return $txt[$alt];
5188
	elseif (!empty($settings['use_buttons']))
5189
		return '<span class="main_icons ' . $name . '" alt="' . $txt[$alt] . '"></span>' . ($label != '' ? '&nbsp;<strong>' . $txt[$label] . '</strong>' : '');
5190
	else
5191
		return '<img src="' . $settings['lang_images_url'] . '/' . $name . '" alt="' . $txt[$alt] . '" ' . $custom . '>';
5192
}
5193
5194
/**
5195
 * Sets up all of the top menu buttons
5196
 * Saves them in the cache if it is available and on
5197
 * Places the results in $context
5198
 */
5199
function setupMenuContext()
5200
{
5201
	global $context, $modSettings, $user_info, $txt, $scripturl, $sourcedir, $settings, $smcFunc, $cache_enable;
5202
5203
	// Set up the menu privileges.
5204
	$context['allow_search'] = !empty($modSettings['allow_guestAccess']) ? allowedTo('search_posts') : (!$user_info['is_guest'] && allowedTo('search_posts'));
5205
	$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'));
5206
5207
	$context['allow_memberlist'] = allowedTo('view_mlist');
5208
	$context['allow_calendar'] = allowedTo('calendar_view') && !empty($modSettings['cal_enabled']);
5209
	$context['allow_moderation_center'] = $context['user']['can_mod'];
5210
	$context['allow_pm'] = allowedTo('pm_read');
5211
5212
	$cacheTime = $modSettings['lastActive'] * 60;
5213
5214
	// Initial "can you post an event in the calendar" option - but this might have been set in the calendar already.
5215
	if (!isset($context['allow_calendar_event']))
5216
	{
5217
		$context['allow_calendar_event'] = $context['allow_calendar'] && allowedTo('calendar_post');
5218
5219
		// If you don't allow events not linked to posts and you're not an admin, we have more work to do...
5220
		if ($context['allow_calendar'] && $context['allow_calendar_event'] && empty($modSettings['cal_allow_unlinked']) && !$user_info['is_admin'])
5221
		{
5222
			$boards_can_post = boardsAllowedTo('post_new');
5223
			$context['allow_calendar_event'] &= !empty($boards_can_post);
5224
		}
5225
	}
5226
5227
	// There is some menu stuff we need to do if we're coming at this from a non-guest perspective.
5228
	if (!$context['user']['is_guest'])
5229
	{
5230
		addInlineJavaScript('
5231
	var user_menus = new smc_PopupMenu();
5232
	user_menus.add("profile", "' . $scripturl . '?action=profile;area=popup");
5233
	user_menus.add("alerts", "' . $scripturl . '?action=profile;area=alerts_popup;u=' . $context['user']['id'] . '");', true);
5234
		if ($context['allow_pm'])
5235
			addInlineJavaScript('
5236
	user_menus.add("pm", "' . $scripturl . '?action=pm;sa=popup");', true);
5237
5238
		if (!empty($modSettings['enable_ajax_alerts']))
5239
		{
5240
			require_once($sourcedir . '/Subs-Notify.php');
5241
5242
			$timeout = getNotifyPrefs($context['user']['id'], 'alert_timeout', true);
5243
			$timeout = empty($timeout) ? 10000 : $timeout[$context['user']['id']]['alert_timeout'] * 1000;
5244
5245
			addInlineJavaScript('
5246
	var new_alert_title = "' . $context['forum_name_html_safe'] . '";
5247
	var alert_timeout = ' . $timeout . ';');
5248
			loadJavaScriptFile('alerts.js', array('minimize' => true), 'smf_alerts');
5249
		}
5250
	}
5251
5252
	// All the buttons we can possible want and then some, try pulling the final list of buttons from cache first.
5253
	if (($menu_buttons = cache_get_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $cacheTime)) === null || time() - $cacheTime <= $modSettings['settings_updated'])
5254
	{
5255
		$buttons = array(
5256
			'home' => array(
5257
				'title' => $txt['home'],
5258
				'href' => $scripturl,
5259
				'show' => true,
5260
				'sub_buttons' => array(
5261
				),
5262
				'is_last' => $context['right_to_left'],
5263
			),
5264
			'search' => array(
5265
				'title' => $txt['search'],
5266
				'href' => $scripturl . '?action=search',
5267
				'show' => $context['allow_search'],
5268
				'sub_buttons' => array(
5269
				),
5270
			),
5271
			'admin' => array(
5272
				'title' => $txt['admin'],
5273
				'href' => $scripturl . '?action=admin',
5274
				'show' => $context['allow_admin'],
5275
				'sub_buttons' => array(
5276
					'featuresettings' => array(
5277
						'title' => $txt['modSettings_title'],
5278
						'href' => $scripturl . '?action=admin;area=featuresettings',
5279
						'show' => allowedTo('admin_forum'),
5280
					),
5281
					'packages' => array(
5282
						'title' => $txt['package'],
5283
						'href' => $scripturl . '?action=admin;area=packages',
5284
						'show' => allowedTo('admin_forum'),
5285
					),
5286
					'errorlog' => array(
5287
						'title' => $txt['errorlog'],
5288
						'href' => $scripturl . '?action=admin;area=logs;sa=errorlog;desc',
5289
						'show' => allowedTo('admin_forum') && !empty($modSettings['enableErrorLogging']),
5290
					),
5291
					'permissions' => array(
5292
						'title' => $txt['edit_permissions'],
5293
						'href' => $scripturl . '?action=admin;area=permissions',
5294
						'show' => allowedTo('manage_permissions'),
5295
					),
5296
					'memberapprove' => array(
5297
						'title' => $txt['approve_members_waiting'],
5298
						'href' => $scripturl . '?action=admin;area=viewmembers;sa=browse;type=approve',
5299
						'show' => !empty($context['unapproved_members']),
5300
						'is_last' => true,
5301
					),
5302
				),
5303
			),
5304
			'moderate' => array(
5305
				'title' => $txt['moderate'],
5306
				'href' => $scripturl . '?action=moderate',
5307
				'show' => $context['allow_moderation_center'],
5308
				'sub_buttons' => array(
5309
					'modlog' => array(
5310
						'title' => $txt['modlog_view'],
5311
						'href' => $scripturl . '?action=moderate;area=modlog',
5312
						'show' => !empty($modSettings['modlog_enabled']) && !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
5313
					),
5314
					'poststopics' => array(
5315
						'title' => $txt['mc_unapproved_poststopics'],
5316
						'href' => $scripturl . '?action=moderate;area=postmod;sa=posts',
5317
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
5318
					),
5319
					'attachments' => array(
5320
						'title' => $txt['mc_unapproved_attachments'],
5321
						'href' => $scripturl . '?action=moderate;area=attachmod;sa=attachments',
5322
						'show' => $modSettings['postmod_active'] && !empty($user_info['mod_cache']['ap']),
5323
					),
5324
					'reports' => array(
5325
						'title' => $txt['mc_reported_posts'],
5326
						'href' => $scripturl . '?action=moderate;area=reportedposts',
5327
						'show' => !empty($user_info['mod_cache']) && $user_info['mod_cache']['bq'] != '0=1',
5328
					),
5329
					'reported_members' => array(
5330
						'title' => $txt['mc_reported_members'],
5331
						'href' => $scripturl . '?action=moderate;area=reportedmembers',
5332
						'show' => allowedTo('moderate_forum'),
5333
						'is_last' => true,
5334
					)
5335
				),
5336
			),
5337
			'calendar' => array(
5338
				'title' => $txt['calendar'],
5339
				'href' => $scripturl . '?action=calendar',
5340
				'show' => $context['allow_calendar'],
5341
				'sub_buttons' => array(
5342
					'view' => array(
5343
						'title' => $txt['calendar_menu'],
5344
						'href' => $scripturl . '?action=calendar',
5345
						'show' => $context['allow_calendar_event'],
5346
					),
5347
					'post' => array(
5348
						'title' => $txt['calendar_post_event'],
5349
						'href' => $scripturl . '?action=calendar;sa=post',
5350
						'show' => $context['allow_calendar_event'],
5351
						'is_last' => true,
5352
					),
5353
				),
5354
			),
5355
			'mlist' => array(
5356
				'title' => $txt['members_title'],
5357
				'href' => $scripturl . '?action=mlist',
5358
				'show' => $context['allow_memberlist'],
5359
				'sub_buttons' => array(
5360
					'mlist_view' => array(
5361
						'title' => $txt['mlist_menu_view'],
5362
						'href' => $scripturl . '?action=mlist',
5363
						'show' => true,
5364
					),
5365
					'mlist_search' => array(
5366
						'title' => $txt['mlist_search'],
5367
						'href' => $scripturl . '?action=mlist;sa=search',
5368
						'show' => true,
5369
						'is_last' => true,
5370
					),
5371
				),
5372
				'is_last' => !$context['right_to_left'] && (!$user_info['is_guest'] || !$context['can_register']),
5373
			),
5374
			'signup' => array(
5375
				'title' => $txt['register'],
5376
				'href' => $scripturl . '?action=signup',
5377
				'show' => $user_info['is_guest'] && $context['can_register'],
5378
				'sub_buttons' => array(
5379
				),
5380
				'is_last' => !$context['right_to_left'],
5381
			),
5382
		);
5383
5384
		// Allow editing menu buttons easily.
5385
		call_integration_hook('integrate_menu_buttons', array(&$buttons));
5386
5387
		// Now we put the buttons in the context so the theme can use them.
5388
		$menu_buttons = array();
5389
		foreach ($buttons as $act => $button)
5390
			if (!empty($button['show']))
5391
			{
5392
				$button['active_button'] = false;
5393
5394
				// Make sure the last button truly is the last button.
5395
				if (!empty($button['is_last']))
5396
				{
5397
					if (isset($last_button))
5398
						unset($menu_buttons[$last_button]['is_last']);
5399
					$last_button = $act;
5400
				}
5401
5402
				// Go through the sub buttons if there are any.
5403
				if (!empty($button['sub_buttons']))
5404
					foreach ($button['sub_buttons'] as $key => $subbutton)
5405
					{
5406
						if (empty($subbutton['show']))
5407
							unset($button['sub_buttons'][$key]);
5408
5409
						// 2nd level sub buttons next...
5410
						if (!empty($subbutton['sub_buttons']))
5411
						{
5412
							foreach ($subbutton['sub_buttons'] as $key2 => $sub_button2)
5413
							{
5414
								if (empty($sub_button2['show']))
5415
									unset($button['sub_buttons'][$key]['sub_buttons'][$key2]);
5416
							}
5417
						}
5418
					}
5419
5420
				// Does this button have its own icon?
5421
				if (isset($button['icon']) && file_exists($settings['theme_dir'] . '/images/' . $button['icon']))
5422
					$button['icon'] = '<img src="' . $settings['images_url'] . '/' . $button['icon'] . '" alt="">';
5423
				elseif (isset($button['icon']) && file_exists($settings['default_theme_dir'] . '/images/' . $button['icon']))
5424
					$button['icon'] = '<img src="' . $settings['default_images_url'] . '/' . $button['icon'] . '" alt="">';
5425
				elseif (isset($button['icon']))
5426
					$button['icon'] = '<span class="main_icons ' . $button['icon'] . '"></span>';
5427
				else
5428
					$button['icon'] = '<span class="main_icons ' . $act . '"></span>';
5429
5430
				$menu_buttons[$act] = $button;
5431
			}
5432
5433
		if (!empty($cache_enable) && $cache_enable >= 2)
5434
			cache_put_data('menu_buttons-' . implode('_', $user_info['groups']) . '-' . $user_info['language'], $menu_buttons, $cacheTime);
5435
	}
5436
5437
	$context['menu_buttons'] = $menu_buttons;
5438
5439
	// Logging out requires the session id in the url.
5440
	if (isset($context['menu_buttons']['logout']))
5441
		$context['menu_buttons']['logout']['href'] = sprintf($context['menu_buttons']['logout']['href'], $context['session_var'], $context['session_id']);
5442
5443
	// Figure out which action we are doing so we can set the active tab.
5444
	// Default to home.
5445
	$current_action = 'home';
5446
5447
	if (isset($context['menu_buttons'][$context['current_action']]))
5448
		$current_action = $context['current_action'];
5449
	elseif ($context['current_action'] == 'search2')
5450
		$current_action = 'search';
5451
	elseif ($context['current_action'] == 'theme')
5452
		$current_action = isset($_REQUEST['sa']) && $_REQUEST['sa'] == 'pick' ? 'profile' : 'admin';
5453
	elseif ($context['current_action'] == 'register2')
5454
		$current_action = 'register';
5455
	elseif ($context['current_action'] == 'login2' || ($user_info['is_guest'] && $context['current_action'] == 'reminder'))
5456
		$current_action = 'login';
5457
	elseif ($context['current_action'] == 'groups' && $context['allow_moderation_center'])
5458
		$current_action = 'moderate';
5459
5460
	// There are certain exceptions to the above where we don't want anything on the menu highlighted.
5461
	if ($context['current_action'] == 'profile' && !empty($context['user']['is_owner']))
5462
	{
5463
		$current_action = !empty($_GET['area']) && $_GET['area'] == 'showalerts' ? 'self_alerts' : 'self_profile';
5464
		$context[$current_action] = true;
5465
	}
5466
	elseif ($context['current_action'] == 'pm')
5467
	{
5468
		$current_action = 'self_pm';
5469
		$context['self_pm'] = true;
5470
	}
5471
5472
	$context['total_mod_reports'] = 0;
5473
	$context['total_admin_reports'] = 0;
5474
5475
	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']))
5476
	{
5477
		$context['total_mod_reports'] = $context['open_mod_reports'];
5478
		$context['menu_buttons']['moderate']['sub_buttons']['reports']['amt'] = $context['open_mod_reports'];
5479
	}
5480
5481
	// Show how many errors there are
5482
	if (!empty($context['menu_buttons']['admin']['sub_buttons']['errorlog']))
5483
	{
5484
		// Get an error count, if necessary
5485
		if (!isset($context['num_errors']))
5486
		{
5487
			$query = $smcFunc['db_query']('', '
5488
				SELECT COUNT(*)
5489
				FROM {db_prefix}log_errors',
5490
				array()
5491
			);
5492
5493
			list($context['num_errors']) = $smcFunc['db_fetch_row']($query);
5494
			$smcFunc['db_free_result']($query);
5495
		}
5496
5497
		if (!empty($context['num_errors']))
5498
		{
5499
			$context['total_admin_reports'] += $context['num_errors'];
5500
			$context['menu_buttons']['admin']['sub_buttons']['errorlog']['amt'] = $context['num_errors'];
5501
		}
5502
	}
5503
5504
	// Show number of reported members
5505
	if (!empty($context['open_member_reports']) && !empty($context['menu_buttons']['moderate']['sub_buttons']['reported_members']))
5506
	{
5507
		$context['total_mod_reports'] += $context['open_member_reports'];
5508
		$context['menu_buttons']['moderate']['sub_buttons']['reported_members']['amt'] = $context['open_member_reports'];
5509
	}
5510
5511
	if (!empty($context['unapproved_members']) && !empty($context['menu_buttons']['admin']))
5512
	{
5513
		$context['menu_buttons']['admin']['sub_buttons']['memberapprove']['amt'] = $context['unapproved_members'];
5514
		$context['total_admin_reports'] += $context['unapproved_members'];
5515
	}
5516
5517
	if ($context['total_admin_reports'] > 0 && !empty($context['menu_buttons']['admin']))
5518
	{
5519
		$context['menu_buttons']['admin']['amt'] = $context['total_admin_reports'];
5520
	}
5521
5522
	// Do we have any open reports?
5523
	if ($context['total_mod_reports'] > 0 && !empty($context['menu_buttons']['moderate']))
5524
	{
5525
		$context['menu_buttons']['moderate']['amt'] = $context['total_mod_reports'];
5526
	}
5527
5528
	// Not all actions are simple.
5529
	call_integration_hook('integrate_current_action', array(&$current_action));
5530
5531
	if (isset($context['menu_buttons'][$current_action]))
5532
		$context['menu_buttons'][$current_action]['active_button'] = true;
5533
}
5534
5535
/**
5536
 * Generate a random seed and ensure it's stored in settings.
5537
 */
5538
function smf_seed_generator()
5539
{
5540
	updateSettings(array('rand_seed' => microtime(true)));
5541
}
5542
5543
/**
5544
 * Process functions of an integration hook.
5545
 * calls all functions of the given hook.
5546
 * supports static class method calls.
5547
 *
5548
 * @param string $hook The hook name
5549
 * @param array $parameters An array of parameters this hook implements
5550
 * @return array The results of the functions
5551
 */
5552
function call_integration_hook($hook, $parameters = array())
5553
{
5554
	global $modSettings, $settings, $boarddir, $sourcedir, $db_show_debug;
5555
	global $context, $txt;
5556
5557
	if ($db_show_debug === true)
5558
		$context['debug']['hooks'][] = $hook;
5559
5560
	// Need to have some control.
5561
	if (!isset($context['instances']))
5562
		$context['instances'] = array();
5563
5564
	$results = array();
5565
	if (empty($modSettings[$hook]))
5566
		return $results;
5567
5568
	$functions = explode(',', $modSettings[$hook]);
5569
	// Loop through each function.
5570
	foreach ($functions as $function)
5571
	{
5572
		// Hook has been marked as "disabled". Skip it!
5573
		if (strpos($function, '!') !== false)
5574
			continue;
5575
5576
		$call = call_helper($function, true);
5577
5578
		// Is it valid?
5579
		if (!empty($call))
5580
			$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

5580
			$results[$function] = call_user_func_array(/** @scrutinizer ignore-type */ $call, $parameters);
Loading history...
5581
		// This failed, but we want to do so silently.
5582
		elseif (!empty($function) && !empty($context['ignore_hook_errors']))
5583
			return $results;
5584
		// Whatever it was suppose to call, it failed :(
5585
		elseif (!empty($function))
5586
		{
5587
			loadLanguage('Errors');
5588
5589
			// Get a full path to show on error.
5590
			if (strpos($function, '|') !== false)
5591
			{
5592
				list ($file, $string) = explode('|', $function);
5593
				$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'])));
5594
				log_error(sprintf($txt['hook_fail_call_to'], $string, $absPath), 'general');
5595
			}
5596
			// "Assume" the file resides on $boarddir somewhere...
5597
			else
5598
				log_error(sprintf($txt['hook_fail_call_to'], $function, $boarddir), 'general');
5599
		}
5600
	}
5601
5602
	return $results;
5603
}
5604
5605
/**
5606
 * Add a function for integration hook.
5607
 * does nothing if the function is already added.
5608
 *
5609
 * @param string $hook The complete hook name.
5610
 * @param string $function The function name. Can be a call to a method via Class::method.
5611
 * @param bool $permanent If true, updates the value in settings table.
5612
 * @param string $file The file. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5613
 * @param bool $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5614
 */
5615
function add_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5616
{
5617
	global $smcFunc, $modSettings;
5618
5619
	// Any objects?
5620
	if ($object)
5621
		$function = $function . '#';
5622
5623
	// Any files  to load?
5624
	if (!empty($file) && is_string($file))
5625
		$function = $file . (!empty($function) ? '|' . $function : '');
5626
5627
	// Get the correct string.
5628
	$integration_call = $function;
5629
5630
	// Is it going to be permanent?
5631
	if ($permanent)
5632
	{
5633
		$request = $smcFunc['db_query']('', '
5634
			SELECT value
5635
			FROM {db_prefix}settings
5636
			WHERE variable = {string:variable}',
5637
			array(
5638
				'variable' => $hook,
5639
			)
5640
		);
5641
		list ($current_functions) = $smcFunc['db_fetch_row']($request);
5642
		$smcFunc['db_free_result']($request);
5643
5644
		if (!empty($current_functions))
5645
		{
5646
			$current_functions = explode(',', $current_functions);
5647
			if (in_array($integration_call, $current_functions))
5648
				return;
5649
5650
			$permanent_functions = array_merge($current_functions, array($integration_call));
5651
		}
5652
		else
5653
			$permanent_functions = array($integration_call);
5654
5655
		updateSettings(array($hook => implode(',', $permanent_functions)));
5656
	}
5657
5658
	// Make current function list usable.
5659
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5660
5661
	// Do nothing, if it's already there.
5662
	if (in_array($integration_call, $functions))
5663
		return;
5664
5665
	$functions[] = $integration_call;
5666
	$modSettings[$hook] = implode(',', $functions);
5667
}
5668
5669
/**
5670
 * Remove an integration hook function.
5671
 * Removes the given function from the given hook.
5672
 * Does nothing if the function is not available.
5673
 *
5674
 * @param string $hook The complete hook name.
5675
 * @param string $function The function name. Can be a call to a method via Class::method.
5676
 * @param boolean $permanent Irrelevant for the function itself but need to declare it to match
5677
 * @param string $file The filename. Must include one of the following wildcards: $boarddir, $sourcedir, $themedir, example: $sourcedir/Test.php
5678
 * @param boolean $object Indicates if your class will be instantiated when its respective hook is called. If true, your function must be a method.
5679
 * @see add_integration_function
5680
 */
5681
function remove_integration_function($hook, $function, $permanent = true, $file = '', $object = false)
5682
{
5683
	global $smcFunc, $modSettings;
5684
5685
	// Any objects?
5686
	if ($object)
5687
		$function = $function . '#';
5688
5689
	// Any files  to load?
5690
	if (!empty($file) && is_string($file))
5691
		$function = $file . '|' . $function;
5692
5693
	// Get the correct string.
5694
	$integration_call = $function;
5695
5696
	// Get the permanent functions.
5697
	$request = $smcFunc['db_query']('', '
5698
		SELECT value
5699
		FROM {db_prefix}settings
5700
		WHERE variable = {string:variable}',
5701
		array(
5702
			'variable' => $hook,
5703
		)
5704
	);
5705
	list ($current_functions) = $smcFunc['db_fetch_row']($request);
5706
	$smcFunc['db_free_result']($request);
5707
5708
	if (!empty($current_functions))
5709
	{
5710
		$current_functions = explode(',', $current_functions);
5711
5712
		if (in_array($integration_call, $current_functions))
5713
			updateSettings(array($hook => implode(',', array_diff($current_functions, array($integration_call)))));
5714
	}
5715
5716
	// Turn the function list into something usable.
5717
	$functions = empty($modSettings[$hook]) ? array() : explode(',', $modSettings[$hook]);
5718
5719
	// You can only remove it if it's available.
5720
	if (!in_array($integration_call, $functions))
5721
		return;
5722
5723
	$functions = array_diff($functions, array($integration_call));
5724
	$modSettings[$hook] = implode(',', $functions);
5725
}
5726
5727
/**
5728
 * Receives a string and tries to figure it out if its a method or a function.
5729
 * If a method is found, it looks for a "#" which indicates SMF should create a new instance of the given class.
5730
 * Checks the string/array for is_callable() and return false/fatal_lang_error is the given value results in a non callable string/array.
5731
 * Prepare and returns a callable depending on the type of method/function found.
5732
 *
5733
 * @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)
5734
 * @param boolean $return If true, the function will not call the function/method but instead will return the formatted string.
5735
 * @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.
5736
 */
5737
function call_helper($string, $return = false)
5738
{
5739
	global $context, $smcFunc, $txt, $db_show_debug;
5740
5741
	// Really?
5742
	if (empty($string))
5743
		return false;
5744
5745
	// An array? should be a "callable" array IE array(object/class, valid_callable).
5746
	// A closure? should be a callable one.
5747
	if (is_array($string) || $string instanceof Closure)
5748
		return $return ? $string : (is_callable($string) ? call_user_func($string) : false);
5749
5750
	// No full objects, sorry! pass a method or a property instead!
5751
	if (is_object($string))
5752
		return false;
5753
5754
	// Stay vitaminized my friends...
5755
	$string = $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($string));
5756
5757
	// Is there a file to load?
5758
	$string = load_file($string);
5759
5760
	// Loaded file failed
5761
	if (empty($string))
5762
		return false;
5763
5764
	// Found a method.
5765
	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

5765
	if (strpos(/** @scrutinizer ignore-type */ $string, '::') !== false)
Loading history...
5766
	{
5767
		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

5767
		list ($class, $method) = explode('::', /** @scrutinizer ignore-type */ $string);
Loading history...
5768
5769
		// Check if a new object will be created.
5770
		if (strpos($method, '#') !== false)
5771
		{
5772
			// Need to remove the # thing.
5773
			$method = str_replace('#', '', $method);
5774
5775
			// Don't need to create a new instance for every method.
5776
			if (empty($context['instances'][$class]) || !($context['instances'][$class] instanceof $class))
5777
			{
5778
				$context['instances'][$class] = new $class;
5779
5780
				// Add another one to the list.
5781
				if ($db_show_debug === true)
5782
				{
5783
					if (!isset($context['debug']['instances']))
5784
						$context['debug']['instances'] = array();
5785
5786
					$context['debug']['instances'][$class] = $class;
5787
				}
5788
			}
5789
5790
			$func = array($context['instances'][$class], $method);
5791
		}
5792
5793
		// Right then. This is a call to a static method.
5794
		else
5795
			$func = array($class, $method);
5796
	}
5797
5798
	// Nope! just a plain regular function.
5799
	else
5800
		$func = $string;
5801
5802
	// We can't call this helper, but we want to silently ignore this.
5803
	if (!is_callable($func, false, $callable_name) && !empty($context['ignore_hook_errors']))
5804
		return false;
5805
5806
	// Right, we got what we need, time to do some checks.
5807
	elseif (!is_callable($func, false, $callable_name))
5808
	{
5809
		loadLanguage('Errors');
5810
		log_error(sprintf($txt['sub_action_fail'], $callable_name), 'general');
5811
5812
		// Gotta tell everybody.
5813
		return false;
5814
	}
5815
5816
	// Everything went better than expected.
5817
	else
5818
	{
5819
		// What are we gonna do about it?
5820
		if ($return)
5821
			return $func;
5822
5823
		// If this is a plain function, avoid the heat of calling call_user_func().
5824
		else
5825
		{
5826
			if (is_array($func))
5827
				call_user_func($func);
5828
5829
			else
5830
				$func();
5831
		}
5832
	}
5833
}
5834
5835
/**
5836
 * Receives a string and tries to figure it out if it contains info to load a file.
5837
 * Checks for a | (pipe) symbol and tries to load a file with the info given.
5838
 * 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.
5839
 *
5840
 * @param string $string The string containing a valid format.
5841
 * @return string|boolean The given string with the pipe and file info removed. Boolean false if the file couldn't be loaded.
5842
 */
5843
function load_file($string)
5844
{
5845
	global $sourcedir, $txt, $boarddir, $settings, $context;
5846
5847
	if (empty($string))
5848
		return false;
5849
5850
	if (strpos($string, '|') !== false)
5851
	{
5852
		list ($file, $string) = explode('|', $string);
5853
5854
		// Match the wildcards to their regular vars.
5855
		if (empty($settings['theme_dir']))
5856
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir));
5857
5858
		else
5859
			$absPath = strtr(trim($file), array('$boarddir' => $boarddir, '$sourcedir' => $sourcedir, '$themedir' => $settings['theme_dir']));
5860
5861
		// Load the file if it can be loaded.
5862
		if (file_exists($absPath))
5863
			require_once($absPath);
5864
5865
		// No? try a fallback to $sourcedir
5866
		else
5867
		{
5868
			$absPath = $sourcedir . '/' . $file;
5869
5870
			if (file_exists($absPath))
5871
				require_once($absPath);
5872
5873
			// Sorry, can't do much for you at this point.
5874
			elseif (empty($context['uninstalling']))
5875
			{
5876
				loadLanguage('Errors');
5877
				log_error(sprintf($txt['hook_fail_loading_file'], $absPath), 'general');
5878
5879
				// File couldn't be loaded.
5880
				return false;
5881
			}
5882
		}
5883
	}
5884
5885
	return $string;
5886
}
5887
5888
/**
5889
 * Get the contents of a URL, irrespective of allow_url_fopen.
5890
 *
5891
 * - reads the contents of an http or ftp address and returns the page in a string
5892
 * - will accept up to 3 page redirections (redirectio_level in the function call is private)
5893
 * - if post_data is supplied, the value and length is posted to the given url as form data
5894
 * - URL must be supplied in lowercase
5895
 *
5896
 * @param string $url The URL
5897
 * @param string $post_data The data to post to the given URL
5898
 * @param bool $keep_alive Whether to send keepalive info
5899
 * @param int $redirection_level How many levels of redirection
5900
 * @return string|false The fetched data or false on failure
5901
 */
5902
function fetch_web_data($url, $post_data = '', $keep_alive = false, $redirection_level = 0)
5903
{
5904
	global $webmaster_email, $sourcedir, $txt;
5905
	static $keep_alive_dom = null, $keep_alive_fp = null;
5906
5907
	preg_match('~^(http|ftp)(s)?://([^/:]+)(:(\d+))?(.+)$~', iri_to_url($url), $match);
5908
5909
	// No scheme? No data for you!
5910
	if (empty($match[1]))
5911
		return false;
5912
5913
	// An FTP url. We should try connecting and RETRieving it...
5914
	elseif ($match[1] == 'ftp')
5915
	{
5916
		// Include the file containing the ftp_connection class.
5917
		require_once($sourcedir . '/Class-Package.php');
5918
5919
		// Establish a connection and attempt to enable passive mode.
5920
		$ftp = new ftp_connection(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? 21 : $match[5], 'anonymous', $webmaster_email);
5921
		if ($ftp->error !== false || !$ftp->passive())
0 ignored issues
show
introduced by
The condition $ftp->error !== false is always true.
Loading history...
5922
			return false;
5923
5924
		// I want that one *points*!
5925
		fwrite($ftp->connection, 'RETR ' . $match[6] . "\r\n");
5926
5927
		// Since passive mode worked (or we would have returned already!) open the connection.
5928
		$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...
5929
		if (!$fp)
5930
			return false;
5931
5932
		// The server should now say something in acknowledgement.
5933
		$ftp->check_response(150);
5934
5935
		$data = '';
5936
		while (!feof($fp))
5937
			$data .= fread($fp, 4096);
5938
		fclose($fp);
5939
5940
		// All done, right?  Good.
5941
		$ftp->check_response(226);
5942
		$ftp->close();
5943
	}
5944
5945
	// This is more likely; a standard HTTP URL.
5946
	elseif (isset($match[1]) && $match[1] == 'http')
5947
	{
5948
		// First try to use fsockopen, because it is fastest.
5949
		if ($keep_alive && $match[3] == $keep_alive_dom)
5950
			$fp = $keep_alive_fp;
5951
		if (empty($fp))
5952
		{
5953
			// Open the socket on the port we want...
5954
			$fp = @fsockopen(($match[2] ? 'ssl://' : '') . $match[3], empty($match[5]) ? ($match[2] ? 443 : 80) : $match[5], $err, $err, 5);
5955
		}
5956
		if (!empty($fp))
5957
		{
5958
			if ($keep_alive)
5959
			{
5960
				$keep_alive_dom = $match[3];
5961
				$keep_alive_fp = $fp;
5962
			}
5963
5964
			// I want this, from there, and I'm not going to be bothering you for more (probably.)
5965
			if (empty($post_data))
5966
			{
5967
				fwrite($fp, 'GET ' . ($match[6] !== '/' ? str_replace(' ', '%20', $match[6]) : '') . ' HTTP/1.0' . "\r\n");
5968
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5969
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5970
				if ($keep_alive)
5971
					fwrite($fp, 'connection: Keep-Alive' . "\r\n\r\n");
5972
				else
5973
					fwrite($fp, 'connection: close' . "\r\n\r\n");
5974
			}
5975
			else
5976
			{
5977
				fwrite($fp, 'POST ' . ($match[6] !== '/' ? $match[6] : '') . ' HTTP/1.0' . "\r\n");
5978
				fwrite($fp, 'Host: ' . $match[3] . (empty($match[5]) ? ($match[2] ? ':443' : '') : ':' . $match[5]) . "\r\n");
5979
				fwrite($fp, 'user-agent: '. SMF_USER_AGENT . "\r\n");
5980
				if ($keep_alive)
5981
					fwrite($fp, 'connection: Keep-Alive' . "\r\n");
5982
				else
5983
					fwrite($fp, 'connection: close' . "\r\n");
5984
				fwrite($fp, 'content-type: application/x-www-form-urlencoded' . "\r\n");
5985
				fwrite($fp, 'content-length: ' . strlen($post_data) . "\r\n\r\n");
5986
				fwrite($fp, $post_data);
5987
			}
5988
5989
			$response = fgets($fp, 768);
5990
5991
			// Redirect in case this location is permanently or temporarily moved.
5992
			if ($redirection_level < 3 && preg_match('~^HTTP/\S+\s+30[127]~i', $response) === 1)
5993
			{
5994
				$header = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $header is dead and can be removed.
Loading history...
5995
				$location = '';
5996
				while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
5997
					if (stripos($header, 'location:') !== false)
5998
						$location = trim(substr($header, strpos($header, ':') + 1));
5999
6000
				if (empty($location))
6001
					return false;
6002
				else
6003
				{
6004
					if (!$keep_alive)
6005
						fclose($fp);
6006
					return fetch_web_data($location, $post_data, $keep_alive, $redirection_level + 1);
6007
				}
6008
			}
6009
6010
			// Make sure we get a 200 OK.
6011
			elseif (preg_match('~^HTTP/\S+\s+20[01]~i', $response) === 0)
6012
				return false;
6013
6014
			// Skip the headers...
6015
			while (!feof($fp) && trim($header = fgets($fp, 4096)) != '')
6016
			{
6017
				if (preg_match('~content-length:\s*(\d+)~i', $header, $match) != 0)
6018
					$content_length = $match[1];
6019
				elseif (preg_match('~connection:\s*close~i', $header) != 0)
6020
				{
6021
					$keep_alive_dom = null;
6022
					$keep_alive = false;
6023
				}
6024
6025
				continue;
6026
			}
6027
6028
			$data = '';
6029
			if (isset($content_length))
6030
			{
6031
				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...
6032
					$data .= fread($fp, $content_length - strlen($data));
6033
			}
6034
			else
6035
			{
6036
				while (!feof($fp))
6037
					$data .= fread($fp, 4096);
6038
			}
6039
6040
			if (!$keep_alive)
6041
				fclose($fp);
6042
		}
6043
6044
		// If using fsockopen didn't work, try to use cURL if available.
6045
		elseif (function_exists('curl_init'))
6046
		{
6047
			// Include the file containing the curl_fetch_web_data class.
6048
			require_once($sourcedir . '/Class-CurlFetchWeb.php');
6049
6050
			$fetch_data = new curl_fetch_web_data();
6051
			$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

6051
			$fetch_data->get_url_data($url, /** @scrutinizer ignore-type */ $post_data);
Loading history...
6052
6053
			// no errors and a 200 result, then we have a good dataset, well we at least have data. ;)
6054
			if ($fetch_data->result('code') == 200 && !$fetch_data->result('error'))
6055
				$data = $fetch_data->result('body');
6056
			else
6057
				return false;
6058
		}
6059
6060
		// Neither fsockopen nor curl are available. Well, phooey.
6061
		else
6062
			return false;
6063
	}
6064
	else
6065
	{
6066
		// Umm, this shouldn't happen?
6067
		loadLanguage('Errors');
6068
		trigger_error($txt['fetch_web_data_bad_url'], E_USER_NOTICE);
6069
		$data = false;
6070
	}
6071
6072
	return $data;
6073
}
6074
6075
/**
6076
 * Attempts to determine the MIME type of some data or a file.
6077
 *
6078
 * @param string $data The data to check, or the path or URL of a file to check.
6079
 * @param string $is_path If true, $data is a path or URL to a file.
6080
 * @return string|bool A MIME type, or false if we cannot determine it.
6081
 */
6082
function get_mime_type($data, $is_path = false)
6083
{
6084
	global $cachedir;
6085
6086
	$finfo_loaded = extension_loaded('fileinfo');
6087
	$exif_loaded = extension_loaded('exif') && function_exists('image_type_to_mime_type');
6088
6089
	// Oh well. We tried.
6090
	if (!$finfo_loaded && !$exif_loaded)
6091
		return false;
6092
6093
	// Start with the 'empty' MIME type.
6094
	$mime_type = 'application/x-empty';
6095
6096
	if ($finfo_loaded)
6097
	{
6098
		// Just some nice, simple data to analyze.
6099
		if (empty($is_path))
6100
			$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
6101
6102
		// A file, or maybe a URL?
6103
		else
6104
		{
6105
			// Local file.
6106
			if (file_exists($data))
6107
				$mime_type = mime_content_type($data);
6108
6109
			// URL.
6110
			elseif ($data = fetch_web_data($data))
6111
				$mime_type = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data);
6112
		}
6113
	}
6114
	// Workaround using Exif requires a local file.
6115
	else
6116
	{
6117
		// If $data is a URL to fetch, do so.
6118
		if (!empty($is_path) && !file_exists($data) && url_exists($data))
6119
		{
6120
			$data = fetch_web_data($data);
6121
			$is_path = false;
6122
		}
6123
6124
		// If we don't have a local file, create one and use it.
6125
		if (empty($is_path))
6126
		{
6127
			$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

6127
			$temp_file = tempnam($cachedir, md5(/** @scrutinizer ignore-type */ $data));
Loading history...
6128
			file_put_contents($temp_file, $data);
6129
			$is_path = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $is_path is dead and can be removed.
Loading history...
6130
			$data = $temp_file;
6131
		}
6132
6133
		$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

6133
		$imagetype = @exif_imagetype(/** @scrutinizer ignore-type */ $data);
Loading history...
6134
6135
		if (isset($temp_file))
6136
			unlink($temp_file);
6137
6138
		// Unfortunately, this workaround only works for image files.
6139
		if ($imagetype !== false)
6140
			$mime_type = image_type_to_mime_type($imagetype);
6141
	}
6142
6143
	return $mime_type;
6144
}
6145
6146
/**
6147
 * Checks whether a file or data has the expected MIME type.
6148
 *
6149
 * @param string $data The data to check, or the path or URL of a file to check.
6150
 * @param string $type_pattern A regex pattern to match the acceptable MIME types.
6151
 * @param string $is_path If true, $data is a path or URL to a file.
6152
 * @return int 1 if the detected MIME type matches the pattern, 0 if it doesn't, or 2 if we can't check.
6153
 */
6154
function check_mime_type($data, $type_pattern, $is_path = false)
6155
{
6156
	// Get the MIME type.
6157
	$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

6157
	$mime_type = get_mime_type($data, /** @scrutinizer ignore-type */ $is_path);
Loading history...
6158
6159
	// Couldn't determine it.
6160
	if ($mime_type === false)
6161
		return 2;
6162
6163
	// Check whether the MIME type matches expectations.
6164
	return (int) @preg_match('~' . $type_pattern . '~', $mime_type);
6165
}
6166
6167
/**
6168
 * Prepares an array of "likes" info for the topic specified by $topic
6169
 *
6170
 * @param integer $topic The topic ID to fetch the info from.
6171
 * @return array An array of IDs of messages in the specified topic that the current user likes
6172
 */
6173
function prepareLikesContext($topic)
6174
{
6175
	global $user_info, $smcFunc;
6176
6177
	// Make sure we have something to work with.
6178
	if (empty($topic))
6179
		return array();
6180
6181
	// We already know the number of likes per message, we just want to know whether the current user liked it or not.
6182
	$user = $user_info['id'];
6183
	$cache_key = 'likes_topic_' . $topic . '_' . $user;
6184
	$ttl = 180;
6185
6186
	if (($temp = cache_get_data($cache_key, $ttl)) === null)
6187
	{
6188
		$temp = array();
6189
		$request = $smcFunc['db_query']('', '
6190
			SELECT content_id
6191
			FROM {db_prefix}user_likes AS l
6192
				INNER JOIN {db_prefix}messages AS m ON (l.content_id = m.id_msg)
6193
			WHERE l.id_member = {int:current_user}
6194
				AND l.content_type = {literal:msg}
6195
				AND m.id_topic = {int:topic}',
6196
			array(
6197
				'current_user' => $user,
6198
				'topic' => $topic,
6199
			)
6200
		);
6201
		while ($row = $smcFunc['db_fetch_assoc']($request))
6202
			$temp[] = (int) $row['content_id'];
6203
6204
		cache_put_data($cache_key, $temp, $ttl);
6205
	}
6206
6207
	return $temp;
6208
}
6209
6210
/**
6211
 * Microsoft uses their own character set Code Page 1252 (CP1252), which is a
6212
 * superset of ISO 8859-1, defining several characters between DEC 128 and 159
6213
 * that are not normally displayable.  This converts the popular ones that
6214
 * appear from a cut and paste from windows.
6215
 *
6216
 * @param string $string The string
6217
 * @return string The sanitized string
6218
 */
6219
function sanitizeMSCutPaste($string)
6220
{
6221
	global $context;
6222
6223
	if (empty($string))
6224
		return $string;
6225
6226
	// UTF-8 occurences of MS special characters
6227
	$findchars_utf8 = array(
6228
		"\xe2\x80\x9a",	// single low-9 quotation mark
6229
		"\xe2\x80\x9e",	// double low-9 quotation mark
6230
		"\xe2\x80\xa6",	// horizontal ellipsis
6231
		"\xe2\x80\x98",	// left single curly quote
6232
		"\xe2\x80\x99",	// right single curly quote
6233
		"\xe2\x80\x9c",	// left double curly quote
6234
		"\xe2\x80\x9d",	// right double curly quote
6235
	);
6236
6237
	// windows 1252 / iso equivalents
6238
	$findchars_iso = array(
6239
		chr(130),
6240
		chr(132),
6241
		chr(133),
6242
		chr(145),
6243
		chr(146),
6244
		chr(147),
6245
		chr(148),
6246
	);
6247
6248
	// safe replacements
6249
	$replacechars = array(
6250
		',',	// &sbquo;
6251
		',,',	// &bdquo;
6252
		'...',	// &hellip;
6253
		"'",	// &lsquo;
6254
		"'",	// &rsquo;
6255
		'"',	// &ldquo;
6256
		'"',	// &rdquo;
6257
	);
6258
6259
	if ($context['utf8'])
6260
		$string = str_replace($findchars_utf8, $replacechars, $string);
6261
	else
6262
		$string = str_replace($findchars_iso, $replacechars, $string);
6263
6264
	return $string;
6265
}
6266
6267
/**
6268
 * Decode numeric html entities to their ascii or UTF8 equivalent character.
6269
 *
6270
 * Callback function for preg_replace_callback in subs-members
6271
 * Uses capture group 2 in the supplied array
6272
 * Does basic scan to ensure characters are inside a valid range
6273
 *
6274
 * @param array $matches An array of matches (relevant info should be the 3rd item)
6275
 * @return string A fixed string
6276
 */
6277
function replaceEntities__callback($matches)
6278
{
6279
	global $context;
6280
6281
	if (!isset($matches[2]))
6282
		return '';
6283
6284
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
0 ignored issues
show
Bug introduced by
$matches[2] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

6284
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6285
6286
	// remove left to right / right to left overrides
6287
	if ($num === 0x202D || $num === 0x202E)
6288
		return '';
6289
6290
	// Quote, Ampersand, Apostrophe, Less/Greater Than get html replaced
6291
	if (in_array($num, array(0x22, 0x26, 0x27, 0x3C, 0x3E)))
6292
		return '&#' . $num . ';';
6293
6294
	if (empty($context['utf8']))
6295
	{
6296
		// no control characters
6297
		if ($num < 0x20)
6298
			return '';
6299
		// text is text
6300
		elseif ($num < 0x80)
6301
			return chr($num);
0 ignored issues
show
Bug introduced by
It seems like $num can also be of type double; however, parameter $codepoint of chr() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

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

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

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

6304
			return '&#' . /** @scrutinizer ignore-type */ $matches[2] . ';';
Loading history...
6305
	}
6306
	else
6307
	{
6308
		// <0x20 are control characters, 0x20 is a space, > 0x10FFFF is past the end of the utf8 character set
6309
		// 0xD800 >= $num <= 0xDFFF are surrogate markers (not valid for utf8 text)
6310
		if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF))
6311
			return '';
6312
		// <0x80 (or less than 128) are standard ascii characters a-z A-Z 0-9 and punctuation
6313
		elseif ($num < 0x80)
6314
			return chr($num);
6315
		// <0x800 (2048)
6316
		elseif ($num < 0x800)
6317
			return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6318
		// < 0x10000 (65536)
6319
		elseif ($num < 0x10000)
6320
			return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6321
		// <= 0x10FFFF (1114111)
6322
		else
6323
			return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6324
	}
6325
}
6326
6327
/**
6328
 * Converts html entities to utf8 equivalents
6329
 *
6330
 * Callback function for preg_replace_callback
6331
 * Uses capture group 1 in the supplied array
6332
 * Does basic checks to keep characters inside a viewable range.
6333
 *
6334
 * @param array $matches An array of matches (relevant info should be the 2nd item in the array)
6335
 * @return string The fixed string
6336
 */
6337
function fixchar__callback($matches)
6338
{
6339
	if (!isset($matches[1]))
6340
		return '';
6341
6342
	$num = $matches[1][0] === 'x' ? hexdec(substr($matches[1], 1)) : (int) $matches[1];
0 ignored issues
show
Bug introduced by
$matches[1] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

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

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

6350
		return chr(/** @scrutinizer ignore-type */ $num);
Loading history...
6351
	// <0x800 (2048)
6352
	elseif ($num < 0x800)
6353
		return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
6354
	// < 0x10000 (65536)
6355
	elseif ($num < 0x10000)
6356
		return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6357
	// <= 0x10FFFF (1114111)
6358
	else
6359
		return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
6360
}
6361
6362
/**
6363
 * Strips out invalid html entities, replaces others with html style &#123; codes
6364
 *
6365
 * Callback function used of preg_replace_callback in smcFunc $ent_checks, for example
6366
 * strpos, strlen, substr etc
6367
 *
6368
 * @param array $matches An array of matches (relevant info should be the 3rd item in the array)
6369
 * @return string The fixed string
6370
 */
6371
function entity_fix__callback($matches)
6372
{
6373
	if (!isset($matches[2]))
6374
		return '';
6375
6376
	$num = $matches[2][0] === 'x' ? hexdec(substr($matches[2], 1)) : (int) $matches[2];
0 ignored issues
show
Bug introduced by
$matches[2] of type array is incompatible with the type string expected by parameter $string of substr(). ( Ignorable by Annotation )

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

6376
	$num = $matches[2][0] === 'x' ? hexdec(substr(/** @scrutinizer ignore-type */ $matches[2], 1)) : (int) $matches[2];
Loading history...
6377
6378
	// we don't allow control characters, characters out of range, byte markers, etc
6379
	if ($num < 0x20 || $num > 0x10FFFF || ($num >= 0xD800 && $num <= 0xDFFF) || $num == 0x202D || $num == 0x202E)
6380
		return '';
6381
	else
6382
		return '&#' . $num . ';';
6383
}
6384
6385
/**
6386
 * Return a Gravatar URL based on
6387
 * - the supplied email address,
6388
 * - the global maximum rating,
6389
 * - the global default fallback,
6390
 * - maximum sizes as set in the admin panel.
6391
 *
6392
 * It is SSL aware, and caches most of the parameters.
6393
 *
6394
 * @param string $email_address The user's email address
6395
 * @return string The gravatar URL
6396
 */
6397
function get_gravatar_url($email_address)
6398
{
6399
	global $modSettings, $smcFunc;
6400
	static $url_params = null;
6401
6402
	if ($url_params === null)
6403
	{
6404
		$ratings = array('G', 'PG', 'R', 'X');
6405
		$defaults = array('mm', 'identicon', 'monsterid', 'wavatar', 'retro', 'blank');
6406
		$url_params = array();
6407
		if (!empty($modSettings['gravatarMaxRating']) && in_array($modSettings['gravatarMaxRating'], $ratings))
6408
			$url_params[] = 'rating=' . $modSettings['gravatarMaxRating'];
6409
		if (!empty($modSettings['gravatarDefault']) && in_array($modSettings['gravatarDefault'], $defaults))
6410
			$url_params[] = 'default=' . $modSettings['gravatarDefault'];
6411
		if (!empty($modSettings['avatar_max_width_external']))
6412
			$size_string = (int) $modSettings['avatar_max_width_external'];
6413
		if (!empty($modSettings['avatar_max_height_external']) && !empty($size_string))
6414
			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...
6415
				$size_string = $modSettings['avatar_max_height_external'];
6416
6417
		if (!empty($size_string))
6418
			$url_params[] = 's=' . $size_string;
6419
	}
6420
	$http_method = !empty($modSettings['force_ssl']) ? 'https://secure' : 'http://www';
6421
6422
	return $http_method . '.gravatar.com/avatar/' . md5($smcFunc['strtolower']($email_address)) . '?' . implode('&', $url_params);
6423
}
6424
6425
/**
6426
 * Get a list of time zones.
6427
 *
6428
 * @param string $when The date/time for which to calculate the time zone values.
6429
 *		May be a Unix timestamp or any string that strtotime() can understand.
6430
 *		Defaults to 'now'.
6431
 * @return array An array of time zone identifiers and label text.
6432
 */
6433
function smf_list_timezones($when = 'now')
6434
{
6435
	global $modSettings, $tztxt, $txt, $context, $cur_profile, $sourcedir;
6436
	static $timezones_when = array();
6437
6438
	require_once($sourcedir . '/Subs-Timezones.php');
6439
6440
	// Parseable datetime string?
6441
	if (is_int($timestamp = strtotime($when)))
0 ignored issues
show
introduced by
The condition is_int($timestamp = strtotime($when)) is always true.
Loading history...
6442
		$when = $timestamp;
6443
6444
	// A Unix timestamp?
6445
	elseif (is_numeric($when))
6446
		$when = intval($when);
6447
6448
	// Invalid value? Just get current Unix timestamp.
6449
	else
6450
		$when = time();
6451
6452
	// No point doing this over if we already did it once
6453
	if (isset($timezones_when[$when]))
6454
		return $timezones_when[$when];
6455
6456
	// We'll need these too
6457
	$date_when = date_create('@' . $when);
6458
	$later = strtotime('@' . $when . ' + 1 year');
6459
6460
	// Load up any custom time zone descriptions we might have
6461
	loadLanguage('Timezones');
6462
6463
	$tzid_metazones = get_tzid_metazones($later);
6464
6465
	// Should we put time zones from certain countries at the top of the list?
6466
	$priority_countries = !empty($modSettings['timezone_priority_countries']) ? explode(',', $modSettings['timezone_priority_countries']) : array();
6467
6468
	$priority_tzids = array();
6469
	foreach ($priority_countries as $country)
6470
	{
6471
		$country_tzids = get_sorted_tzids_for_country($country);
6472
6473
		if (!empty($country_tzids))
6474
			$priority_tzids = array_merge($priority_tzids, $country_tzids);
6475
	}
6476
6477
	// Antarctic research stations should be listed last, unless you're running a penguin forum
6478
	$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...
6479
6480
	$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

6480
	$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...
6481
6482
	// Process them in order of importance.
6483
	$tzids = array_merge($priority_tzids, $normal_priority_tzids, $low_priority_tzids);
6484
6485
	// Idea here is to get exactly one representative identifier for each and every unique set of time zone rules.
6486
	$dst_types = array();
6487
	$labels = array();
6488
	$offsets = array();
6489
	foreach ($tzids as $tzid)
6490
	{
6491
		// We don't want UTC right now
6492
		if ($tzid == 'UTC')
6493
			continue;
6494
6495
		$tz = @timezone_open($tzid);
6496
6497
		if ($tz == null)
6498
			continue;
6499
6500
		// First, get the set of transition rules for this tzid
6501
		$tzinfo = timezone_transitions_get($tz, $when, $later);
6502
6503
		// Use the entire set of transition rules as the array *key* so we can avoid duplicates
6504
		$tzkey = serialize($tzinfo);
6505
6506
		// ...But make sure to include all explicitly defined meta-zones.
6507
		if (isset($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6508
			$tzkey = serialize(array_merge($tzinfo, array('metazone' => $tzid_metazones[$tzid])));
6509
6510
		// Don't overwrite our preferred tzids
6511
		if (empty($zones[$tzkey]['tzid']))
6512
		{
6513
			$zones[$tzkey]['tzid'] = $tzid;
6514
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6515
6516
			foreach ($tzinfo as $transition) {
6517
				$zones[$tzkey]['abbrs'][] = $transition['abbr'];
6518
			}
6519
6520
			if (isset($tzid_metazones[$tzid]))
6521
				$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6522
			else
6523
			{
6524
				$tzgeo = timezone_location_get($tz);
6525
				$country_tzids = get_sorted_tzids_for_country($tzgeo['country_code']);
6526
6527
				if (count($country_tzids) === 1)
6528
					$zones[$tzkey]['metazone'] = $txt['iso3166'][$tzgeo['country_code']];
6529
			}
6530
		}
6531
6532
		// A time zone from a prioritized country?
6533
		if (in_array($tzid, $priority_tzids))
6534
			$priority_zones[$tzkey] = true;
6535
6536
		// Keep track of the location for this tzid.
6537
		if (!empty($txt[$tzid]))
6538
			$zones[$tzkey]['locations'][] = $txt[$tzid];
6539
		else
6540
		{
6541
			$tzid_parts = explode('/', $tzid);
6542
			$zones[$tzkey]['locations'][] = str_replace(array('St_', '_'), array('St. ', ' '), array_pop($tzid_parts));
6543
		}
6544
6545
		// Keep track of the current offset for this tzid.
6546
		$offsets[$tzkey] = $tzinfo[0]['offset'];
6547
6548
		// Keep track of the Standard Time offset for this tzid.
6549
		foreach ($tzinfo as $transition)
6550
		{
6551
			if (!$transition['isdst'])
6552
			{
6553
				$std_offsets[$tzkey] = $transition['offset'];
6554
				break;
6555
			}
6556
		}
6557
		if (!isset($std_offsets[$tzkey]))
6558
			$std_offsets[$tzkey] = $tzinfo[0]['offset'];
6559
6560
		// Figure out the "meta-zone" info for the label
6561
		if (empty($zones[$tzkey]['metazone']) && isset($tzid_metazones[$tzid]))
6562
		{
6563
			$zones[$tzkey]['metazone'] = $tzid_metazones[$tzid];
6564
			$zones[$tzkey]['dst_type'] = count($tzinfo) > 1 ? 1 : ($tzinfo[0]['isdst'] ? 2 : 0);
6565
		}
6566
		$dst_types[$tzkey] = count($tzinfo) > 1 ? 'c' : ($tzinfo[0]['isdst'] ? 't' : 'f');
6567
		$labels[$tzkey] = !empty($zones[$tzkey]['metazone']) && !empty($tztxt[$zones[$tzkey]['metazone']]) ? $tztxt[$zones[$tzkey]['metazone']] : '';
6568
6569
		// Remember this for later
6570
		if (isset($cur_profile['timezone']) && $cur_profile['timezone'] == $tzid)
6571
			$member_tzkey = $tzkey;
6572
		if (isset($context['event']['tz']) && $context['event']['tz'] == $tzid)
6573
			$event_tzkey = $tzkey;
6574
	}
6575
6576
	// Sort by current offset, then standard offset, then DST type, then label.
6577
	array_multisort($offsets, SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
0 ignored issues
show
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

6577
	array_multisort($offsets, SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, /** @scrutinizer ignore-type */ SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $zones does not seem to be defined for all execution paths leading up to this point.
Loading history...
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

6577
	array_multisort($offsets, SORT_DESC, /** @scrutinizer ignore-type */ SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Bug introduced by
SORT_DESC 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

6577
	array_multisort($offsets, /** @scrutinizer ignore-type */ SORT_DESC, SORT_NUMERIC, $std_offsets, SORT_DESC, SORT_NUMERIC, $dst_types, SORT_ASC, $labels, SORT_ASC, $zones);
Loading history...
Comprehensibility Best Practice introduced by
The variable $std_offsets does not seem to be defined for all execution paths leading up to this point.
Loading history...
6578
6579
	// Build the final array of formatted values
6580
	$priority_timezones = array();
6581
	$timezones = array();
6582
	foreach ($zones as $tzkey => $tzvalue)
6583
	{
6584
		date_timezone_set($date_when, timezone_open($tzvalue['tzid']));
6585
6586
		// Use the human friendly time zone name, if there is one.
6587
		$desc = '';
6588
		if (!empty($tzvalue['metazone']))
6589
		{
6590
			if (!empty($tztxt[$tzvalue['metazone']]))
6591
				$metazone = $tztxt[$tzvalue['metazone']];
6592
			else
6593
				$metazone = sprintf($tztxt['generic_timezone'], $tzvalue['metazone'], '%1$s');
6594
6595
			switch ($tzvalue['dst_type'])
6596
			{
6597
				case 0:
6598
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_false']);
6599
					break;
6600
6601
				case 1:
6602
					$desc = sprintf($metazone, '');
6603
					break;
6604
6605
				case 2:
6606
					$desc = sprintf($metazone, $tztxt['daylight_saving_time_true']);
6607
					break;
6608
			}
6609
		}
6610
		// Otherwise, use the list of locations (max 5, so things don't get silly)
6611
		else
6612
			$desc = implode(', ', array_slice(array_unique($tzvalue['locations']), 0, 5)) . (count($tzvalue['locations']) > 5 ? ', ' . $txt['etc'] : '');
6613
6614
		// We don't want abbreviations like '+03' or '-11'.
6615
		$abbrs = array_filter(
6616
			$tzvalue['abbrs'],
6617
			function ($abbr)
6618
			{
6619
				return !strspn($abbr, '+-');
6620
			}
6621
		);
6622
		$abbrs = count($abbrs) == count($tzvalue['abbrs']) ? array_unique($abbrs) : array();
6623
6624
		// Show the UTC offset and abbreviation(s).
6625
		$desc = '[UTC' . date_format($date_when, 'P') . '] - ' . str_replace('  ', ' ', $desc) . (!empty($abbrs) ? ' (' . implode('/', $abbrs) . ')' : '');
6626
6627
		if (isset($priority_zones[$tzkey]))
6628
			$priority_timezones[$tzvalue['tzid']] = $desc;
6629
		else
6630
			$timezones[$tzvalue['tzid']] = $desc;
6631
6632
		// Automatically fix orphaned time zones.
6633
		if (isset($member_tzkey) && $member_tzkey == $tzkey)
6634
			$cur_profile['timezone'] = $tzvalue['tzid'];
6635
		if (isset($event_tzkey) && $event_tzkey == $tzkey)
6636
			$context['event']['tz'] = $tzvalue['tzid'];
6637
	}
6638
6639
	if (!empty($priority_timezones))
6640
		$priority_timezones[] = '-----';
6641
6642
	$timezones = array_merge(
6643
		$priority_timezones,
6644
		array('UTC' => 'UTC' . (!empty($tztxt['UTC']) ? ' - ' . $tztxt['UTC'] : ''), '-----'),
6645
		$timezones
6646
	);
6647
6648
	$timezones_when[$when] = $timezones;
6649
6650
	return $timezones_when[$when];
6651
}
6652
6653
/**
6654
 * Gets a member's selected time zone identifier
6655
 *
6656
 * @param int $id_member The member id to look up. If not provided, the current user's id will be used.
6657
 * @return string The time zone identifier string for the user's time zone.
6658
 */
6659
function getUserTimezone($id_member = null)
6660
{
6661
	global $smcFunc, $context, $user_info, $modSettings, $user_settings;
6662
	static $member_cache = array();
6663
6664
	if (is_null($id_member) && $user_info['is_guest'] == false)
6665
		$id_member = $context['user']['id'];
6666
6667
	// Did we already look this up?
6668
	if (isset($id_member) && isset($member_cache[$id_member]))
6669
	{
6670
		return $member_cache[$id_member];
6671
	}
6672
6673
	// Check if we already have this in $user_settings.
6674
	if (isset($user_settings['id_member']) && $user_settings['id_member'] == $id_member && !empty($user_settings['timezone']))
6675
	{
6676
		$member_cache[$id_member] = $user_settings['timezone'];
6677
		return $user_settings['timezone'];
6678
	}
6679
6680
	// Look it up in the database.
6681
	if (isset($id_member))
6682
	{
6683
		$request = $smcFunc['db_query']('', '
6684
			SELECT timezone
6685
			FROM {db_prefix}members
6686
			WHERE id_member = {int:id_member}',
6687
			array(
6688
				'id_member' => $id_member,
6689
			)
6690
		);
6691
		list($timezone) = $smcFunc['db_fetch_row']($request);
6692
		$smcFunc['db_free_result']($request);
6693
	}
6694
6695
	// If it is invalid, fall back to the default.
6696
	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

6696
	if (empty($timezone) || !in_array($timezone, /** @scrutinizer ignore-type */ timezone_identifiers_list(DateTimeZone::ALL_WITH_BC)))
Loading history...
6697
		$timezone = isset($modSettings['default_timezone']) ? $modSettings['default_timezone'] : date_default_timezone_get();
6698
6699
	// Save for later.
6700
	if (isset($id_member))
6701
		$member_cache[$id_member] = $timezone;
6702
6703
	return $timezone;
6704
}
6705
6706
/**
6707
 * Converts an IP address into binary
6708
 *
6709
 * @param string $ip_address An IP address in IPv4, IPv6 or decimal notation
6710
 * @return string|false The IP address in binary or false
6711
 */
6712
function inet_ptod($ip_address)
6713
{
6714
	if (!isValidIP($ip_address))
6715
		return $ip_address;
6716
6717
	$bin = inet_pton($ip_address);
6718
	return $bin;
6719
}
6720
6721
/**
6722
 * Converts a binary version of an IP address into a readable format
6723
 *
6724
 * @param string $bin An IP address in IPv4, IPv6 (Either string (postgresql) or binary (other databases))
6725
 * @return string|false The IP address in presentation format or false on error
6726
 */
6727
function inet_dtop($bin)
6728
{
6729
	global $db_type;
6730
6731
	if (empty($bin))
6732
		return '';
6733
	elseif ($db_type == 'postgresql')
6734
		return $bin;
6735
	// Already a String?
6736
	elseif (isValidIP($bin))
6737
		return $bin;
6738
	return inet_ntop($bin);
6739
}
6740
6741
/**
6742
 * Safe serialize() and unserialize() replacements
6743
 *
6744
 * @license Public Domain
6745
 *
6746
 * @author anthon (dot) pang (at) gmail (dot) com
6747
 */
6748
6749
/**
6750
 * Safe serialize() replacement. Recursive
6751
 * - output a strict subset of PHP's native serialized representation
6752
 * - does not serialize objects
6753
 *
6754
 * @param mixed $value
6755
 * @return string
6756
 */
6757
function _safe_serialize($value)
6758
{
6759
	if (is_null($value))
6760
		return 'N;';
6761
6762
	if (is_bool($value))
6763
		return 'b:' . (int) $value . ';';
6764
6765
	if (is_int($value))
6766
		return 'i:' . $value . ';';
6767
6768
	if (is_float($value))
6769
		return 'd:' . str_replace(',', '.', $value) . ';';
6770
6771
	if (is_string($value))
6772
		return 's:' . strlen($value) . ':"' . $value . '";';
6773
6774
	if (is_array($value))
6775
	{
6776
		// Check for nested objects or resources.
6777
		$contains_invalid = false;
6778
		array_walk_recursive(
6779
			$value,
6780
			function($v) use (&$contains_invalid)
6781
			{
6782
				if (is_object($v) || is_resource($v))
6783
					$contains_invalid = true;
6784
			}
6785
		);
6786
		if ($contains_invalid)
6787
			return false;
6788
6789
		$out = '';
6790
		foreach ($value as $k => $v)
6791
			$out .= _safe_serialize($k) . _safe_serialize($v);
6792
6793
		return 'a:' . count($value) . ':{' . $out . '}';
6794
	}
6795
6796
	// safe_serialize cannot serialize resources or objects.
6797
	return false;
6798
}
6799
6800
/**
6801
 * Wrapper for _safe_serialize() that handles exceptions and multibyte encoding issues.
6802
 *
6803
 * @param mixed $value
6804
 * @return string
6805
 */
6806
function safe_serialize($value)
6807
{
6808
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6809
	if (function_exists('mb_internal_encoding') &&
6810
		(((int) ini_get('mbstring.func_overload')) & 2))
6811
	{
6812
		$mbIntEnc = mb_internal_encoding();
6813
		mb_internal_encoding('ASCII');
6814
	}
6815
6816
	$out = _safe_serialize($value);
6817
6818
	if (isset($mbIntEnc))
6819
		mb_internal_encoding($mbIntEnc);
6820
6821
	return $out;
6822
}
6823
6824
/**
6825
 * Safe unserialize() replacement
6826
 * - accepts a strict subset of PHP's native serialized representation
6827
 * - does not unserialize objects
6828
 *
6829
 * @param string $str
6830
 * @return mixed
6831
 * @throw Exception if $str is malformed or contains unsupported types (e.g., resources, objects)
6832
 */
6833
function _safe_unserialize($str)
6834
{
6835
	// Input  is not a string.
6836
	if (empty($str) || !is_string($str))
6837
		return false;
6838
6839
	// The substring 'O:' is used to serialize objects.
6840
	// If it is not present, then there are none in the serialized data.
6841
	if (strpos($str, 'O:') === false)
6842
		return unserialize($str);
6843
6844
	$stack = array();
6845
	$expected = array();
6846
6847
	/*
6848
	 * states:
6849
	 *   0 - initial state, expecting a single value or array
6850
	 *   1 - terminal state
6851
	 *   2 - in array, expecting end of array or a key
6852
	 *   3 - in array, expecting value or another array
6853
	 */
6854
	$state = 0;
6855
	while ($state != 1)
6856
	{
6857
		$type = isset($str[0]) ? $str[0] : '';
6858
		if ($type == '}')
6859
			$str = substr($str, 1);
6860
6861
		elseif ($type == 'N' && $str[1] == ';')
6862
		{
6863
			$value = null;
6864
			$str = substr($str, 2);
6865
		}
6866
		elseif ($type == 'b' && preg_match('/^b:([01]);/', $str, $matches))
6867
		{
6868
			$value = $matches[1] == '1' ? true : false;
6869
			$str = substr($str, 4);
6870
		}
6871
		elseif ($type == 'i' && preg_match('/^i:(-?[0-9]+);(.*)/s', $str, $matches))
6872
		{
6873
			$value = (int) $matches[1];
6874
			$str = $matches[2];
6875
		}
6876
		elseif ($type == 'd' && preg_match('/^d:(-?[0-9]+\.?[0-9]*(E[+-][0-9]+)?);(.*)/s', $str, $matches))
6877
		{
6878
			$value = (float) $matches[1];
6879
			$str = $matches[3];
6880
		}
6881
		elseif ($type == 's' && preg_match('/^s:([0-9]+):"(.*)/s', $str, $matches) && substr($matches[2], (int) $matches[1], 2) == '";')
6882
		{
6883
			$value = substr($matches[2], 0, (int) $matches[1]);
6884
			$str = substr($matches[2], (int) $matches[1] + 2);
6885
		}
6886
		elseif ($type == 'a' && preg_match('/^a:([0-9]+):{(.*)/s', $str, $matches))
6887
		{
6888
			$expectedLength = (int) $matches[1];
6889
			$str = $matches[2];
6890
		}
6891
6892
		// Object or unknown/malformed type.
6893
		else
6894
			return false;
6895
6896
		switch ($state)
6897
		{
6898
			case 3: // In array, expecting value or another array.
6899
				if ($type == 'a')
6900
				{
6901
					$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...
6902
					$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...
6903
					$list = &$list[$key];
6904
					$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...
6905
					$state = 2;
6906
					break;
6907
				}
6908
				if ($type != '}')
6909
				{
6910
					$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...
6911
					$state = 2;
6912
					break;
6913
				}
6914
6915
				// Missing array value.
6916
				return false;
6917
6918
			case 2: // in array, expecting end of array or a key
6919
				if ($type == '}')
6920
				{
6921
					// Array size is less than expected.
6922
					if (count($list) < end($expected))
6923
						return false;
6924
6925
					unset($list);
6926
					$list = &$stack[count($stack) - 1];
6927
					array_pop($stack);
6928
6929
					// Go to terminal state if we're at the end of the root array.
6930
					array_pop($expected);
6931
6932
					if (count($expected) == 0)
6933
						$state = 1;
6934
6935
					break;
6936
				}
6937
6938
				if ($type == 'i' || $type == 's')
6939
				{
6940
					// Array size exceeds expected length.
6941
					if (count($list) >= end($expected))
6942
						return false;
6943
6944
					$key = $value;
6945
					$state = 3;
6946
					break;
6947
				}
6948
6949
				// Illegal array index type.
6950
				return false;
6951
6952
			// Expecting array or value.
6953
			case 0:
6954
				if ($type == 'a')
6955
				{
6956
					$data = array();
6957
					$list = &$data;
6958
					$expected[] = $expectedLength;
6959
					$state = 2;
6960
					break;
6961
				}
6962
6963
				if ($type != '}')
6964
				{
6965
					$data = $value;
6966
					$state = 1;
6967
					break;
6968
				}
6969
6970
				// Not in array.
6971
				return false;
6972
		}
6973
	}
6974
6975
	// Trailing data in input.
6976
	if (!empty($str))
6977
		return false;
6978
6979
	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...
6980
}
6981
6982
/**
6983
 * Wrapper for _safe_unserialize() that handles exceptions and multibyte encoding issue
6984
 *
6985
 * @param string $str
6986
 * @return mixed
6987
 */
6988
function safe_unserialize($str)
6989
{
6990
	// Make sure we use the byte count for strings even when strlen() is overloaded by mb_strlen()
6991
	if (function_exists('mb_internal_encoding') &&
6992
		(((int) ini_get('mbstring.func_overload')) & 0x02))
6993
	{
6994
		$mbIntEnc = mb_internal_encoding();
6995
		mb_internal_encoding('ASCII');
6996
	}
6997
6998
	$out = _safe_unserialize($str);
6999
7000
	if (isset($mbIntEnc))
7001
		mb_internal_encoding($mbIntEnc);
7002
7003
	return $out;
7004
}
7005
7006
/**
7007
 * Tries different modes to make file/dirs writable. Wrapper function for chmod()
7008
 *
7009
 * @param string $file The file/dir full path.
7010
 * @param int $value Not needed, added for legacy reasons.
7011
 * @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.
7012
 */
7013
function smf_chmod($file, $value = 0)
7014
{
7015
	// No file? no checks!
7016
	if (empty($file))
7017
		return false;
7018
7019
	// Already writable?
7020
	if (is_writable($file))
7021
		return true;
7022
7023
	// Do we have a file or a dir?
7024
	$isDir = is_dir($file);
7025
	$isWritable = false;
7026
7027
	// Set different modes.
7028
	$chmodValues = $isDir ? array(0750, 0755, 0775, 0777) : array(0644, 0664, 0666);
7029
7030
	foreach ($chmodValues as $val)
7031
	{
7032
		// If it's writable, break out of the loop.
7033
		if (is_writable($file))
7034
		{
7035
			$isWritable = true;
7036
			break;
7037
		}
7038
7039
		else
7040
			@chmod($file, $val);
7041
	}
7042
7043
	return $isWritable;
7044
}
7045
7046
/**
7047
 * Wrapper function for json_decode() with error handling.
7048
 *
7049
 * @param string $json The string to decode.
7050
 * @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.
7051
 * @param bool $logIt To specify if the error will be logged if theres any.
7052
 * @return array Either an empty array or the decoded data as an array.
7053
 */
7054
function smf_json_decode($json, $returnAsArray = false, $logIt = true)
7055
{
7056
	global $txt;
7057
7058
	// Come on...
7059
	if (empty($json) || !is_string($json))
7060
		return array();
7061
7062
	$returnArray = @json_decode($json, $returnAsArray);
7063
7064
	// PHP 5.3 so no json_last_error_msg()
7065
	switch (json_last_error())
7066
	{
7067
		case JSON_ERROR_NONE:
7068
			$jsonError = false;
7069
			break;
7070
		case JSON_ERROR_DEPTH:
7071
			$jsonError = 'JSON_ERROR_DEPTH';
7072
			break;
7073
		case JSON_ERROR_STATE_MISMATCH:
7074
			$jsonError = 'JSON_ERROR_STATE_MISMATCH';
7075
			break;
7076
		case JSON_ERROR_CTRL_CHAR:
7077
			$jsonError = 'JSON_ERROR_CTRL_CHAR';
7078
			break;
7079
		case JSON_ERROR_SYNTAX:
7080
			$jsonError = 'JSON_ERROR_SYNTAX';
7081
			break;
7082
		case JSON_ERROR_UTF8:
7083
			$jsonError = 'JSON_ERROR_UTF8';
7084
			break;
7085
		default:
7086
			$jsonError = 'unknown';
7087
			break;
7088
	}
7089
7090
	// Something went wrong!
7091
	if (!empty($jsonError) && $logIt)
7092
	{
7093
		// Being a wrapper means we lost our smf_error_handler() privileges :(
7094
		$jsonDebug = debug_backtrace();
7095
		$jsonDebug = $jsonDebug[0];
7096
		loadLanguage('Errors');
7097
7098
		if (!empty($jsonDebug))
7099
			log_error($txt['json_' . $jsonError], 'critical', $jsonDebug['file'], $jsonDebug['line']);
7100
7101
		else
7102
			log_error($txt['json_' . $jsonError], 'critical');
7103
7104
		// Everyone expects an array.
7105
		return array();
7106
	}
7107
7108
	return $returnArray;
7109
}
7110
7111
/**
7112
 * Check the given String if he is a valid IPv4 or IPv6
7113
 * return true or false
7114
 *
7115
 * @param string $IPString
7116
 *
7117
 * @return bool
7118
 */
7119
function isValidIP($IPString)
7120
{
7121
	return filter_var($IPString, FILTER_VALIDATE_IP) !== false;
7122
}
7123
7124
/**
7125
 * Outputs a response.
7126
 * It assumes the data is already a string.
7127
 *
7128
 * @param string $data The data to print
7129
 * @param string $type The content type. Defaults to Json.
7130
 * @return void
7131
 */
7132
function smf_serverResponse($data = '', $type = 'content-type: application/json')
7133
{
7134
	global $db_show_debug, $modSettings;
7135
7136
	// Defensive programming anyone?
7137
	if (empty($data))
7138
		return false;
7139
7140
	// Don't need extra stuff...
7141
	$db_show_debug = false;
7142
7143
	// Kill anything else.
7144
	ob_end_clean();
7145
7146
	if (!empty($modSettings['CompressedOutput']))
7147
		@ob_start('ob_gzhandler');
7148
7149
	else
7150
		ob_start();
7151
7152
	// Set the header.
7153
	header($type);
7154
7155
	// Echo!
7156
	echo $data;
7157
7158
	// Done.
7159
	obExit(false);
7160
}
7161
7162
/**
7163
 * Creates an optimized regex to match all known top level domains.
7164
 *
7165
 * The optimized regex is stored in $modSettings['tld_regex'].
7166
 *
7167
 * To update the stored version of the regex to use the latest list of valid
7168
 * TLDs from iana.org, set the $update parameter to true. Updating can take some
7169
 * time, based on network connectivity, so it should normally only be done by
7170
 * calling this function from a background or scheduled task.
7171
 *
7172
 * If $update is not true, but the regex is missing or invalid, the regex will
7173
 * be regenerated from a hard-coded list of TLDs. This regenerated regex will be
7174
 * overwritten on the next scheduled update.
7175
 *
7176
 * @param bool $update If true, fetch and process the latest official list of TLDs from iana.org.
7177
 */
7178
function set_tld_regex($update = false)
7179
{
7180
	global $sourcedir, $smcFunc, $modSettings;
7181
	static $done = false;
7182
7183
	// If we don't need to do anything, don't
7184
	if (!$update && $done)
7185
		return;
7186
7187
	// Should we get a new copy of the official list of TLDs?
7188
	if ($update)
7189
	{
7190
		$tlds = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt');
7191
		$tlds_md5 = fetch_web_data('https://data.iana.org/TLD/tlds-alpha-by-domain.txt.md5');
7192
7193
		/**
7194
		 * If the Internet Assigned Numbers Authority can't be reached, the Internet is GONE!
7195
		 * We're probably running on a server hidden in a bunker deep underground to protect
7196
		 * it from marauding bandits roaming on the surface. We don't want to waste precious
7197
		 * electricity on pointlessly repeating background tasks, so we'll wait until the next
7198
		 * regularly scheduled update to see if civilization has been restored.
7199
		 */
7200
		if ($tlds === false || $tlds_md5 === false)
7201
			$postapocalypticNightmare = true;
7202
7203
		// Make sure nothing went horribly wrong along the way.
7204
		if (md5($tlds) != substr($tlds_md5, 0, 32))
0 ignored issues
show
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

7204
		if (md5($tlds) != substr(/** @scrutinizer ignore-type */ $tlds_md5, 0, 32))
Loading history...
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

7204
		if (md5(/** @scrutinizer ignore-type */ $tlds) != substr($tlds_md5, 0, 32))
Loading history...
7205
			$tlds = array();
7206
	}
7207
	// If we aren't updating and the regex is valid, we're done
7208
	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

7208
	elseif (!empty($modSettings['tld_regex']) && @preg_match('~' . $modSettings['tld_regex'] . '~', /** @scrutinizer ignore-type */ null) !== false)
Loading history...
7209
	{
7210
		$done = true;
7211
		return;
7212
	}
7213
7214
	// If we successfully got an update, process the list into an array
7215
	if (!empty($tlds))
7216
	{
7217
		// Clean $tlds and convert it to an array
7218
		$tlds = array_filter(
7219
			explode("\n", strtolower($tlds)),
7220
			function($line)
7221
			{
7222
				$line = trim($line);
7223
				if (empty($line) || strlen($line) != strspn($line, 'abcdefghijklmnopqrstuvwxyz0123456789-'))
7224
					return false;
7225
				else
7226
					return true;
7227
			}
7228
		);
7229
7230
		// Convert Punycode to Unicode
7231
		if (!function_exists('idn_to_utf8'))
7232
			require_once($sourcedir . '/Subs-Compat.php');
7233
7234
		foreach ($tlds as &$tld)
7235
			$tld = idn_to_utf8($tld, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
7236
	}
7237
	// Otherwise, use the 2012 list of gTLDs and ccTLDs for now and schedule a background update
7238
	else
7239
	{
7240
		$tlds = array('com', 'net', 'org', 'edu', 'gov', 'mil', 'aero', 'asia', 'biz',
7241
			'cat', 'coop', 'info', 'int', 'jobs', 'mobi', 'museum', 'name', 'post',
7242
			'pro', 'tel', 'travel', 'xxx', 'ac', 'ad', 'ae', 'af', 'ag', 'ai', 'al',
7243
			'am', 'ao', 'aq', 'ar', 'as', 'at', 'au', 'aw', 'ax', 'az', 'ba', 'bb', 'bd',
7244
			'be', 'bf', 'bg', 'bh', 'bi', 'bj', 'bm', 'bn', 'bo', 'br', 'bs', 'bt', 'bv',
7245
			'bw', 'by', 'bz', 'ca', 'cc', 'cd', 'cf', 'cg', 'ch', 'ci', 'ck', 'cl', 'cm',
7246
			'cn', 'co', 'cr', 'cu', 'cv', 'cx', 'cy', 'cz', 'de', 'dj', 'dk', 'dm', 'do',
7247
			'dz', 'ec', 'ee', 'eg', 'er', 'es', 'et', 'eu', 'fi', 'fj', 'fk', 'fm', 'fo',
7248
			'fr', 'ga', 'gb', 'gd', 'ge', 'gf', 'gg', 'gh', 'gi', 'gl', 'gm', 'gn', 'gp',
7249
			'gq', 'gr', 'gs', 'gt', 'gu', 'gw', 'gy', 'hk', 'hm', 'hn', 'hr', 'ht', 'hu',
7250
			'id', 'ie', 'il', 'im', 'in', 'io', 'iq', 'ir', 'is', 'it', 'je', 'jm', 'jo',
7251
			'jp', 'ke', 'kg', 'kh', 'ki', 'km', 'kn', 'kp', 'kr', 'kw', 'ky', 'kz', 'la',
7252
			'lb', 'lc', 'li', 'lk', 'lr', 'ls', 'lt', 'lu', 'lv', 'ly', 'ma', 'mc', 'md',
7253
			'me', 'mg', 'mh', 'mk', 'ml', 'mm', 'mn', 'mo', 'mp', 'mq', 'mr', 'ms', 'mt',
7254
			'mu', 'mv', 'mw', 'mx', 'my', 'mz', 'na', 'nc', 'ne', 'nf', 'ng', 'ni', 'nl',
7255
			'no', 'np', 'nr', 'nu', 'nz', 'om', 'pa', 'pe', 'pf', 'pg', 'ph', 'pk', 'pl',
7256
			'pm', 'pn', 'pr', 'ps', 'pt', 'pw', 'py', 'qa', 're', 'ro', 'rs', 'ru', 'rw',
7257
			'sa', 'sb', 'sc', 'sd', 'se', 'sg', 'sh', 'si', 'sj', 'sk', 'sl', 'sm', 'sn',
7258
			'so', 'sr', 'ss', 'st', 'su', 'sv', 'sx', 'sy', 'sz', 'tc', 'td', 'tf', 'tg',
7259
			'th', 'tj', 'tk', 'tl', 'tm', 'tn', 'to', 'tr', 'tt', 'tv', 'tw', 'tz', 'ua',
7260
			'ug', 'uk', 'us', 'uy', 'uz', 'va', 'vc', 've', 'vg', 'vi', 'vn', 'vu', 'wf',
7261
			'ws', 'ye', 'yt', 'za', 'zm', 'zw',
7262
		);
7263
7264
		// Schedule a background update, unless civilization has collapsed and/or we are having connectivity issues.
7265
		if (empty($postapocalypticNightmare))
7266
		{
7267
			$smcFunc['db_insert']('insert', '{db_prefix}background_tasks',
7268
				array('task_file' => 'string-255', 'task_class' => 'string-255', 'task_data' => 'string', 'claimed_time' => 'int'),
7269
				array('$sourcedir/tasks/UpdateTldRegex.php', 'Update_TLD_Regex', '', 0), array()
7270
			);
7271
		}
7272
	}
7273
7274
	// Tack on some "special use domain names" that aren't in DNS but may possibly resolve.
7275
	// See https://www.iana.org/assignments/special-use-domain-names/ for more info.
7276
	$tlds = array_merge($tlds, array('local', 'onion', 'test'));
7277
7278
	// Get an optimized regex to match all the TLDs
7279
	$tld_regex = build_regex($tlds);
7280
7281
	// Remember the new regex in $modSettings
7282
	updateSettings(array('tld_regex' => $tld_regex));
7283
7284
	// Redundant repetition is redundant
7285
	$done = true;
7286
}
7287
7288
/**
7289
 * Creates optimized regular expressions from an array of strings.
7290
 *
7291
 * An optimized regex built using this function will be much faster than a
7292
 * simple regex built using `implode('|', $strings)` --- anywhere from several
7293
 * times to several orders of magnitude faster.
7294
 *
7295
 * However, the time required to build the optimized regex is approximately
7296
 * equal to the time it takes to execute the simple regex. Therefore, it is only
7297
 * worth calling this function if the resulting regex will be used more than
7298
 * once.
7299
 *
7300
 * Because PHP places an upper limit on the allowed length of a regex, very
7301
 * large arrays of $strings may not fit in a single regex. Normally, the excess
7302
 * strings will simply be dropped. However, if the $returnArray parameter is set
7303
 * to true, this function will build as many regexes as necessary to accommodate
7304
 * everything in $strings and return them in an array. You will need to iterate
7305
 * through all elements of the returned array in order to test all possible
7306
 * matches.
7307
 *
7308
 * @param array $strings An array of strings to make a regex for.
7309
 * @param string $delim An optional delimiter character to pass to preg_quote().
7310
 * @param bool $returnArray If true, returns an array of regexes.
7311
 * @return string|array One or more regular expressions to match any of the input strings.
7312
 */
7313
function build_regex($strings, $delim = null, $returnArray = false)
7314
{
7315
	global $smcFunc;
7316
	static $regexes = array();
7317
7318
	// If it's not an array, there's not much to do. ;)
7319
	if (!is_array($strings))
0 ignored issues
show
introduced by
The condition is_array($strings) is always true.
Loading history...
7320
		return preg_quote(@strval($strings), $delim);
7321
7322
	$regex_key = md5(json_encode(array($strings, $delim, $returnArray)));
7323
7324
	if (isset($regexes[$regex_key]))
7325
		return $regexes[$regex_key];
7326
7327
	// The mb_* functions are faster than the $smcFunc ones, but may not be available
7328
	if (function_exists('mb_internal_encoding') && function_exists('mb_detect_encoding') && function_exists('mb_strlen') && function_exists('mb_substr'))
7329
	{
7330
		if (($string_encoding = mb_detect_encoding(implode(' ', $strings))) !== false)
7331
		{
7332
			$current_encoding = mb_internal_encoding();
7333
			mb_internal_encoding($string_encoding);
7334
		}
7335
7336
		$strlen = 'mb_strlen';
7337
		$substr = 'mb_substr';
7338
	}
7339
	else
7340
	{
7341
		$strlen = $smcFunc['strlen'];
7342
		$substr = $smcFunc['substr'];
7343
	}
7344
7345
	// This recursive function creates the index array from the strings
7346
	$add_string_to_index = function($string, $index) use (&$strlen, &$substr, &$add_string_to_index)
7347
	{
7348
		static $depth = 0;
7349
		$depth++;
7350
7351
		$first = (string) @$substr($string, 0, 1);
7352
7353
		// No first character? That's no good.
7354
		if ($first === '')
7355
		{
7356
			// A nested array? Really? Ugh. Fine.
7357
			if (is_array($string) && $depth < 20)
7358
			{
7359
				foreach ($string as $str)
7360
					$index = $add_string_to_index($str, $index);
7361
			}
7362
7363
			$depth--;
7364
			return $index;
7365
		}
7366
7367
		if (empty($index[$first]))
7368
			$index[$first] = array();
7369
7370
		if ($strlen($string) > 1)
7371
		{
7372
			// Sanity check on recursion
7373
			if ($depth > 99)
7374
				$index[$first][$substr($string, 1)] = '';
7375
7376
			else
7377
				$index[$first] = $add_string_to_index($substr($string, 1), $index[$first]);
7378
		}
7379
		else
7380
			$index[$first][''] = '';
7381
7382
		$depth--;
7383
		return $index;
7384
	};
7385
7386
	// This recursive function turns the index array into a regular expression
7387
	$index_to_regex = function(&$index, $delim) use (&$strlen, &$index_to_regex)
7388
	{
7389
		static $depth = 0;
7390
		$depth++;
7391
7392
		// Absolute max length for a regex is 32768, but we might need wiggle room
7393
		$max_length = 30000;
7394
7395
		$regex = array();
7396
		$length = 0;
7397
7398
		foreach ($index as $key => $value)
7399
		{
7400
			$key_regex = preg_quote($key, $delim);
7401
			$new_key = $key;
7402
7403
			if (empty($value))
7404
				$sub_regex = '';
7405
			else
7406
			{
7407
				$sub_regex = $index_to_regex($value, $delim);
7408
7409
				if (count(array_keys($value)) == 1)
7410
				{
7411
					$new_key_array = explode('(?' . '>', $sub_regex);
7412
					$new_key .= $new_key_array[0];
7413
				}
7414
				else
7415
					$sub_regex = '(?' . '>' . $sub_regex . ')';
7416
			}
7417
7418
			if ($depth > 1)
7419
				$regex[$new_key] = $key_regex . $sub_regex;
7420
			else
7421
			{
7422
				if (($length += strlen($key_regex . $sub_regex) + 1) < $max_length || empty($regex))
7423
				{
7424
					$regex[$new_key] = $key_regex . $sub_regex;
7425
					unset($index[$key]);
7426
				}
7427
				else
7428
					break;
7429
			}
7430
		}
7431
7432
		// Sort by key length and then alphabetically
7433
		uksort(
7434
			$regex,
7435
			function($k1, $k2) use (&$strlen)
7436
			{
7437
				$l1 = $strlen($k1);
7438
				$l2 = $strlen($k2);
7439
7440
				if ($l1 == $l2)
7441
					return strcmp($k1, $k2) > 0 ? 1 : -1;
7442
				else
7443
					return $l1 > $l2 ? -1 : 1;
7444
			}
7445
		);
7446
7447
		$depth--;
7448
		return implode('|', $regex);
7449
	};
7450
7451
	// Now that the functions are defined, let's do this thing
7452
	$index = array();
7453
	$regex = '';
7454
7455
	foreach ($strings as $string)
7456
		$index = $add_string_to_index($string, $index);
7457
7458
	if ($returnArray === true)
7459
	{
7460
		$regex = array();
7461
		while (!empty($index))
7462
			$regex[] = '(?' . '>' . $index_to_regex($index, $delim) . ')';
7463
	}
7464
	else
7465
		$regex = '(?' . '>' . $index_to_regex($index, $delim) . ')';
7466
7467
	// Restore PHP's internal character encoding to whatever it was originally
7468
	if (!empty($current_encoding))
7469
		mb_internal_encoding($current_encoding);
7470
7471
	$regexes[$regex_key] = $regex;
7472
	return $regex;
7473
}
7474
7475
/**
7476
 * Check if the passed url has an SSL certificate.
7477
 *
7478
 * Returns true if a cert was found & false if not.
7479
 *
7480
 * @param string $url to check, in $boardurl format (no trailing slash).
7481
 */
7482
function ssl_cert_found($url)
7483
{
7484
	// This check won't work without OpenSSL
7485
	if (!extension_loaded('openssl'))
7486
		return true;
7487
7488
	// First, strip the subfolder from the passed url, if any
7489
	$parsedurl = parse_url($url);
7490
	$url = 'ssl://' . $parsedurl['host'] . ':443';
7491
7492
	// Next, check the ssl stream context for certificate info
7493
	if (version_compare(PHP_VERSION, '5.6.0', '<'))
7494
		$ssloptions = array("capture_peer_cert" => true);
7495
	else
7496
		$ssloptions = array("capture_peer_cert" => true, "verify_peer" => true, "allow_self_signed" => true);
7497
7498
	$result = false;
7499
	$context = stream_context_create(array("ssl" => $ssloptions));
7500
	$stream = @stream_socket_client($url, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $context);
7501
	if ($stream !== false)
7502
	{
7503
		$params = stream_context_get_params($stream);
7504
		$result = isset($params["options"]["ssl"]["peer_certificate"]) ? true : false;
7505
	}
7506
	return $result;
7507
}
7508
7509
/**
7510
 * Check if the passed url has a redirect to https:// by querying headers.
7511
 *
7512
 * Returns true if a redirect was found & false if not.
7513
 * Note that when force_ssl = 2, SMF issues its own redirect...  So if this
7514
 * returns true, it may be caused by SMF, not necessarily an .htaccess redirect.
7515
 *
7516
 * @param string $url to check, in $boardurl format (no trailing slash).
7517
 */
7518
function https_redirect_active($url)
7519
{
7520
	// Ask for the headers for the passed url, but via http...
7521
	// Need to add the trailing slash, or it puts it there & thinks there's a redirect when there isn't...
7522
	$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

7522
	$url = /** @scrutinizer ignore-type */ str_ireplace('https://', 'http://', $url) . '/';
Loading history...
7523
	$headers = @get_headers($url);
7524
	if ($headers === false)
7525
		return false;
7526
7527
	// Now to see if it came back https...
7528
	// First check for a redirect status code in first row (301, 302, 307)
7529
	if (strstr($headers[0], '301') === false && strstr($headers[0], '302') === false && strstr($headers[0], '307') === false)
7530
		return false;
7531
7532
	// Search for the location entry to confirm https
7533
	$result = false;
7534
	foreach ($headers as $header)
7535
	{
7536
		if (stristr($header, 'Location: https://') !== false)
7537
		{
7538
			$result = true;
7539
			break;
7540
		}
7541
	}
7542
	return $result;
7543
}
7544
7545
/**
7546
 * Build query_wanna_see_board and query_see_board for a userid
7547
 *
7548
 * Returns array with keys query_wanna_see_board and query_see_board
7549
 *
7550
 * @param int $userid of the user
7551
 */
7552
function build_query_board($userid)
7553
{
7554
	global $user_info, $modSettings, $smcFunc, $db_prefix;
7555
7556
	$query_part = array();
7557
7558
	// If we come from cron, we can't have a $user_info.
7559
	if (isset($user_info['id']) && $user_info['id'] == $userid && SMF != 'BACKGROUND')
7560
	{
7561
		$groups = $user_info['groups'];
7562
		$can_see_all_boards = $user_info['is_admin'] || $user_info['can_manage_boards'];
7563
		$ignoreboards = !empty($user_info['ignoreboards']) ? $user_info['ignoreboards'] : null;
7564
	}
7565
	else
7566
	{
7567
		$request = $smcFunc['db_query']('', '
7568
			SELECT mem.ignore_boards, mem.id_group, mem.additional_groups, mem.id_post_group
7569
			FROM {db_prefix}members AS mem
7570
			WHERE mem.id_member = {int:id_member}
7571
			LIMIT 1',
7572
			array(
7573
				'id_member' => $userid,
7574
			)
7575
		);
7576
7577
		$row = $smcFunc['db_fetch_assoc']($request);
7578
7579
		if (empty($row['additional_groups']))
7580
			$groups = array($row['id_group'], $row['id_post_group']);
7581
		else
7582
			$groups = array_merge(
7583
				array($row['id_group'], $row['id_post_group']),
7584
				explode(',', $row['additional_groups'])
7585
			);
7586
7587
		// Because history has proven that it is possible for groups to go bad - clean up in case.
7588
		foreach ($groups as $k => $v)
7589
			$groups[$k] = (int) $v;
7590
7591
		$can_see_all_boards = in_array(1, $groups) || (!empty($modSettings['board_manager_groups']) && count(array_intersect($groups, explode(',', $modSettings['board_manager_groups']))) > 0);
7592
7593
		$ignoreboards = !empty($row['ignore_boards']) && !empty($modSettings['allow_ignore_boards']) ? explode(',', $row['ignore_boards']) : array();
7594
	}
7595
7596
	// Just build this here, it makes it easier to change/use - administrators can see all boards.
7597
	if ($can_see_all_boards)
7598
		$query_part['query_see_board'] = '1=1';
7599
	// Otherwise just the groups in $user_info['groups'].
7600
	else
7601
	{
7602
		$query_part['query_see_board'] = '
7603
			EXISTS (
7604
				SELECT bpv.id_board
7605
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7606
				WHERE bpv.id_group IN ('. implode(',', $groups) .')
7607
					AND bpv.deny = 0
7608
					AND bpv.id_board = b.id_board
7609
			)';
7610
7611
		if (!empty($modSettings['deny_boards_access']))
7612
			$query_part['query_see_board'] .= '
7613
			AND NOT EXISTS (
7614
				SELECT bpv.id_board
7615
				FROM ' . $db_prefix . 'board_permissions_view AS bpv
7616
				WHERE bpv.id_group IN ( '. implode(',', $groups) .')
7617
					AND bpv.deny = 1
7618
					AND bpv.id_board = b.id_board
7619
			)';
7620
	}
7621
7622
	$query_part['query_see_message_board'] = str_replace('b.', 'm.', $query_part['query_see_board']);
7623
	$query_part['query_see_topic_board'] = str_replace('b.', 't.', $query_part['query_see_board']);
7624
7625
	// Build the list of boards they WANT to see.
7626
	// This will take the place of query_see_boards in certain spots, so it better include the boards they can see also
7627
7628
	// If they aren't ignoring any boards then they want to see all the boards they can see
7629
	if (empty($ignoreboards))
7630
	{
7631
		$query_part['query_wanna_see_board'] = $query_part['query_see_board'];
7632
		$query_part['query_wanna_see_message_board'] = $query_part['query_see_message_board'];
7633
		$query_part['query_wanna_see_topic_board'] = $query_part['query_see_topic_board'];
7634
	}
7635
	// Ok I guess they don't want to see all the boards
7636
	else
7637
	{
7638
		$query_part['query_wanna_see_board'] = '(' . $query_part['query_see_board'] . ' AND b.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7639
		$query_part['query_wanna_see_message_board'] = '(' . $query_part['query_see_message_board'] . ' AND m.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7640
		$query_part['query_wanna_see_topic_board'] = '(' . $query_part['query_see_topic_board'] . ' AND t.id_board NOT IN (' . implode(',', $ignoreboards) . '))';
7641
	}
7642
7643
	return $query_part;
7644
}
7645
7646
/**
7647
 * Check if the connection is using https.
7648
 *
7649
 * @return boolean true if connection used https
7650
 */
7651
function httpsOn()
7652
{
7653
	$secure = false;
7654
7655
	if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on')
7656
		$secure = true;
7657
	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...
7658
		$secure = true;
7659
7660
	return $secure;
7661
}
7662
7663
/**
7664
 * A wrapper for `filter_var($url, FILTER_VALIDATE_URL)` that can handle URLs
7665
 * with international characters (a.k.a. IRIs)
7666
 *
7667
 * @param string $iri The IRI to test.
7668
 * @param int $flags Optional flags to pass to filter_var()
7669
 * @return string|bool Either the original IRI, or false if the IRI was invalid.
7670
 */
7671
function validate_iri($iri, $flags = null)
7672
{
7673
	$url = iri_to_url($iri);
7674
7675
	// PHP 5 doesn't recognize IPv6 addresses in the URL host.
7676
	if (version_compare(phpversion(), '7.0.0', '<'))
7677
	{
7678
		$host = parse_url((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
7679
7680
		if (strpos($host, '[') === 0 && strpos($host, ']') === strlen($host) - 1 && strpos($host, ':') !== false)
7681
			$url = str_replace($host, '127.0.0.1', $url);
7682
	}
7683
7684
	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

7684
	if (filter_var($url, FILTER_VALIDATE_URL, /** @scrutinizer ignore-type */ $flags) !== false)
Loading history...
7685
		return $iri;
7686
	else
7687
		return false;
7688
}
7689
7690
/**
7691
 * A wrapper for `filter_var($url, FILTER_SANITIZE_URL)` that can handle URLs
7692
 * with international characters (a.k.a. IRIs)
7693
 *
7694
 * Note: The returned value will still be an IRI, not a URL. To convert to URL,
7695
 * feed the result of this function to iri_to_url()
7696
 *
7697
 * @param string $iri The IRI to sanitize.
7698
 * @return string|bool The sanitized version of the IRI
7699
 */
7700
function sanitize_iri($iri)
7701
{
7702
	// Encode any non-ASCII characters (but not space or control characters of any sort)
7703
	// Also encode '%' in order to preserve anything that is already percent-encoded.
7704
	$iri = preg_replace_callback(
7705
		'~[^\x00-\x7F\pZ\pC]|%~u',
7706
		function($matches)
7707
		{
7708
			return rawurlencode($matches[0]);
7709
		},
7710
		$iri
7711
	);
7712
7713
	// Perform normal sanitization
7714
	$iri = filter_var($iri, FILTER_SANITIZE_URL);
7715
7716
	// Decode the non-ASCII characters
7717
	$iri = rawurldecode($iri);
7718
7719
	return $iri;
7720
}
7721
7722
/**
7723
 * Converts a URL with international characters (an IRI) into a pure ASCII URL
7724
 *
7725
 * Uses Punycode to encode any non-ASCII characters in the domain name, and uses
7726
 * standard URL encoding on the rest.
7727
 *
7728
 * @param string $iri A IRI that may or may not contain non-ASCII characters.
7729
 * @return string|bool The URL version of the IRI.
7730
 */
7731
function iri_to_url($iri)
7732
{
7733
	global $smcFunc, $sourcedir;
7734
7735
	// Weird stuff can happen if parse_url() is given un-normalized Unicode.
7736
	$iri = $smcFunc['normalize'](sanitize_iri($iri), 'c');
7737
7738
	$host = parse_url((strpos($iri, '//') === 0 ? 'http:' : '') . $iri, PHP_URL_HOST);
7739
7740
	if (!empty($host))
7741
	{
7742
		if (!function_exists('idn_to_ascii'))
7743
			require_once($sourcedir . '/Subs-Compat.php');
7744
7745
		// Convert the host using the Punycode algorithm
7746
		$encoded_host = idn_to_ascii($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
7747
7748
		$pos = strpos($iri, $host);
7749
	}
7750
	else
7751
	{
7752
		$encoded_host = '';
7753
		$pos = 0;
7754
	}
7755
7756
	$before_host = substr($iri, 0, $pos);
7757
	$after_host = substr($iri, $pos + strlen($host));
7758
7759
	// Encode any disallowed characters in the rest of the URL
7760
	$unescaped = array(
7761
		'%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&',
7762
		'%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*',
7763
		'%2B' => '+', '%2C' => ',', '%2F' => '/', '%3A' => ':',
7764
		'%3B' => ';', '%3D' => '=', '%3F' => '?', '%40' => '@',
7765
		'%25' => '%',
7766
	);
7767
7768
	$before_host = strtr(rawurlencode($before_host), $unescaped);
7769
	$after_host = strtr(rawurlencode($after_host), $unescaped);
7770
7771
	return $before_host . $encoded_host . $after_host;
7772
}
7773
7774
/**
7775
 * Decodes a URL containing encoded international characters to UTF-8
7776
 *
7777
 * Decodes any Punycode encoded characters in the domain name, then uses
7778
 * standard URL decoding on the rest.
7779
 *
7780
 * @param string $url The pure ASCII version of a URL.
7781
 * @return string|bool The UTF-8 version of the URL.
7782
 */
7783
function url_to_iri($url)
7784
{
7785
	global $sourcedir;
7786
7787
	$host = parse_url((strpos($url, '//') === 0 ? 'http:' : '') . $url, PHP_URL_HOST);
7788
7789
	if (!empty($host))
7790
	{
7791
		if (!function_exists('idn_to_utf8'))
7792
			require_once($sourcedir . '/Subs-Compat.php');
7793
7794
		// Decode the domain from Punycode
7795
		$decoded_host = idn_to_utf8($host, IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
7796
7797
		$pos = strpos($url, $host);
7798
	}
7799
	else
7800
	{
7801
		$decoded_host = '';
7802
		$pos = 0;
7803
	}
7804
7805
	$before_host = substr($url, 0, $pos);
7806
	$after_host = substr($url, $pos + strlen($host));
7807
7808
	// Decode the rest of the URL
7809
	$before_host = rawurldecode($before_host);
7810
	$after_host = rawurldecode($after_host);
7811
7812
	return $before_host . $decoded_host . $after_host;
7813
}
7814
7815
/**
7816
 * Ensures SMF's scheduled tasks are being run as intended
7817
 *
7818
 * If the admin activated the cron_is_real_cron setting, but the cron job is
7819
 * not running things at least once per day, we need to go back to SMF's default
7820
 * behaviour using "web cron" JavaScript calls.
7821
 */
7822
function check_cron()
7823
{
7824
	global $modSettings, $smcFunc, $txt;
7825
7826
	if (!empty($modSettings['cron_is_real_cron']) && time() - @intval($modSettings['cron_last_checked']) > 84600)
7827
	{
7828
		$request = $smcFunc['db_query']('', '
7829
			SELECT COUNT(*)
7830
			FROM {db_prefix}scheduled_tasks
7831
			WHERE disabled = {int:not_disabled}
7832
				AND next_time < {int:yesterday}',
7833
			array(
7834
				'not_disabled' => 0,
7835
				'yesterday' => time() - 84600,
7836
			)
7837
		);
7838
		list($overdue) = $smcFunc['db_fetch_row']($request);
7839
		$smcFunc['db_free_result']($request);
7840
7841
		// If we have tasks more than a day overdue, cron isn't doing its job.
7842
		if (!empty($overdue))
7843
		{
7844
			loadLanguage('ManageScheduledTasks');
7845
			log_error($txt['cron_not_working']);
7846
			updateSettings(array('cron_is_real_cron' => 0));
7847
		}
7848
		else
7849
			updateSettings(array('cron_last_checked' => time()));
7850
	}
7851
}
7852
7853
/**
7854
 * Sends an appropriate HTTP status header based on a given status code
7855
 *
7856
 * @param int $code The status code
7857
 * @param string $status The string for the status. Set automatically if not provided.
7858
 */
7859
function send_http_status($code, $status = '')
7860
{
7861
	global $sourcedir;
7862
7863
	$statuses = array(
7864
		204 => 'No Content',
7865
		206 => 'Partial Content',
7866
		304 => 'Not Modified',
7867
		400 => 'Bad Request',
7868
		403 => 'Forbidden',
7869
		404 => 'Not Found',
7870
		410 => 'Gone',
7871
		500 => 'Internal Server Error',
7872
		503 => 'Service Unavailable',
7873
	);
7874
7875
	$protocol = preg_match('~^\s*(HTTP/[12]\.\d)\s*$~i', $_SERVER['SERVER_PROTOCOL'], $matches) ? $matches[1] : 'HTTP/1.0';
7876
7877
	// Typically during these requests, we have cleaned the response (ob_*clean), ensure these headers exist.
7878
	require_once($sourcedir . '/Security.php');
7879
	frameOptionsHeader();
7880
	corsPolicyHeader();
7881
7882
	if (!isset($statuses[$code]) && empty($status))
7883
		header($protocol . ' 500 Internal Server Error');
7884
	else
7885
		header($protocol . ' ' . $code . ' ' . (!empty($status) ? $status : $statuses[$code]));
7886
}
7887
7888
/**
7889
 * Concatenates an array of strings into a grammatically correct sentence list
7890
 *
7891
 * Uses formats defined in the language files to build the list appropropriately
7892
 * for the currently loaded language.
7893
 *
7894
 * @param array $list An array of strings to concatenate.
7895
 * @return string The localized sentence list.
7896
 */
7897
function sentence_list($list)
7898
{
7899
	global $txt;
7900
7901
	// Make sure the bare necessities are defined
7902
	if (empty($txt['sentence_list_format']['n']))
7903
		$txt['sentence_list_format']['n'] = '{series}';
7904
	if (!isset($txt['sentence_list_separator']))
7905
		$txt['sentence_list_separator'] = ', ';
7906
	if (!isset($txt['sentence_list_separator_alt']))
7907
		$txt['sentence_list_separator_alt'] = '; ';
7908
7909
	// Which format should we use?
7910
	if (isset($txt['sentence_list_format'][count($list)]))
7911
		$format = $txt['sentence_list_format'][count($list)];
7912
	else
7913
		$format = $txt['sentence_list_format']['n'];
7914
7915
	// Do we want the normal separator or the alternate?
7916
	$separator = $txt['sentence_list_separator'];
7917
	foreach ($list as $item)
7918
	{
7919
		if (strpos($item, $separator) !== false)
7920
		{
7921
			$separator = $txt['sentence_list_separator_alt'];
7922
			$format = strtr($format, trim($txt['sentence_list_separator']), trim($separator));
7923
			break;
7924
		}
7925
	}
7926
7927
	$replacements = array();
7928
7929
	// Special handling for the last items on the list
7930
	$i = 0;
7931
	while (empty($done))
7932
	{
7933
		if (strpos($format, '{'. --$i . '}') !== false)
7934
			$replacements['{'. $i . '}'] = array_pop($list);
7935
		else
7936
			$done = true;
7937
	}
7938
	unset($done);
7939
7940
	// Special handling for the first items on the list
7941
	$i = 0;
7942
	while (empty($done))
7943
	{
7944
		if (strpos($format, '{'. ++$i . '}') !== false)
7945
			$replacements['{'. $i . '}'] = array_shift($list);
7946
		else
7947
			$done = true;
7948
	}
7949
	unset($done);
7950
7951
	// Whatever is left
7952
	$replacements['{series}'] = implode($separator, $list);
7953
7954
	// Do the deed
7955
	return strtr($format, $replacements);
7956
}
7957
7958
/**
7959
 * Truncate an array to a specified length
7960
 *
7961
 * @param array $array The array to truncate
7962
 * @param int $max_length The upperbound on the length
7963
 * @param int $deep How levels in an multidimensional array should the function take into account.
7964
 * @return array The truncated array
7965
 */
7966
function truncate_array($array, $max_length = 1900, $deep = 3)
7967
{
7968
	$array = (array) $array;
7969
7970
	$curr_length = array_length($array, $deep);
7971
7972
	if ($curr_length <= $max_length)
7973
		return $array;
7974
7975
	else
7976
	{
7977
		// Truncate each element's value to a reasonable length
7978
		$param_max = floor($max_length / count($array));
7979
7980
		$current_deep = $deep - 1;
7981
7982
		foreach ($array as $key => &$value)
7983
		{
7984
			if (is_array($value))
7985
				if ($current_deep > 0)
7986
					$value = truncate_array($value, $current_deep);
7987
7988
			else
7989
				$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

7989
				$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

7989
				$value = substr($value, 0, /** @scrutinizer ignore-type */ $param_max - strlen($key) - 5);
Loading history...
7990
		}
7991
7992
		return $array;
7993
	}
7994
}
7995
7996
/**
7997
 * array_length Recursive
7998
 * @param array $array
7999
 * @param int $deep How many levels should the function
8000
 * @return int
8001
 */
8002
function array_length($array, $deep = 3)
8003
{
8004
	// Work with arrays
8005
	$array = (array) $array;
8006
	$length = 0;
8007
8008
	$deep_count = $deep - 1;
8009
8010
	foreach ($array as $value)
8011
	{
8012
		// Recursive?
8013
		if (is_array($value))
8014
		{
8015
			// No can't do
8016
			if ($deep_count <= 0)
8017
				continue;
8018
8019
			$length += array_length($value, $deep_count);
8020
		}
8021
		else
8022
			$length += strlen($value);
8023
	}
8024
8025
	return $length;
8026
}
8027
8028
/**
8029
 * Compares existance request variables against an array.
8030
 *
8031
 * The input array is associative, where keys denote accepted values
8032
 * in a request variable denoted by `$req_val`. Values can be:
8033
 *
8034
 * - another associative array where at least one key must be found
8035
 *   in the request and their values are accepted request values.
8036
 * - A scalar value, in which case no furthur checks are done.
8037
 *
8038
 * @param array $array
8039
 * @param string $req_var request variable
8040
 *
8041
 * @return bool whether any of the criteria was satisfied
8042
 */
8043
function is_filtered_request(array $array, $req_var)
8044
{
8045
	$matched = false;
8046
	if (isset($_REQUEST[$req_var], $array[$_REQUEST[$req_var]]))
8047
	{
8048
		if (is_array($array[$_REQUEST[$req_var]]))
8049
		{
8050
			foreach ($array[$_REQUEST[$req_var]] as $subtype => $subnames)
8051
				$matched |= isset($_REQUEST[$subtype]) && in_array($_REQUEST[$subtype], $subnames);
8052
		}
8053
		else
8054
			$matched = true;
8055
	}
8056
8057
	return (bool) $matched;
8058
}
8059
8060
/**
8061
 * Clean up the XML to make sure it doesn't contain invalid characters.
8062
 *
8063
 * See https://www.w3.org/TR/xml/#charsets
8064
 *
8065
 * @param string $string The string to clean
8066
 * @return string The cleaned string
8067
 */
8068
function cleanXml($string)
8069
{
8070
	global $context;
8071
8072
	$illegal_chars = array(
8073
		// Remove all ASCII control characters except \t, \n, and \r.
8074
		"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07", "\x08",
8075
		"\x0B", "\x0C", "\x0E", "\x0F", "\x10", "\x11", "\x12", "\x13", "\x14",
8076
		"\x15", "\x16", "\x17", "\x18", "\x19", "\x1A", "\x1B", "\x1C", "\x1D",
8077
		"\x1E", "\x1F",
8078
		// Remove \xFFFE and \xFFFF
8079
		"\xEF\xBF\xBE", "\xEF\xBF\xBF",
8080
	);
8081
8082
	$string = str_replace($illegal_chars, '', $string);
8083
8084
	// The Unicode surrogate pair code points should never be present in our
8085
	// strings to begin with, but if any snuck in, they need to be removed.
8086
	if (!empty($context['utf8']) && strpos($string, "\xED") !== false)
8087
		$string = preg_replace('/\xED[\xA0-\xBF][\x80-\xBF]/', '', $string);
8088
8089
	return $string;
8090
}
8091
8092
/**
8093
 * Escapes (replaces) characters in strings to make them safe for use in javascript
8094
 *
8095
 * @param string $string The string to escape
8096
 * @return string The escaped string
8097
 */
8098
function JavaScriptEscape($string)
8099
{
8100
	global $scripturl;
8101
8102
	return '\'' . strtr($string, array(
8103
		"\r" => '',
8104
		"\n" => '\\n',
8105
		"\t" => '\\t',
8106
		'\\' => '\\\\',
8107
		'\'' => '\\\'',
8108
		'</' => '<\' + \'/',
8109
		'<script' => '<scri\'+\'pt',
8110
		'<body>' => '<bo\'+\'dy>',
8111
		'<a href' => '<a hr\'+\'ef',
8112
		$scripturl => '\' + smf_scripturl + \'',
8113
	)) . '\'';
8114
}
8115
8116
function tokenTxtReplace($stringSubject = '')
8117
{
8118
	global $txt;
8119
8120
	if (empty($stringSubject))
8121
		return '';
8122
8123
	$translatable_tokens = preg_match_all('/{(.*?)}/' , $stringSubject, $matches);
0 ignored issues
show
Unused Code introduced by
The assignment to $translatable_tokens is dead and can be removed.
Loading history...
8124
	$toFind = array();
8125
	$replaceWith = array();
8126
8127
	if (!empty($matches[1]))
8128
		foreach ($matches[1] as $token) {
8129
			$toFind[] = '{' . $token . '}';
8130
			$replaceWith[] = isset($txt[$token]) ? $txt[$token] : $token;
8131
		}
8132
8133
	return str_replace($toFind, $replaceWith, $stringSubject);
8134
}
8135
8136
?>